File Coverage

blib/lib/Imager/Bing/MapLayer/Utils.pm
Criterion Covered Total %
statement 90 122 73.7
branch 17 26 65.3
condition 3 8 37.5
subroutine 20 23 86.9
pod 10 10 100.0
total 140 189 74.0


line stmt bran cond sub pod time code
1             package Imager::Bing::MapLayer::Utils;
2              
3 6     6   772370 use v5.10.1;
  6         23  
  6         333  
4              
5 6     6   35 use strict;
  6         12  
  6         3902  
6 6     6   38 use warnings;
  6         11  
  6         211  
7              
8 6     6   83 use Carp qw/ confess /;
  6         11  
  6         442  
9 6     6   5549 use Const::Exporter;
  6         59207  
  6         43  
10 6     6   1053 use Const::Fast;
  6         15  
  6         42  
11 6     6   428 use List::MoreUtils qw/ minmax /;
  6         14  
  6         349  
12 6     6   4953 use POSIX::2008 qw/ round /;
  6         10336  
  6         1410  
13              
14 6     6   47 use version 0.77; our $VERSION = version->declare('v0.1.8');
  6         144  
  6         44  
15              
16             our @EXPORT;
17              
18             our @EXPORT_OK = (
19             @EXPORT,
20             qw/
21             width_at_level latlon_to_pixel pixel_to_tile_coords
22             tile_coords_to_pixel_origin
23             tile_coords_to_quad_key quad_key_to_tile_coords
24             bounding_box optimize_points
25             get_ground_resolution get_map_scale
26             /
27             );
28              
29             =head1 NAME
30              
31             Imager::Bing::MapLayer::Utils - utility functions for map layer modules
32              
33             =head1 DESCRIPTION
34              
35             This module contains utility functions for L<Imager::Bing::MapLayer>.
36              
37             =head1 EXPORTS
38              
39             By default, none. Constants and functions must be included in the
40             usage line explicitly.
41              
42             =head1 CONSTANTS
43              
44             =head2 C<$TILE_WIDTH>
45              
46             =head2 C<$TILE_HEIGHT>
47              
48             The width and height of individual tiles.
49              
50             =cut
51              
52 6         45 use Const::Exporter default => [
53             '$TILE_WIDTH' => 256,
54             '$TILE_HEIGHT' => 256,
55 6     6   978 ];
  6         19  
