File Coverage

blib/lib/GD/Map/Mercator.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             =pod
2              
3             =begin classdoc
4              
5             Renders map images using a Mercator projection
6             applied to GIS datasets available from
7             CIA World DataBank II project.
8            

9             Copyright© 2008, Dean Arnold, Presicient Corp., USA
10            

11             Permission is granted to use, copy, modify, and redistribute this software
12             under the terms of the
13             Perl Artistic License.
14              
15             @author D. Arnold
16             @since 2008-Jan-29
17             @see GD::Map
18             @see Geo::Mercator
19              
20             =end classdoc
21              
22             =cut
23              
24             package GD::Map::Mercator;
25              
26 1     1   40645 use strict;
  1         2  
  1         232  
27 1     1   7 use warnings;
  1         3  
  1         34  
28              
29 1     1   989 use GD;
  0            
  0            
30             use GD::Polyline;
31              
32             our $VERSION = '1.03';
33              
34              
35             #
36             # keep map of regions to lat/long bounding boxes,
37             # so we can optimize dataset loading
38             #
39             our %regions = (
40             'africa-bdy' => [ -30.658889, -17.075556, 41.594722,53.089722],
41             'africa-cil' => [ -54.462778, -25.360556, 65.650278,77.588889],
42             'africa-riv' => [ -34.400278, -16.708611, 42.038056,55.2525],
43             'asia-bdy' => [ -9.126944, 19.001389, 54.476667,141.008889],
44             'asia-cil' => [ -54.753889, -190.351944, 81.851944,180.0],
45             'asia-riv' => [ -46.569722, -179.988056, 74.412222,180.0],
46             'europe-bdy' => [ 36.151389, -8.751389, 70.088889,31.586667],
47             'europe-cil' => [ 34.808889, -31.29, 71.165000,31.250278],
48             'europe-riv' => [ 36.566111, -21.791944, 69.999722,29.468889],
49             'namer-bdy' => [ 41.675556, -141.003056, 69.645556,-66.901111],
50             'namer-cil' => [ 24.538333, -168.132778, 83.623611,-12.155],
51             'namer-pby' => [ 29.706944, -139.047778, 68.900556,-57.1],
52             'namer-riv' => [ 25.768333, -166.053056, 74.032222,-54.470833],
53             'samer-bdy' => [ -55.556389, -117.1225, 32.718333,-51.682778],
54             'samer-cil' => [ -85.470278, -179.987778, 33.003333,179.976111],
55             'samer-riv' => [ -52.733333, -117.126111, 32.718333,-34.917222],
56             );
57              
58             our %datasets = (
59             africa => [ 'bdy', 'cil', 'riv' ],
60             asia => [ 'bdy', 'cil', 'riv' ],
61             europe => [ 'bdy', 'cil', 'riv' ],
62             namer => [ 'bdy', 'cil', 'riv', 'pby' ],
63             samer => [ 'bdy', 'cil', 'riv' ],
64             );
65              
66             our %colors = (
67             white => [255,255,255],
68             lgray => [191,191,191],
69             gray => [127,127,127],
70             dgray => [63,63,63],
71             black => [0,0,0],
72             lblue => [0,0,255],
73             blue => [0,0,191],
74             dblue => [0,0,127],
75             gold => [255,215,0],
76             lyellow => [255,255,0],
77             yellow => [191,191,0],
78             dyellow => [127,127,0],
79             lgreen => [0,255,0],
80             green => [0,191,0],
81             dgreen => [0,127,0],
82             lred => [255,0,0],
83             red => [191,0,0],
84             dred => [127,0,0],
85             lpurple => [255,0,255],
86             purple => [191,0,191],
87             dpurple => [127,0,127],
88             lorange => [255,183,0],
89             orange => [255,127,0],
90             pink => [255,183,193],
91             dpink => [255,105,180],
92             marine => [127,127,255],
93             cyan => [0,255,255],
94             lbrown => [210,180,140],
95             dbrown => [165,42,42],
96             transparent => [1,1,1]
97             );
98              
99             my %valid_fmt = qw(png newFromPng gif newFromGif jpg newFromJpeg jpeg newFromJpeg);
100              
101             =pod
102              
103             =begin classdoc
104              
105             @constructor
106              
107             Create an instance of GD::Map::Mercator. Either creates a
108             new basemap image from the specified minimum/maximum latitude/longitude
109             values, or loads an existing basemap of the given name. Applies
110             the Mercator Projection
111             to datapoints collected from the
112             CIA World Databank II dataset
113             to render a map image.
114            

115             The map region to be rendered is specified by providing a set of
116             minimum/maximum latitude/longitude values (in degrees) defining the
117             bounding box of the region to be mapped. If the bounding box coordinates
118             are not specified, this object attempts to load a pre-existing
119             map image and data. When a map is rendered, an additional file
120             of configuration data is saved containing the bounding box coordinates
121             in both latitude/longitude and Mercator distances (in meters).
122            

