File Coverage

blib/lib/Geo/GoogleMaps/FitBoundsZoomer.pm
Criterion Covered Total %
statement 97 97 100.0
branch 36 46 78.2
condition 20 26 76.9
subroutine 13 13 100.0
pod 3 3 100.0
total 169 185 91.3


line stmt bran cond sub pod time code
1             package Geo::GoogleMaps::FitBoundsZoomer;
2              
3 2     2   44080 use 5.10.0;
  2         6  
  2         76  
4              
5 2     2   11 use strict;
  2         2  
  2         58  
6 2     2   9 use warnings;
  2         16  
  2         73  
7              
8             our $VERSION = '1.03';
9              
10 2     2   9 use Carp;
  2         3  
  2         158  
11 2     2   11 use List::Util qw(min max);
  2         2  
  2         246  
12              
13 2     2   9 use constant ZOOM_LIMIT => 20;
  2         2  
  2         143  
14 2     2   9 use constant PI => 3.141592653589793;
  2         4  
  2         2488  
15              
16             sub new {
17 33     33 1 20233 my ($class, %params) = @_;
18              
19 33         56 my $points = delete $params{points};
20 33         50 my $map_width = delete $params{width};
21 33         41 my $map_height = delete $params{height};
22 33   100     125 my $zoom_limit = delete $params{zoom_limit} // ZOOM_LIMIT;
23            
24 33         169 bless {
25             points => $points,
26             width => $map_width,
27             height => $map_height,
28             zoom_limit => $zoom_limit
29             }, $class;
30             }
31              
32             # returns max zoom for a min bounding box
33             sub max_bounding_zoom {
34 31     31 1 3035 my $self = shift;
35              
36 31 100 100     133 if ( @_ || ! defined $self->{max_bounding_zoom} ) {
37              
38 30 100       61 if ( @_ ) {
39 6         15 my %params = @_;
40              
41 6         9 undef $self->{map_center};
42 6         11 my @all_params = ('points', 'width', 'height', 'zoom_limit');
43 6   66     79 $self->{$_} = delete $params{$_} // $self->{$_} for @all_params;
44              
45             }
46            
47 30         80 foreach ('points', 'width', 'height') {
48 87 100       224 croak "No map $_ parameter! Usage: max_bounding_zoom( points => \$points, width => \$map_width, height => \$map_height )"
49             unless defined $self->{$_};
50             }
51            
52 27 100       32 croak "At least one point must be provided!" unless @{$self->{points}} > 0;
  27         70  
53 26 100       58 croak "Map width must be a positive number!" unless $self->{width} > 0;
54 25 100       51 croak "Map height must be a positive number!" unless $self->{height} > 0;
55 24 100       59 croak "Zoom limit must be greater of equal to 0!" unless $self->{zoom_limit} >= 0;
56            
57 23         38 $self->{bounds} = $self->_get_bounds();
58 19         44 $self->{max_bounding_zoom} = $self->_zoom_level( $self->{bounds} );
59            
60             }
61              
62 20 50       52 croak "Insufficient data to calculate maximum zoom! Usage: max_bounding_zoom( points => \$points, width => \$map_width, height => \$map_height )"
63             unless defined $self->{max_bounding_zoom};
64              
65 20         65 return $self->{max_bounding_zoom};
66             }
67              
68             # returns center of the rectangular bounding box
69             sub bounding_box_center {
70 5     5 1 70 my $self = shift;
71            
72 5 100 66     37 croak "Map data not initialized! max_bounding_zoom needs to be called first. Usage: max_bounding_zoom( points => \$points, width => \$map_width, height => \$map_height )"
73             if !$self->{points} || !$self->{bounds};
74              
75 3 50       7 if ( ! $self->{map_center} ) {
76 3         2 my $center;
77 3         4 my $bounds = $self->{bounds};
78              
79 3         5 my ($blp, $trp) = ($bounds->{blp}, $bounds->{trp});
80              
81 3         9 $center->{lat} = ( ($trp->{lat} - $blp->{lat}) / 2 ) + $blp->{lat};
82 3         7 $center->{long} = ( ($trp->{long} - $blp->{long}) / 2 ) + $blp->{long};
83              
84 3         5 $self->{map_center} = $center;
85             }
86              
87 3         11 return $self->{map_center};
88             }
89              
90             # returns a bounding box (bottom left and top right point) for coordinates
91             sub _get_bounds {
92 30     30   49 my $self = shift;
93              
94 30         63 my $blp = { 'lat' => 90, 'long' => 180 }; # bottom left point
95 30         51 my $trp = { 'lat' => -90, 'long' => -180 }; # top right point
96            
97 30 50       64 my $points = $self->{points}
98             or croak "Cannot calculate map bounds without points!";
99              
100 30         43 foreach my $point (@$points) {
101            
102 55         86 my ($lat, $lng) = ($point->{lat}, $point->{long});
103            
104 55         123 $blp->{'lat'} = min ($blp->{'lat'}, $lat);
105 55         95 $trp->{'lat'} = max ($trp->{'lat'}, $lat);
106 55         88 $blp->{'long'} = min ($blp->{'long'}, $lng);
107 55         122 $trp->{'long'} = max ($trp->{'long'}, $lng);
108             }
109            
110 30 100 66     178 croak "Point latitude out of bounds ( < -90 or > 90 )" unless ( -90 <= $blp->{'lat'} && $blp->{'lat'} <= 90 );
111 29 100 66     127 croak "Point latitude out of bounds ( < -90 or > 90 )" unless ( -90 <= $trp->{'lat'} && $trp->{'lat'} <= 90 );
112 28 100 66     110 croak "Point longitude out of bounds ( < -180 or > 180 )" unless ( -180 <= $blp->{'long'} && $blp->{'long'} <= 180 );
113 27 100 66     152 croak "Point longitude out of bounds ( < -180 or > 180 )" unless ( -180 <= $trp->{'long'} && $trp->{'long'} <= 180 );
114              
115 26         87 return { 'blp' => $blp, 'trp' => $trp };
116             }
117              
118             # returns max bounding zoom level given a set of points, map width and height
119             sub _zoom_level {
120 19     19   24 my ($self, $bounds) = @_;
121              
122 19 50       563 croak "Map bounds not set!" if !$bounds;
123              
124 19         34 my ($width, $height) = ($self->{width}, $self->{height});
125            
126 19 50       36 croak "Map width not set!" unless defined $width;
127 19 50       48 croak "Map height not set!" unless defined $height;
128            
129 19         20 my $zoom_limit = $self->{zoom_limit};
130              
131 19         27 my ($blp, $trp) = ($bounds->{blp}, $bounds->{trp});
132            
133 19         46 foreach my $zoom_level (reverse (0 .. $zoom_limit)) {
134 188         372 my $blpxl = $self->_coord2pix($blp->{lat}, $blp->{long}, $zoom_level);
135 188         365 my $trpxl = $self->_coord2pix($trp->{lat}, $trp->{long}, $zoom_level);
136            
137 188 50       426 $blpxl->{x} -= (2**($zoom_level + 8)) if ( $blpxl->{x} > $trpxl->{x} );
138 188         463 my $delta={ x => abs($trpxl->{x} - $blpxl->{x}), y => abs($trpxl->{y} - $blpxl->{y}) };
139 188 100 100     709 return $zoom_level if ( ($delta->{x} <= $width) && ($delta->{y} <= $height) );
140             }
141 1         3 return 0;
142             }
143              
144             # returns an X,Y pixel cordinate from $lat, $lng coordinates for a given zoom level
145             sub _coord2pix {
146             #values hash is the output of google_magicâ„¢ for a given zoom level
147 376     376   428 my ($self, $lat, $lng, $zoom ) = @_;
148            
149 376 50       593 croak "Latitude not set!" unless defined $lat;
150 376 50       587 croak "Longitude not set!" unless defined $lng;
151 376 50       537 croak "Zoom not set!" unless defined $zoom;
152              
153 376         386 my $center_point = 2**($zoom + 7);
154 376         382 my $total_pixels = $center_point*2;
155 376         388 my $pixels_per_lng_degree = $total_pixels / 360;
156 376         330 my $pixels_per_lng_radian = $total_pixels / (2 * PI);
157 376         863 my $siny = min ( max( sin( $lat*(PI/180) ), -0.99999999 ), 0.99999999 );
158              
159 376         344 my $coord;
160 376         595 $coord->{x} = $center_point + $lng * $pixels_per_lng_degree;
161 376         750 $coord->{y} = $center_point - 0.5 * log((1+$siny)/(1-$siny)) * $pixels_per_lng_radian;
162            
163 376         519 return $coord;
164             }
165              
166             1; # end of Geo::GoogleMaps::FitBoundsZoomer
167              
168             __END__