56              
57             =head2 C<$MIN_ZOOM_LEVEL>
58              
59             =head2 C<$MIN_ZOOM_LEVEL>
60              
61             The minimum and maximum zoom levels supported by these modules.
62              
63             Note that C<$MAX_ZOOM_LEVEL> can actually be as high as 23, but that
64             causes bit overflows for calculations on 32-bit integers. We also
65              
66             don't want to generate tiles beyond level 18, since the amount of
67             tiles required is so large that we run out of memory (and we also
68             don't need it, since Bing switches to a street view mode).
69              
70             When the tiles are not saved in memory, then we can generate higher
71             resolutions. However, Bing doesn't seem to support zoom levels higher
72             than 19 at this time.
73              
74             =cut
75              
76 6         35 use Const::Exporter default => [
77             '$MIN_ZOOM_LEVEL' => 1,
78             '$MAX_ZOOM_LEVEL' => 19,
79 6     6   1787 ];
  6         13  
80              
81             # Local constants used by these functions
82              
83             const my $PI => 3.1415926535897932;
84              
85             const my $EARTH_RADIUS => 6_378_137; # Earth radius (meters)
86             const my $METERS_PER_INCH => 0.0254;
87              
88             =head1 FUNCTIONS
89              
90             =head2 C<width_at_level>
91              
92             my $width = width_at_level( $level );
93              
94             Returns the width of a zoom level.
95              
96             =cut
97              
98             sub width_at_level {
99 63     63 1 38508 my ($level) = @_;
100              
101 63 100 100     476 confess
102             "invalid level (must be between ${MIN_ZOOM_LEVEL} and ${MAX_ZOOM_LEVEL}"
103             if ( ( $level < $MIN_ZOOM_LEVEL ) || ( $level > $MAX_ZOOM_LEVEL ) );
104              
105 61         168 return 1 << ( $level + 8 );
106             }
107              
108             =head2 C<latlon_to_pixel>
109              
110             my ($pixel_x, $pixel_y) = latlon_to_pixel( $level, $latitude, $longitude );
111              
112             Converts latitude and longitude to pixel coodinates on a specific zoom level.
113              
114             =cut
115              
116             sub latlon_to_pixel {
117 42     42 1 13982 my ( $level, $latitude, $longitude ) = @_;
118              
119 42         133 my $width = width_at_level($level);
120              
121 42         277 my $sin_latitude = sin( $latitude * $PI / 180 );
122              
123 42         312 return map { round($_) } (
  84         516  
124             ( ( $longitude + 180 ) / 360 ) * $width,
125             ( 0.5 - log( ( 1 + $sin_latitude ) / ( 1 - $sin_latitude ) )
126             / ( 4 * $PI )
127             ) * $width,
128             );
129              
130             }
131              
132             =head2 C<pixel_to_tile_coords>
133              
134             my ($tile_x, $tile_y) = pixel_to_tile_coords( $pixel_x, $pixel_y );
135              
136             Converts pixel coordinates to map tile coordinates.
137              
138             =cut
139              
140             sub pixel_to_tile_coords {
141 45     45 1 92 my ( $pixel_x, $pixel_y ) = @_;
142 45         190 return map { $_ >> 8 } ( $pixel_x, $pixel_y );
  90         263  
143             }
144              
145             =head2 C<tile_coords_to_pixel_origin>
146              
147             my ($origin_x, $origin_y) = tile_coords_to_pixel_origin( $tile_x, $tile_y );
148              
149             Returns the top-left pixel coordinates from tile coordinates.
150              
151             =cut
152              
153             sub tile_coords_to_pixel_origin {
154 34     34 1 75 my ( $tile_x, $tile_y ) = @_;
155 34         70 return map { $_ << 8 } ( $tile_x, $tile_y );
  68         843  
156             }
157              
158             =head2 C<tile_coords_to_quad_key>
159              
160             my $quad_key = tile_coords_to_quad_key( $level, $tile_x, $tile_y );
161              
162             Returns the quadrant key ("quad key") for a given tile at a given level.
163              
164             =cut
165              
166             sub tile_coords_to_quad_key {
167 6     6   5423 use integer;
  6         14  
  6         53  
168              
169 32     32 1 10846 my ( $level, $tile_x, $tile_y ) = @_;
170              
171 32         90 my $mask = 1 << ( $level - 1 );
172 32         67 my $key = '';
173              
174 32         95 while ($mask) {
175              
176 331         401 my $digit = 0;
177              
178 331 100       645 $digit |= 1 if ( $tile_x & $mask );
179 331 100       601 $digit |= 2 if ( $tile_y & $mask );
180              
181 331         402 $key .= $digit;
182              
183 331         797 $mask = $mask >> 1;
184              
185             }
186              
187 32         808 return $key;
188             }
189              
190             =head2 C<quad_key_to_tile_coords>
191              
192             my ($tile_x, $tile_y, $level) = quad_key_to_tile_coords( $quad_key );
193              
194             Returns the tile coordinates and level from the quad key.
195              
196             =cut
197              
198             sub quad_key_to_tile_coords {
199 6     6   767 use integer;
  6         15  
  6         21  
200              
201 34     34 1 87 my ($quad_key) = @_;
202              
203 34         237 state $re = qr/^[0-3]{$MIN_ZOOM_LEVEL,$MAX_ZOOM_LEVEL}$/;
204              
205 34 50       291 unless ( $quad_key =~ $re ) {
206 0         0 confess "invalid quad key";
207             }
208              
209 34         78 my ( $tile_x, $tile_y ) = ( 0, 0 );
210              
211 34         81 my $level = length($quad_key); # implicitly checked by regex
212 34         74 my $mask = 1 << ( $level - 1 );
213              
214             # Translate the quad key into a string of digits
215              
216 34         195 foreach my $digit ( map { $_ - 48 } ( unpack 'c*', $quad_key ) ) {
  363         525  
217              
218 363 100       694 $tile_x |= $mask if ( $digit & 1 );
219 363 100       797 $tile_y |= $mask if ( $digit & 2 );
220              
221 363         476 $mask = $mask >> 1;
222             }
223              
224 34         828 return ( $tile_x, $tile_y, $level );
225             }
226              
227             =head2 C<get_ground_resolution>
228              
229             $meters_per_pixel = get_ground_resolution( $level, $latitude );
230              
231             This returns the distance on the ground that's represented by a single
232             pixel.
233              
234             =cut
235              
236             sub get_ground_resolution {
237 0     0 1 0 my ( $level, $latitude ) = @_;
238              
239 0         0 return ( cos( $latitude * $PI / 180 ) * ( 2 * $PI * $EARTH_RADIUS ) )
240             / width_at_level($level);
241              
242             }
243              
244             =head2 C<get_map_scale>
245              
246             TODO
247              
248             =cut
249              
250             sub get_map_scale {
251 0     0 1 0 my ( $level, $latitude, $screen_dpi ) = @_;
252              
253 0   0     0 $screen_dpi //= 96; # a standard screen dpi
254              
255 0         0 return get_ground_resolution( $level, $latitude )
256             * $screen_dpi / $METERS_PER_INCH;
257             }
258              
259             =head2 C<bounding_box>
260              
261             my ($left, $top, $right, $bottom) = bounding_box( %args );
262              
263             This parses the arguments given to L<Imager::Draw> methods to
264             calculate a bounding box.
265              
266             =cut
267              
268             sub bounding_box {
269 13     13 1 49 my (%args) = @_;
270              
271 13         55 my %points = ( x => [], 'y' => [] );
272              
273 13 50       80 if ( my $radius = $args{r} ) { # radius for arcs and circles
    50          
    50          
274              
275 0         0 foreach my $axis (qw/ x y /) {
276              
277 0         0 push @{ $points{$axis} },
  0         0  
278             ( $args{$axis} - $radius, $args{$axis} + $radius );
279              
280             }
281              
282             } elsif ( my $box = $args{box} ) {
283              
284 0         0 push @{ $points{x} }, ( $box->[0], $box->[2] );
  0         0  
285 0         0 push @{ $points{y} }, ( $box->[1], $box->[3] );
  0         0  
286              
287             } elsif ( my $list = $args{points} ) {
288              
289 0         0 foreach my $pt ( @{$list} ) {
  0         0  
290              
291 0         0 push @{ $points{x} }, $pt->[0];
  0         0  
292 0         0 push @{ $points{y} }, $pt->[1];
  0         0  
293              
294             }
295              
296             } else {
297              
298 13         35 foreach my $axis (qw/ x y /) {
299              
300 26 50       69 if ( ref $args{$axis} ) {
301              
302 0         0 push @{ $points{$axis} }, @{ $args{$axis} };
  0         0  
  0         0  
303              
304             } else {
305              
306 26 50       79 push @{ $points{$axis} }, $args{$axis}
  26         64  
307             if ( defined $args{$axis} );
308              
309             }
310              
311 26         49 foreach my $alt (qw/ 1 2 min max /) {
312              
313 104         133 my $arg = $axis . $alt;
314              
315 104 50       247 push @{ $points{$axis} }, $args{$arg}
  0         0  
316             if ( defined $args{$arg} );
317              
318             }
319              
320             }
321              
322             }
323              
324 13         24 my ( $xmin, $xmax ) = minmax( @{ $points{x} } );
  13         73  
325 13         22 my ( $ymin, $ymax ) = minmax( @{ $points{y} } );
  13         40  
326              
327 13         87 return ( $xmin, $ymin, $xmax, $ymax );
328              
329             }
330              
331             =head2 C<optimize_points>
332              
333             my @points2 = @{ optimize_points( \@points ) };
334              
335             This function takes a reference to a list of points and returns
336             another reference to a list of points, without adjacent duplicate
337             points. This reduces the number of points to plot for complex
338             polylines on lower zoom levels.
339              
340             =cut
341              
342             sub optimize_points {
343 0     0 1   my ($points) = @_;
344              
345 0           my $last = $points->[0];
346              
347 0           my @list = ($last);
348              
349 0           my $i = 1;
350              
351 0           while ( my $point = $points->[ $i++ ] ) {
352              
353 0 0 0       if ( ( $point->[0] != $last->[0] ) || ( $point->[1] != $last->[1] ) )
354             {
355              
356 0           push @list, $point;
357              
358 0           $last = $point;
359              
360             }
361              
362             }
363              
364 0           return \@list;
365             }
366              
367             =head1 SEE ALSO
368              
369             =over
370              
371             =item A discussion of the Bing Maps Tile System
372              
373             L<http://msdn.microsoft.com/en-us/library/bb259689.aspx>
374              
375             =back
376              
377             =cut
378              
379             1;