123             Once the base map has been created or loaded, the application may
124             use this object to
125              
126            
127            
  • access the GD object to directly manipulate it,
  • 128            
  • translate latitude/longitude coordinates to pixel coordinates
  • 129             within the image
    130            
  • convert pixel coordinates back to latitude/longitude coordinates
  • 131            
  • rescale the image and its associated configuration data
  • 132            
  • extract sub-images from the map to create new map images
  • 133            
    134            

    135              
    136             NOTES:
    137            
    138            
  • The Mercator projection is subject to severe dimensional
  • 139             distortions near the poles. Use of map coordinates above 70 degrees or
    140             below -70 degrees latitude is discouraged.
    141              
    142            
  • Latitiude values are specified in degrees between
  • 143             +90 and -90, where negative values are in the southern hemisphere;
    144             longitude values are in degrees between +180 and -180, with negative values
    145             in the western hemisphere.
    146              
    147            
  • This package uses the "Mercated" binary datasets generated
  • 148             by wdb2merc. These datasets must be generated before
    149             using this package.
    150            
    151              
    152             @optional basemap_path directory path for basemap image and datafile. If a
    153             new map is generated, its image and config files will ba saved in this
    154             path; if using an existing map, its image and config files are loaded
    155             from this path. Note that, if creating a new map, this parameter is optional.
    156              
    157             @optional basemap_name filename of basemap image and data; may contain
    158             the image format suffix ('.png', '.gif', '.jpg', '.jpeg'); if not
    159             suffixed, defaults to '.png'. The config data for the map is stored
    160             in a file of the same name, but with the format suffix replaced by
    161             '.conf'. Note that, if creating a new map, this parameter is optional.
    162              
    163             @optional data_path directory path to the WDB data files; required if a new basemap
    164             is being rendered, or a submap may be extracted/scaled.
    165              
    166             @optional min_lat minimum latitude in degrees; may be fractional. If not specified,
    167             and no other latitude/longitude values are specified, this object attempts to
    168             load a pre-existing basemap. Dies if not specified when other lat/long values
    169             are specified.
    170             @optional min_long minimum longitude in degrees; same rules apply as for min_lat
    171             @optional max_lat maximum latitude in degrees; same rules apply as for min_lat
    172             @optional max_long maximum longitude in degrees; same rules apply as for min_lat
    173             Note that max_long may be negative, and min_long postitive, in which case
    174             the rendered basemap will span the antipodal meridian (i.e., where longitude crosses from +180
    175             to -180)
    176             @optional width width of the basemap image in pixels; default 400. This
    177             parameter is actually an upper bound, as the process of rendering adjusts the
    178             image width and height to properly reflect the scaling of the Mercator
    179             projection.
    180             @optional height height of the basemap image in pixels; default 400. This
    181             parameter is actually an upper bound, as the process of rendering adjusts the
    182             image width and height to properly reflect the scaling of the Mercator
    183             projection.
    184             @optional background background color of basemap image; default 'white'. May be either
    185             a named color supported by GD::Color, or an arrayref of RGB values.
    186             @optional foreground foreground color of basemap image (i.e., color of lines drawn);
    187             default 'black'. May be either a named color supported by GD::Color,
    188             or an arrayref of RGB values.
    189             @optional thickness thickness of lines in pixels; default 2
    190             @optional omit arrayref of dataset types to omit. Each continent may have
    191             any of 'bdy' (body), 'cli' (coast/islands), 'pby' (political boundry), or
    192             'riv' (rivers) datasets. To omit one or more of them, include them
    193             in this omit list, e.g., omit => [ 'riv' ] omits rivers.
    194             @optional keep hashref mapping dataset types to an arrayref of
    195             latitude, longitude bounding box coordinates. Useful for filtering,
    196             e.g., all coasts/islands internal to a region, but keeping
    197             ocean coastal regions.
    198             @optional save_coords name of file to write pixel coordinates of the rendered
    199             image segments. Useful for e.g., creating HTML imagemaps, etc.
    200             @optional silent if true, no progress or diagnostic information is emitted;
    201             default false
    202              
    203             @returns an instance of GD::Map::Mercator
    204              
    205             =end classdoc
    206              
    207             =cut
    208              
    209             sub new {
    210             my ($class, %opts) = @_;
    211              
    212             if ($opts{data_path}) {
    213             die "data_path $opts{data_path} not found"
    214             unless(-d $opts{data_path});
    215              
    216             $opts{data_path} .= '/'
    217             unless (substr($opts{data_path}, -1) eq '/');
    218             }
    219              
    220             if ($opts{basemap_path}) {
    221             die "basemap_path $opts{basemap_path} not found."
    222             unless(-d $opts{basemap_path});
    223            
    224             $opts{basemap_path} .= '/'
    225             unless (substr($opts{basemap_path}, -1) eq '/');
    226             }
    227             else {
    228             $opts{basemap_path} = '';
    229             }
    230              
    231             my ($name, $fmt) = ('', 'png');
    232             if ($opts{basemap_name}) {
    233             $opts{basemap_name} .= '.png'
    234             unless $opts{basemap_name}=~/\.(?:png|gif|jpg|jpeg)$/;
    235              
    236             ($name, $fmt) = ($opts{basemap_name}=~/^(.+)\.(png|gif|jpg|jpeg)$/);
    237             }
    238            
    239             $opts{silent} = 0 unless exists $opts{silent};
    240             my $self = {
    241             data_path => $opts{data_path},
    242             basemap_path => $opts{basemap_path},
    243             basemap_loc => $opts{basemap_path} . $name,
    244             imgfmt => $fmt,
    245             verbose => !$opts{silent},
    246             thickness => $opts{thickness} || 2,
    247             foreground => $opts{foreground} || 'black',
    248             background => $opts{background} || 'white',
    249             keeps => $opts{keep} || {},
    250             omit => $opts{omit},
    251             save_coords => $opts{save_coords},
    252             };
    253             bless $self, $class;
    254             #
    255             # if min/max info is provided, create a new basemap
    256             # else attempt to load existing basemap
    257             #
    258             my $haspts = 0;
    259             $haspts += defined($opts{$_}) ? 1 : 0
    260             foreach (qw(min_lat min_long max_lat max_long));
    261              
    262             die "Incomplete latitude/longitude datapoints provided."
    263             if ($haspts > 0) && ($haspts < 4);
    264             #
    265             # attempt to load existing basemap; dies on any error
    266             #
    267             die "No basemap path or coordinates provided."
    268             unless $haspts || ($self->{basemap_path} && $self->{basemap_loc});
    269              
    270             return $self->_load_basemap()
    271             unless $haspts;
    272             #
    273             # create a new basemap (maybe we should go ahead and check for a matching
    274             # existing basemap ?)
    275             #
    276             die "No data_path specified."
    277             unless $self->{data_path};
    278              
    279             my ($minlat, $minlong, $maxlat, $maxlong, $width, $height) =
    280             ($opts{min_lat}, $opts{min_long},
    281             $opts{max_lat}, $opts{max_long},
    282             $opts{width} || 400, $opts{height} || 400);
    283            
    284             my $mercator;
    285             $self->{mercator} = $mercator = GD::Map::Mercator::Projector->new(
    286             $minlat, $minlong, $maxlat, $maxlong, $width, $height, $self->{verbose});
    287              
    288             my ($bg, $fg, $linew) = @$self{qw(background foreground thickness)};
    289             $| = 1 if $self->{verbose};
    290             #
    291             # create empty image before loading data
    292             # Note that image dimensions are adjusted by the projector
    293             #
    294             print "Creating GD image ($width x $height)\n"
    295             if $self->{verbose};
    296             ($width, $height) = $mercator->dimensions();
    297             my $img = $self->{image} = GD::Image->new($width, $height);
    298             $self->{fg} = ref $fg ? $img->colorAllocate(@$fg) :
    299             ($fg=~/^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i)
    300             ? $img->colorAllocate(hex($1), hex($2), hex($3))
    301             : $img->colorAllocate(@{$colors{$fg}});
    302             $self->{bg} = ref $bg ? $img->colorAllocate(@$bg) :
    303             ($bg=~/^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i)
    304             ? $img->colorAllocate(hex($1), hex($2), hex($3))
    305             : $img->colorAllocate(@{$colors{$bg}});
    306             $img->filledRectangle(0,0,$width-1,$height-1,$self->{bg});
    307             $img->setThickness($linew);
    308              
    309             my @xy;
    310             my $seg = [];
    311             #
    312             # check which of the datasets we need
    313             #
    314             my @regions = ();
    315             my %omits = ();
    316             map $omits{$_} = 1, @{$opts{omit}}
    317             if $opts{omit};
    318            
    319             foreach (keys %regions) {
    320             if ((!$omits{substr($_, -3)}) &&
    321             (
    322             (($regions{$_}[0] <= $minlat) &&
    323             ($regions{$_}[2] >= $minlat)) ||
    324             (($regions{$_}[0] <= $maxlat) &&
    325             ($regions{$_}[2] >= $maxlat)) ||
    326             (($regions{$_}[0] >= $minlat) &&
    327             ($regions{$_}[2] <= $maxlat))
    328             ) &&
    329             (
    330             (($regions{$_}[1] <= $minlong) &&
    331             ($regions{$_}[3] >= $minlong)) ||
    332             (($regions{$_}[1] <= $maxlong) &&
    333             ($regions{$_}[3] >= $maxlong)) ||
    334             (($regions{$_}[1] >= $minlong) &&
    335             ($regions{$_}[3] <= $maxlong))
    336             )) {
    337             print "Using $_\n" if $self->{verbose};
    338             push(@regions, $_);
    339             }
    340             }
    341             # open each of the data files
    342             my $oldfd = select(STDOUT);
    343             $| = 1;
    344             select $oldfd;
    345             foreach (@regions) {
    346             my $datapath = "$opts{data_path}$_.bin";
    347             print "\nLoading datafile $datapath\n"
    348             if $self->{verbose};
    349             my $fd;
    350             print "Skipping $datapath ($!)\n" and next
    351             unless open $fd, $datapath;
    352             binmode $fd;
    353             $mercator->filter($fd, $self, $_, $self->{keep}{$_});
    354             close $fd;
    355             }
    356              
    357             return $self->{basemap_loc}
    358             ? $self->save("$self->{basemap_loc}.$self->{imgfmt}")
    359             : $self;
    360             }
    361              
    362             =pod
    363              
    364             =begin classdoc
    365              
    366             Return this object's configuration information.
    367              
    368             @returnlist a list of the latitude/longitude bounding box coordinates (in degrees),
    369             the Mercator distance bounding box coordinates (in meters), and the final
    370             width and height (in pixels) of the associated image, i.e.,
    371            
     
    372             ($minlat, $minlong, $maxlat, $maxlong,
    373             $minmerclong, $minmerclat, $maxmerclong, $maxmerclat,
    374             $width, $height)
    375            
    376              
    377             =end classdoc
    378              
    379             =cut
    380              
    381             sub config {
    382             return $_[0]->{mercator}->config();
    383             }
    384              
    385             =pod
    386              
    387             =begin classdoc
    388              
    389             Extract a submap from this object's basemap.
    390             Given latitude/longitude bounding box coordinates,
    391             creates a new GD::Map::Mercator object with data and
    392             image contained by the specified bounding box. The extracted
    393             map may optionally be scaled.
    394            

    395             Note that any saved coordinates
    396             will be lost unless a scale operation is applied to regenerate
    397             them.
    398              
    399             @param $minlat minimum latitude of bounding box
    400             @param $minlong minimum longitude of bounding box
    401             @param $maxlat maximum latitude of bounding box
    402             @param $maxlong maximum longitude of bounding box
    403             @optional $scale any scaling to be applied to the submap
    404              
    405             @returns a new GD::Map::Mercator object
    406              
    407             =end classdoc
    408              
    409             =cut
    410              
    411             sub extract {
    412             my ($self, $minlat, $minlong, $maxlat, $maxlong, $scale) = @_;
    413              
    414             my @coords = $self->{mercator}->config();
    415            
    416             $@ = 'Specified region outside the bounds of this map.',
    417             return undef
    418             if ($minlat < $coords[0]) || ($maxlat > $coords[2]) ||
    419             ($minlong < $coords[1]) || ($maxlong > $coords[3]);
    420              
    421             my ($xmin, $ymin, $xminmerc, $yminmerc) = $self->{mercator}->project($minlat, $minlong);
    422             my ($xmax, $ymax, $xmaxmerc, $ymaxmerc) = $self->{mercator}->project($maxlat, $maxlong);
    423             my ($width, $height) = (($xmax - $xmin + 1), ($ymax - $ymin + 1));
    424              
    425             my $class = ref $self;
    426             if ($scale) {
    427             #
    428             # just create a new map from scratch that scales the selected region
    429             #
    430             $width *= $scale;
    431             $height *= $scale;
    432            
    433             return ${class}->new(
    434             data_path => $self->{data_path},
    435             min_lat => $minlat,
    436             min_long => $minlong,
    437             max_lat => $maxlat,
    438             max_long => $maxlong,
    439             width => $width,
    440             height => $height,
    441             verbose => $self->{verbose},
    442             thickness => $self->{thickness},
    443             foreground => $self->{foreground},
    444             background => $self->{background},
    445             keep => $self->{keep},
    446             omit => $self->{omit},
    447             save_coords => $self->{save_coords},
    448             );
    449             }
    450              
    451             my $newmap = {
    452             data_path => $self->{data_path},
    453             min_lat => $minlat,
    454             min_long => $minlong,
    455             max_lat => $maxlat,
    456             max_long => $maxlong,
    457             width => $width,
    458             height => $height,
    459             verbose => $self->{verbose},
    460             thickness => $self->{thickness},
    461             foreground => $self->{foreground},
    462             background => $self->{background},
    463             keep => $self->{keep},
    464             omit => $self->{omit},
    465             save_coords => $self->{save_coords},
    466             };
    467             $newmap->{mercator} = GD::Map::Mercator::Projector->new(
    468             $minlat, $minlong, $maxlat, $maxlong, $width, $height, $self->{verbose});
    469             ($width, $height) = $newmap->{mercator}->dimensions();
    470             my ($fg, $bg) = @$self{qw(foreground background)};
    471             my $img = $newmap->{image} = GD::Image->new($width, $height);
    472             $newmap->{fg} = ref $fg ? $img->colorAllocate(@$fg) :
    473             ($fg=~/^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i)
    474             ? $img->colorAllocate(hex($1), hex($2), hex($3))
    475             : $img->colorAllocate(@{$colors{$fg}});
    476             $newmap->{bg} = ref $bg ? $img->colorAllocate(@$bg) :
    477             ($bg=~/^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i)
    478             ? $img->colorAllocate(hex($1), hex($2), hex($3))
    479             : $img->colorAllocate(@{$colors{$bg}});
    480             $img->filledRectangle(1,1,$width,$height,$newmap->{bg});
    481             $img->setThickness($newmap->{thickness});
    482             $newmap->{image}->copy($self->{image}, 0, 0, $xmax, $ymax, $width, $height);
    483              
    484             return bless $newmap, $class;
    485             }
    486              
    487             =pod
    488              
    489             =begin classdoc
    490              
    491             Create a new scaled map from this object's basemap.
    492             Causes a recalculation/rerendering of the map, in order
    493             to properly render details on zoom-in scaling, or smooth
    494             details on zom-out.
    495              
    496             @param $scale scaling factor; the width and height of
    497             this object's image are multiplied by this factor
    498             to generate the new GD::Map::Mercator object
    499              
    500             @returns a new GD::Map::Mercator object
    501              
    502             =end classdoc
    503              
    504             =cut
    505              
    506             sub scale {
    507             my ($self, $scale) = @_;
    508            
    509             my $class = ref $self;
    510             my @coords = $self->{mercator}->config();
    511            
    512             return ${class}->new(
    513             data_path => $self->{data_path},
    514             min_lat => $coords[0],
    515             min_long => $coords[1],
    516             max_lat => $coords[2],
    517             max_long => $coords[3],
    518             width => $coords[-2] * $scale,
    519             height => $coords[-1] * $scale,
    520             silent => !$self->{verbose},
    521             thickness => $self->{thickness},
    522             foreground => $self->{foreground},
    523             background => $self->{background},
    524             keep => $self->{keep},
    525             omit => $self->{omit},
    526             save_coords => $self->{save_coords},
    527             );
    528             }
    529              
    530             =pod
    531              
    532             =begin classdoc
    533              
    534             Return the pixel and Mercator distance coordinates for the
    535             input latitude/longitude coordinates. Note that the input
    536             coordinates do not need to
    537             be within the bounding box of this object's map.
    538            

    539             Multiple sets of input coordinates may be supplied, in which
    540             case multiple coordinate results will be returned (4 output values
    541             per input coordinate pair).
    542              
    543             @param $latitude latitude of coordinate to project (in degrees)
    544             @param $longitude longitude of coordinate to project (in degrees)
    545              
    546             @returnlist a list of the (X,Y) pixel coordinate and the
    547             (X,Y) Mercator distances (in meters) for each set of input
    548             GIS coordinates.
    549              
    550             =end classdoc
    551              
    552             =cut
    553              
    554             sub project {
    555             my $self = shift;
    556             return $self->{mercator}->project(@_);
    557             }
    558              
    559             =pod
    560              
    561             =begin classdoc
    562              
    563             Return the latitude/longitude and Mercator distance coordinates
    564             for the input pixel coordinates. Note that the input coordinates do not need to
    565             be within the bounding box of this object's map.
    566            

    567             Multiple sets of input coordinates may be supplied, in which
    568             case multiple coordinate results will be returned (4 output values
    569             per input coordinate pair).
    570              
    571             @param $x horizontal pixel coordinate to translate
    572             @param $y vertical pixel coordinate to translate
    573              
    574             @returnlist a list of the latitude/longitude coordinate (in degrees) and the
    575             (X,Y) Mercator distances (in meters) for each set of input pixel coordinates.
    576              
    577             =end classdoc
    578              
    579             =cut
    580              
    581             sub translate {
    582             my $self = shift;
    583             return $self->{mercator}->translate(@_);
    584             }
    585              
    586             =pod
    587              
    588             =begin classdoc
    589              
    590             Return this object's GD::Image object.
    591              
    592             @return the GD::Image object for this object's map image
    593              
    594             =end classdoc
    595              
    596             =cut
    597              
    598             sub image { return $_[0]->{image}; }
    599              
    600             =pod
    601              
    602             =begin classdoc
    603              
    604             Write this object's image, configuration, and optional
    605             coordinates map information
    606             to the specified file. Useful for saving extracted or
    607             scaled maps derived from basemaps.
    608              
    609             @param $outpath a filepath specification, optionally
    610             including the image filename with an image type
    611             suffix of 'png', 'gif', 'jpg', or 'jpeg'.
    612              
    613             @return undef on failure, with an error messge in $@;
    614             this object on success
    615              
    616             =end classdoc
    617              
    618             =cut
    619              
    620             sub save {
    621             my ($self, $path) = @_;
    622            
    623             my $fmt = (substr($path, -4, 1) eq '.') ? substr($path, -3)
    624             : (substr($path, -5, 1) eq '.') ? substr($path, -4)
    625             : undef;
    626             my ($confpath, $imgpath, $coordpath) = ($path, $path, $path);
    627              
    628             if ($fmt) {
    629             $@ = "Invalid or unsupported image format $fmt",
    630             return undef
    631             unless $valid_fmt{$fmt} && $self->{image}->can($valid_fmt{$fmt});
    632             $confpath=~s/$fmt$/conf/;
    633             $coordpath=~s/$fmt$/coords/;
    634             }
    635             else {
    636             $confpath .= '.conf';
    637             $coordpath .= '.coords';
    638             $imgpath .= '.png';
    639             $fmt = 'png';
    640             }
    641              
    642             die "Cannot open $imgpath: $!" unless open OUTF, ">$imgpath";
    643             binmode OUTF;
    644             print "Writing $imgpath\n"
    645             if $self->{verbose};
    646             print OUTF ($fmt eq 'png') ? $self->{image}->png
    647             : ($fmt eq 'gif') ? $self->{image}->gif
    648             : $self->{image}->jpeg;
    649             close OUTF;
    650              
    651             die "Cannot open $confpath: $!"
    652             unless open OUTF, ">$confpath";
    653             #
    654             # save as simple CSV, no D::D madness
    655             #
    656             print OUTF join(',', $self->{mercator}->config()), "\n";
    657             close OUTF;
    658              
    659             if ($self->{save_coords}) {
    660             die "Cannot open $coordpath: $!"
    661             unless open OUTF, ">$coordpath";
    662             print OUTF $self->{_coords};
    663             close OUTF;
    664             }
    665             return $self;
    666             }
    667              
    668             =pod
    669              
    670             =begin classdoc
    671              
    672             Return the actual width and height of this object's map image.
    673             Note that the returned values may be different than the originally
    674             requested dimensions, due to scaling for Mercator projection.
    675              
    676             @returnlist the actual width and height (in pixels) of this object's map image
    677              
    678             =end classdoc
    679              
    680             =cut
    681              
    682             sub dimensions {
    683             return $_[0]->{mercator}->dimensions();
    684             }
    685              
    686             ############################
    687             # PRIVATE FUNCTIONS
    688             ############################
    689              
    690             #
    691             # actually, this might be considered a pucliy overloaded
    692             # function; its the callback from the Mercator::Projector
    693             # object to render a segment
    694             #
    695             # NOTE: need to apply some compress here: reduce adjacent coords
    696             # to a single pair of coords of equivalent line segment
    697             #
    698             sub add_segment {
    699             my ($self, $seg, $dataset, $segno) = @_;
    700            
    701             print "\rDrawing segment $segno "
    702             if $self->{verbose};
    703              
    704             if ($self->{save_coords}) {
    705             my $coords = $self->{_coords} || '';
    706             my $area = 0;
    707             my $i = 0;
    708             while ($i < @$seg) {
    709             $i += 2
    710             while ($i < @$seg) && (!defined $seg->[$i]);
    711             last unless ($i < @$seg);
    712              
    713             $coords .= "Dataset $dataset Segment $segno Area $area: ";
    714             $coords .= join(',', $seg->[$i++], $seg->[$i++], '')
    715             while ($i < @$seg) && (defined $seg->[$i]);
    716              
    717             $coords .= "\n";
    718             $area++;
    719             }
    720             $self->{_coords} = $coords;
    721             }
    722              
    723             my $fg = $self->{fg};
    724             my $img = $self->{image};
    725             my $polyline = GD::Polyline->new();
    726             my ($lx, $ly) = ($seg->[0], $seg->[1]);
    727             my $i = 2;
    728             ($lx, $ly) = ($seg->[$i++], $seg->[$i++])
    729             unless defined $lx;
    730             $polyline->addPt($lx, $ly);
    731             my $ptcnt = 0;
    732             while ($i < @$seg) {
    733             my ($x, $y) = ($seg->[$i++], $seg->[$i++]);
    734             $polyline->addPt($x, $y),
    735             $ptcnt++,
    736             ($lx, $ly) = ($x, $y),
    737             next
    738             if defined $x;
    739             #
    740             # if an undef marker, draw current line
    741             #
    742             $img->polyline($polyline, $fg)
    743             if ($ptcnt > 2);
    744             $polyline = GD::Polyline->new();
    745             ($lx, $ly) = (undef, undef);
    746             $ptcnt = 0;
    747             next;
    748             }
    749             #
    750             # draw any remaining line
    751             #
    752             $img->polyline($polyline, $fg)
    753             if ($ptcnt > 1);
    754             return 1;
    755             }
    756              
    757             sub _load_basemap {
    758             my $self = shift;
    759              
    760             my $pat = $valid_fmt{$self->{imgfmt}};
    761              
    762             my $conf = "$self->{basemap_loc}.conf";
    763             my $imgfile = "$self->{basemap_loc}.$self->{imgfmt}";
    764             die "Unsupported image format $self->{imgfmt}; check your GD configuration."
    765             unless GD::Image->can($pat);
    766              
    767             die "$conf not found."
    768             unless (-s $conf);
    769              
    770             die "$imgfile not found."
    771             unless (-s $imgfile);
    772              
    773             die "Cannot open $conf: $!"
    774             unless open F, $conf;
    775             my $data = ;
    776             close F;
    777             chomp $data;
    778             my @mapdata = split /,/, $data;
    779            
    780             die "Invalid map data file"
    781             unless (scalar @mapdata == 10);
    782             #
    783             # only install the lat/long and pixel coords, skip the mercator distances
    784             #
    785             $self->{mercator} = GD::Map::Mercator::Projector->new(
    786             @mapdata[0..3], @mapdata[8..9], $self->{verbose});
    787              
    788             my $fd;
    789             die "Cannot open $imgfile: $!"
    790             unless open $fd, $imgfile;
    791             $self->{image} =
    792             ($self->{imgfmt} eq 'gif') ? GD::Image->newFromGif($fd)
    793             : ($self->{imgfmt} eq 'png') ? GD::Image->newFromPng($fd)
    794             : GD::Image->newFromJpeg($fd);
    795             close $fd;
    796              
    797             return $self;
    798             }
    799              
    800             1;
    801              
    802              
    803             package GD::Map::Mercator::Projector;
    804              
    805             =pod
    806              
    807             =begin hidden
    808              
    809             Translates latitude, longitude datapoints to pixel coordinates
    810             using Mercator Projection.
    811              
    812             (Yes, I know Mercator is bad. Really bad. Criminal, even.
    813             But its nice for rasterization. And its good enough for Google, so
    814             its good enough for me.)
    815              
    816             To convert to Mercator, we first computes
    817             scales based on min/max lat/long pts.
    818              
    819             Longitude is linear, except for some fiddling to deal with
    820             crossing boundry from positive to negative.
    821              
    822             Latitude requires some trig:
    823              
    824             y = log(tan(lat) + sec(lat)) = log(sin(lat)/cos(lat) + 1/cos(lat))
    825             = log((sin(lat) + 1)/cos(lat));
    826              
    827             (all in radians, of course)
    828              
    829             (We should really use UTM...)
    830              
    831             =end hidden
    832              
    833             =cut
    834              
    835             use POSIX;
    836             use Geo::Mercator;
    837              
    838             use strict;
    839             use warnings;
    840              
    841             sub new {
    842             my ($class, $minlat, $minlong, $maxlat, $maxlong, $width, $height, $verbose) = @_;
    843              
    844             die "Bad min/max longitude"
    845             if ($minlong < -180) || ($minlong > 180) ||
    846             ($maxlong < -180) || ($maxlong > 180) ||
    847             (($maxlong < $minlong) &&
    848             ((($maxlong < 0) && ($minlong < 0)) ||
    849             (($maxlong > 0) && ($minlong > 0))));
    850             die "Bad min/max latitude"
    851             if ($minlat > $maxlat) || ($minlat < -90) || ($minlat > 90) ||
    852             ($maxlat < -90) || ($maxlat > 90);
    853              
    854             my ($xmin, $ymin) = mercate($minlat, $minlong);
    855             my ($xmax, $ymax) = mercate($maxlat, $maxlong);
    856             my $longadj = (($xmin > 0) && ($xmax < 0));
    857              
    858             my $hscale = $width/($xmax - $xmin);
    859             my $vscale = $height/($ymax - $ymin);
    860             #
    861             # adjust the image dimensions to match best scaling
    862             #
    863             my $scale = ($hscale < $vscale) ? $hscale : $vscale;
    864             $height = _round(($ymax - $ymin) * $scale);
    865             $width = _round(($xmax - $xmin) * $scale);
    866            
    867             my ($minmerclong, $minmerclat) = mercate($minlat, $minlong);
    868             my ($maxmerclong, $maxmerclat) = mercate($maxlat, $maxlong);
    869            
    870             return bless {
    871             minlat => $minlat,
    872             minlong => $minlong,
    873             maxlat => $maxlat,
    874             maxlong => $maxlong,
    875             minmerclat => $minmerclat,
    876             minmerclong => $minmerclong,
    877             maxmerclat => $maxmerclat,
    878             maxmerclong => $maxmerclong,
    879             xmin => $xmin,
    880             ymin => $ymin,
    881             xmax => $xmax,
    882             ymax => $ymax,
    883             scale => $scale,
    884             longadj => $longadj,
    885             width => $width,
    886             height => $height,
    887             verbose => $verbose}, $class;
    888             }
    889              
    890             sub config {
    891             my $self = shift;
    892             return (@$self{qw(minlat minlong maxlat maxlong
    893             minmerclong minmerclat maxmerclong maxmerclat width height)});
    894             }
    895              
    896             sub dimensions { return ($_[0]->{width}, $_[0]->{height}); }
    897             #
    898             # return pixel coord from input lat/long
    899             #
    900             sub project {
    901             my $self = shift;
    902             #
    903             # note: we assume the inputs are in a valid range, but not
    904             # neccesarily inside our bounding box
    905             #
    906             my @result = ();
    907             while (@_) {
    908             my ($x, $y) = mercate(shift, shift);
    909             push @result, (($y <= $self->{ymax}) &&
    910             ($y >= $self->{ymin}) &&
    911             ($x >= $self->{xmin}) &&
    912             ($x <= $self->{xmax}))
    913             ? (_round(($x - $self->{xmin}) * $self->{scale}),
    914             # lat goes bottom to top, pixels top to bottom
    915             _round($self->{height} - (($y - $self->{ymin}) * $self->{scale})))
    916             : ();
    917             }
    918             return @result;
    919             }
    920             #
    921             # return lat/long and mercator distances for input pixel coords
    922             #
    923             sub translate {
    924             my $self = shift;
    925             #
    926             # upconvert to meters before demercating
    927             #
    928             my @result = ();
    929             my ($xmin, $ymin, $scale, $longadj, $height) =
    930             @$self{qw(xmin ymin scale longadj height)};
    931             while (@_) {
    932             my ($x, $y) = (shift, $height - shift);
    933             $x /= $scale;
    934             $x += $xmin;
    935             $y /= $scale;
    936             $y += $ymin;
    937             push @result, demercate($x, $y), $y, $x;
    938             }
    939             return @result;
    940             }
    941              
    942             sub _round {
    943             return (ceil($_[0]) - $_[0]) < ($_[0] - floor($_[0]))
    944             ? ceil($_[0])
    945             : floor($_[0]);
    946             }
    947              
    948             sub filter {
    949             my ($self, $fd, $container, $dataset, $keepers) = @_;
    950            
    951             my ($xmin, $ymin, $xmax, $ymax, $scale, $longadj, $width, $height) =
    952             @$self{qw(xmin ymin xmax ymax scale longadj width height)};
    953             my ($n, $len, $record, $segno, $rank, $pts);
    954             my @coords = ();
    955             my @mercs = ();
    956             my @keeppx = ();
    957             #
    958             # convert keep region coords to pixel coords
    959             #
    960             if ($keepers) {
    961             my $i = 0;
    962             my ($keepx, $keepy);
    963             while ($i < @$keepers) {
    964             ($keepx, $keepy) = mercate($keepers->[$i++], $keepers->[$i++]);
    965             push @keeppx,
    966             (_round(($keepx - $xmin) * $scale),
    967             _round($height - (($keepy - $ymin) * $scale)));
    968             }
    969             }
    970             while ($n = read($fd, $len, 4)) {
    971             $len = unpack('L', $len);
    972             $n = read($fd, $record, $len);
    973             ($segno, $rank, $pts) = unpack('LLL', substr($record, 0, 12));
    974             $pts *= 2;
    975             #
    976             # sometimes Perl doesn't believe me the 1st time!
    977             #
    978             @mercs = unpack("d$pts", substr($record, 12));
    979             my $retry = 3;
    980             $retry--,
    981             @mercs = unpack("d$pts", substr($record, 12))
    982             while ($retry && (@mercs != $pts));
    983             die "No coords read!!!!" unless scalar @mercs == $pts;
    984             print "\n*** Had to reload segment $segno ", 3 - $retry, " times!\n"
    985             if $self->{verbose} && ($retry < 3);
    986              
    987             my $inside = 0;
    988             my ($lx, $ly) =
    989             (_round(($mercs[0] - $xmin) * $scale),
    990             _round($height - (($mercs[1] - $ymin) * $scale)));
    991             my $i = 2;
    992             while ($i < @mercs) {
    993             my ($x, $y) =
    994             (_round(($mercs[$i++] - $xmin) * $scale),
    995             _round($height - (($mercs[$i++] - $ymin) * $scale)));
    996             #
    997             # scaling causes many pts to overlap, so skip them
    998             # !!!NOTE need to optimze for pts on the same line segment
    999             # (ie, no change in x, xor no change in y)
    1000             #
    1001             next
    1002             if ($x == $lx) && ($y == $ly);
    1003              
    1004             if (($y <= $height) && ($y >= 0) && ($x >= 0) && ($x <= $width) &&
    1005             ((!$keepers) || _keep(\@keeppx, $x, $y))) {
    1006             #
    1007             # if prior point outside, add it w/ a undef marker
    1008             # probably need to compute clipping intersection
    1009             #
    1010             push @coords, undef, undef, $lx, $ly
    1011             unless (($ly <= $height) && ($ly >= 0) && ($lx >= 0) && ($lx <= $width));
    1012             push @coords, $x, $y;
    1013             $inside++;
    1014             }
    1015             ($lx, $ly) = ($x, $y);
    1016             }
    1017             if ($inside) {
    1018             $container->add_segment(\@coords, $dataset, $segno)
    1019             }
    1020             elsif ($self->{verbose}) {
    1021             print "\r*** Skipping segment $segno \r";
    1022             }
    1023             @mercs = ();
    1024             @coords = ();
    1025             }
    1026             print "\n" if $self->{verbose};
    1027             return $self;
    1028             }
    1029              
    1030             sub _keep {
    1031             my ($keeppx, $x, $y) = @_;
    1032             my $i = 0;
    1033             $i += 4
    1034             while ($i < @$keeppx) &&
    1035             (($y > $keeppx->[$i+3]) || ($y < $keeppx->[1]) ||
    1036             ($x < $keeppx->[$i]) || ($x > $keeppx->[$i+2]));
    1037             return ($i != @$keeppx);
    1038             }
    1039              
    1040             1;
    1041              
    1042             __END__