File Coverage

blib/lib/Imager/Bing/MapLayer/Utils.pm
Criterion Covered Total %
statement 68 115 59.1
branch 11 26 42.3
condition 3 8 37.5
subroutine 17 21 80.9
pod 10 10 100.0
total 109 180 60.5


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