File Coverage

blib/lib/Imager/Bing/MapLayer/Level.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Imager::Bing::MapLayer::Level;
2              
3 1     1   101320 use v5.10.1;
  1         3  
  1         43  
4              
5 1     1   385 use Moose;
  0            
  0            
6             with 'Imager::Bing::MapLayer::Role::TileClass';
7             with 'Imager::Bing::MapLayer::Role::FileHandling';
8             with 'Imager::Bing::MapLayer::Role::Centroid';
9             with 'Imager::Bing::MapLayer::Role::Misc';
10              
11             use Carp qw/ confess /;
12             use Class::MOP::Method;
13             use Const::Fast;
14             use Imager;
15             use List::Util qw/ min max /;
16             use Moose::Util::TypeConstraints;
17             use MooseX::StrictConstructor;
18             use POSIX::2008 qw/ round /;
19              
20             use Imager::Bing::MapLayer::Utils qw/
21             $MIN_ZOOM_LEVEL $MAX_ZOOM_LEVEL $TILE_WIDTH $TILE_HEIGHT
22             width_at_level bounding_box pixel_to_tile_coords tile_coords_to_quad_key
23             optimize_points get_ground_resolution
24             /;
25              
26             use Imager::Bing::MapLayer::Image;
27             use Imager::Bing::MapLayer::Tile;
28              
29             use version 0.77; our $VERSION = version->declare('v0.1.5');
30              
31             =head1 NAME
32              
33             Imager::Bing::MapLayer::Level - zoom levels for Bing Maps
34              
35             =head1 SYNOPSIS
36              
37             my $level = Imager::Bing::MapLayer::Level->new(
38             level => $level, # zoom level
39             base_dir => $dir, # base directory (default '.')
40             overwrite => 1, # overwrite existing (default)
41             autosave => 1, # save on exit (default)
42             in_memory => 0, # keep tiles in memory (default false)
43             combine => 'darken', # tile combination method (default)
44             );
45              
46             $level->polygon(
47             points => $points, # listref to [ lat, lon ] points
48             fill => Imager::Fill->new( ... ), #
49             );
50              
51             =head1 DESCRIPTION
52              
53             =head1 ATTRIBUTES
54              
55             =head2 C<level>
56              
57             The zoom level.
58              
59             =cut
60              
61             has 'level' => (
62             is => 'ro',
63             isa => subtype(
64             as 'Int',
65             where { ( $_ >= $MIN_ZOOM_LEVEL ) && ( $_ <= $MAX_ZOOM_LEVEL ) }
66             ),
67             );
68              
69             =head2 C<tiles>
70              
71             A hash reference of C<Imager::Bing::MapLayer::Tile> objects.
72              
73             The keys are tile coordinates of the form C<$tile_x . $; . $tile_y>.
74              
75             =cut
76              
77             has 'tiles' => (
78             is => 'ro',
79             isa => 'HashRef',
80             default => sub { return {} },
81             init_arg => undef,
82             );
83              
84             =head2 C<timeouts>
85              
86             =cut
87              
88             # TODO - the last-modified value should be saved with each tile
89              
90             has 'timeouts' => (
91             is => 'ro',
92             isa => 'HashRef',
93             default => sub { return {} },
94             );
95              
96             =head2 C<last_cleanup_time>
97              
98             =cut
99              
100             has 'last_cleanup_time' => (
101             is => 'rw',
102             isa => 'Int',
103             default => sub { return time; },
104             );
105              
106             =head1 METHODS
107              
108             =head2 C<width>
109              
110             The width of the layer.
111              
112             =cut
113              
114             sub width {
115             my ($self) = @_;
116             return width_at_level( $self->level );
117             }
118              
119             =head2 C<height>
120              
121             The height of the layer.
122              
123             =cut
124              
125             sub height {
126             my ($self) = @_;
127             return width_at_level( $self->level );
128             }
129              
130             =head2 C<latlon_to_pixel>
131              
132             my ($x, $y) = $level->latlon_to_pixel($latitude, $longitude);
133              
134             Translates a latitude and longitude coordinate into a pixel on the
135             zoom level.
136              
137             =cut
138              
139             sub latlon_to_pixel {
140             my ( $self, @latlon ) = @_;
141             return Imager::Bing::MapLayer::Utils::latlon_to_pixel( $self->level,
142             @latlon );
143             }
144              
145             =head2 C<_translate_points>
146              
147             This is a utility method for translating C<points> parameters from
148             L<Imager> methods.
149              
150             At lower zoom levels, these are "optimized" by removing duplicate
151             adjacent points.
152              
153             =cut
154              
155             sub _translate_points {
156             my ( $self, $points ) = @_;
157             return optimize_points(
158             [ map { [ $self->latlon_to_pixel( @{$_} ) ] } @{$points} ] );
159             }
160              
161             =head2 C<_translate_coords>
162              
163             This is a utility method for translating C<box> parameters from
164             L<Imager> methods.
165              
166             =cut
167              
168             sub _translate_coords {
169             my ( $self, $points ) = @_;
170             no warnings 'once';
171             return [ pairmap { ( $self->latlon_to_pixel( $a, $b ) ) } @{$points} ];
172             }
173              
174             =head2 C<_translate_radius>
175              
176             my $pixels = $level->_translate_radius( $meters, $min_pixels);
177              
178             This method translates the C<r> parameter for cirlces and arcs from
179             meters into pixels.
180              
181             If the C<$min_pixels> parameter is given, then the radius will be no
182             smaller than the given number of pixels. (This is useful to ensure
183             that small circles show up on lower zoom levels.)
184              
185             =cut
186              
187             sub _translate_radius {
188             my ( $self, $r, $min_r ) = @_;
189              
190             return max(
191             round(
192             $r / get_ground_resolution(
193             $self->level, $self->centroid_latitude
194             )
195             ),
196             $min_r // 0
197             );
198             }
199              
200             # This is a hash that says which utility method to use for translating
201             # point arguments for Imager methods.
202              
203             const my %ARG_TO_METHOD => (
204             points => '_translate_points',
205             box => '_translate_coords', # TODO - this does not seem to work
206             r => '_translate_radius',
207             );
208              
209             =head2 C<_translate_point_arguments>
210              
211             This is an I<internal> utility method for translating coordinate
212             parameters from L<Imager> methods.
213              
214             =cut
215              
216             sub _translate_point_arguments {
217             my ( $self, %args ) = @_;
218              
219             my %i_args;
220              
221             foreach my $arg ( keys %ARG_TO_METHOD ) {
222              
223             if ( my $method = $self->can( $ARG_TO_METHOD{$arg} ) ) {
224              
225             $i_args{$arg}
226             = $self->$method( $args{$arg}, $args{"-min_${arg}"} )
227             if ( exists $args{$arg} );
228              
229             }
230              
231             }
232              
233             # Ideally, we could translate x and y separately, using the
234             # centroid_longitude and centroid_latitude as defaults for the
235             # missing coordinate. But this does not seem to work properly.
236             # So we translate them together.
237              
238             # TODO - clean up this code.
239              
240             foreach my $suffix ( '', qw/ 1 2 min max / ) {
241              
242             my $x = $args{ 'x' . $suffix };
243             my $y = $args{ 'y' . $suffix };
244              
245             # If either the x or y parameter is missing, then it won't be
246             # translated.
247              
248             if ( ( defined $x ) && ( defined $y ) ) {
249              
250             if ( ( ref $x ) || ( ref $y ) ) {
251              
252             if ($suffix) {
253              
254             confess
255             sprintf(
256             "x%s and y%s as coordinate lists are not supported");
257              
258             } else {
259              
260             # If there are a pair of x,y coordinate lists,
261             # then we just reassemble them into a 'points'
262             # parameter and translate that.
263              
264             # Note that this is based on how Imager treats
265             # these.
266              
267             # TODO - rewrite this code
268              
269             my @xs = ( ref $x ) ? @{$x} : ($x);
270             my @ys = ( ref $y ) ? @{$y} : ($y);
271              
272             my $last_x = shift @xs;
273             my $last_y = shift @ys;
274              
275             my @points = ( [ $last_y, $last_x ] );
276              
277             while ( @xs || @ys ) {
278              
279             my $this_x = ( shift @xs ) // $last_x;
280             my $this_y = ( shift @ys ) // $last_y;
281              
282             push @points, [ $this_y, $this_x ];
283              
284             ( $last_x, $last_y ) = ( $this_x, $this_y );
285              
286             }
287              
288             $i_args{points} = $self->_translate_points( \@points );
289              
290             }
291              
292             } else {
293              
294             my ( $pixel_x, $pixel_y ) = $self->latlon_to_pixel( $y, $x );
295             $i_args{ 'x' . $suffix } = $pixel_x;
296             $i_args{ 'y' . $suffix } = $pixel_y;
297              
298             }
299              
300             }
301              
302             }
303              
304             return %i_args;
305             }
306              
307             =head2 C<_tile_coords_to_internal_key>
308              
309             my $key = $level->_tile_coords_to_internal_key($tile_x, $tile_y);
310              
311             This is an I<internal method> for generating a key for the L</tiles>
312             and L</timeouts>.
313              
314             We join the tile coordinates into a small key to use for this, instead
315             of generating a quad key (which requires more work, and is only needed
316             for creating a new tile).
317              
318             =cut
319              
320             sub _tile_coords_to_internal_key {
321             my ( $self, $tile_x, $tile_y ) = @_;
322             return join( $;, $tile_x, $tile_y );
323             }
324              
325             =head2 C<_internal_key_to_tile_coords>
326              
327              
328             my ($tile_x, $tile_y) = $level->_internal_key_to_tile_coords($key);
329              
330             This is an I<internal> method for determining tile coordinates from a
331             key. It is the inverse of L</_tile_coords_to_internal_key>.
332              
333             =cut
334              
335             sub _internal_key_to_tile_coords {
336             my ( $self, $key ) = @_;
337             return ( split $;, $key );
338             }
339              
340             =head2 C<_load_tile>
341              
342             my $tile = $level->_load_tile($tile_x, $tile_y, $overwrite);
343              
344             This is an I<internal> method that loads a tile for this level, if it
345             exists. Otherwise it creates a new tile.
346              
347             =cut
348              
349             sub _load_tile {
350             my ( $self, $tile_x, $tile_y, $overwrite ) = @_;
351              
352             my $class = $self->tile_class;
353              
354             return $class->new(
355             quad_key => tile_coords_to_quad_key( $self->level, $tile_x, $tile_y ),
356             base_dir => $self->base_dir,
357             overwrite => $overwrite,
358             autosave => $self->autosave,
359             );
360              
361             }
362              
363             =head2 C<_cleanup_tiles>
364              
365             $level->_cleanup_tiles();
366              
367             This is an I<internal> method that removes tiles from memory that have
368             not been drawn to within the L</in_memory> timeout.
369              
370             =cut
371              
372             sub _cleanup_tiles {
373             my ($self) = @_;
374              
375             return unless $self->in_memory;
376              
377             my $time = time;
378              
379             if ( ( $self->last_cleanup_time + $self->in_memory ) < $time ) {
380              
381             my $tiles = $self->tiles;
382             my $timeouts = $self->timeouts;
383              
384             foreach my $key ( keys %{$tiles} ) {
385              
386             if ( $tiles->{$key} ) {
387              
388             if ( $timeouts->{$key} < $time ) {
389              
390             # For some reason, ignoring save when
391             # $self->autosave is true does not seem to
392             # consistently save the tile. So we always save
393             # it.
394              
395             $tiles->{$key}->save;
396              
397             $tiles->{$key} = undef;
398              
399             delete $timeouts->{$key};
400              
401             }
402              
403             }
404              
405             }
406              
407             $self->last_cleanup_time($time);
408             }
409             }
410              
411             =head2 C<_make_imager_wrapper_method>
412              
413             This is an I<internal> function generates wrapper methods for a tile's
414             L<Imager::Draw> methods.
415              
416             Basically, it calculates the bounding box for whatever is to be drawn, and creates a
417             L<Imager::Bing::MapLayer::Image> "pseudo-tile" to draw on.
418              
419             It then composes pieces from the pseudo tile onto the actual tile
420             (using the L</combine> method>).
421              
422             =cut
423              
424             sub _make_imager_wrapper_method {
425             my ( $class, $opts ) = @_;
426              
427             $opts->{args} //= [];
428             $opts->{name} //= "undef"; # to catch missing method names
429              
430             $class->meta->add_method(
431              
432             $opts->{name} => sub {
433              
434             my ( $self, %args ) = @_;
435              
436             return
437             if (
438             ( $args{'-min_level'} // $MIN_ZOOM_LEVEL ) > $self->level );
439             return
440             if (
441             ( $args{'-max_level'} // $MAX_ZOOM_LEVEL ) < $self->level );
442              
443             my %imager_args = $self->_translate_point_arguments(%args);
444              
445             foreach my $arg ( @{ $opts->{args} } ) {
446             $imager_args{$arg} = $args{$arg} if ( exists $args{$arg} );
447             }
448              
449             my ( $left, $top, $right, $bottom ) = bounding_box(%imager_args);
450              
451             # We create a temporary image and draw on it. We then
452             # compose the appropriate pieces of that image on each
453             # tile. This is faster than drawing the image on every
454             # tile, for complex polylines and polygons like geographic
455             # boundaries.
456              
457             # TODO - for very large polygons, this will require images
458             # that are too large for higher zoom levels. We need to
459             # modify this to render images on multiple pseudo-tiles.
460              
461             # TODO - get* methods should be handled differently.
462              
463             # Note: Try::Tiny does not catch malloc errors
464              
465             my $image = Imager::Bing::MapLayer::Image->new(
466             pixel_origin => [ $left, $top ],
467             width => 1 + $right - $left,
468             height => 1 + $bottom - $top,
469             );
470              
471             unless ($image) {
472              
473             confess
474             sprintf(
475             "unable to create image for (%d , %d) (%d , %d) at level %d: %s",
476             $left, $top, $right, $bottom, $self->level, $_ );
477              
478             }
479              
480             if ( my $method = $image->can( $opts->{name} ) ) {
481              
482             my $result = $image->$method(%imager_args);
483              
484             # Now get the tile boundaries
485              
486             my ( $tile_left, $tile_top )
487             = pixel_to_tile_coords( $left, $top );
488             my ( $tile_right, $tile_bottom )
489             = pixel_to_tile_coords( $right, $bottom );
490              
491             my $tiles = $self->tiles;
492             my $timeouts = $self->timeouts;
493              
494             for (
495             my $tile_y = $tile_top;
496             $tile_y <= $tile_bottom;
497             $tile_y++
498             )
499             {
500              
501             for (
502             my $tile_x = $tile_left;
503             $tile_x <= $tile_right;
504             $tile_x++
505             )
506             {
507              
508             my $key
509             = $self->_tile_coords_to_internal_key( $tile_x,
510             $tile_y );
511              
512             unless ( defined $tiles->{$key} ) {
513              
514             my $overwrite
515             = ( exists $tiles->{$key}
516             && $self->in_memory )
517             ? 0
518             : $self->overwrite;
519              
520             $tiles->{$key}
521             = $self->_load_tile( $tile_x, $tile_y,
522             $overwrite );
523              
524             $timeouts->{$key} = time() + $self->in_memory;
525             }
526              
527             if ( my $tile = $tiles->{$key} ) {
528              
529             my $crop_left = max( $left, $tile->left );
530             my $crop_top = max( $top, $tile->top );
531              
532             my $crop = $image->crop(
533             left => $crop_left,
534             top => $crop_top,
535             width => 1 + min(
536             $right - $crop_left,
537             $tile->right - $crop_left
538             ),
539             height => 1 + min(
540             $bottom - $crop_top,
541             $tile->bottom - $crop_top
542             ),
543             );
544              
545             $tile->compose(
546             src => $crop,
547             left => $crop_left,
548             top => $crop_top,
549             width => $crop->getwidth,
550             height => $crop->getheight,
551             combine => $self->combine,
552             );
553              
554             $crop = undef; # force garbage collection
555              
556             if ( $self->in_memory ) {
557              
558             $timeouts->{$key} = time() + $self->in_memory;
559              
560             $self->_cleanup_tiles();
561              
562             } else {
563              
564             # See comments about regarding
565             # autosave consistency.
566              
567             $tile->save;
568              
569             $tiles->{$key} = undef;
570              
571             }
572              
573             }
574              
575             }
576             }
577              
578             $image = undef; # force garbage collection
579              
580             } else {
581              
582             confess sprintf( "invalid method name: %s", $opts->{name} );
583              
584             }
585              
586             },
587             );
588              
589             }
590              
591             __PACKAGE__->_make_imager_wrapper_method( { name => 'radial_circle', } );
592              
593             __PACKAGE__->_make_imager_wrapper_method( { name => 'getpixel', } );
594              
595             __PACKAGE__->_make_imager_wrapper_method(
596             { name => 'setpixel',
597             args => [qw/ color /],
598             }
599             );
600              
601             __PACKAGE__->_make_imager_wrapper_method(
602             { name => 'line',
603             args => [qw/ color endp aa antialias /],
604             }
605             );
606              
607             __PACKAGE__->_make_imager_wrapper_method(
608             { name => 'box',
609             args => [qw/ color filled fill /],
610             }
611             );
612              
613             __PACKAGE__->_make_imager_wrapper_method(
614             { name => 'polyline',
615             args => [qw/ color aa antialias /],
616             }
617             );
618              
619             __PACKAGE__->_make_imager_wrapper_method(
620             { name => 'polygon',
621             args => [qw/ color fill /],
622             }
623             );
624              
625             __PACKAGE__->_make_imager_wrapper_method(
626             { name => 'arc',
627             args => [qw/ d1 d2 color fill aa filled /],
628             }
629             );
630              
631             __PACKAGE__->_make_imager_wrapper_method(
632             { name => 'circle',
633             args => [qw/ color fill aa filled /],
634             }
635             );
636              
637             __PACKAGE__->_make_imager_wrapper_method(
638             { name => 'flood_fill',
639             args => [qw/ color border fill /],
640             }
641             );
642              
643             __PACKAGE__->_make_imager_wrapper_method(
644             { name => 'string',
645             args => [
646             qw/ string font aa align channel color size sizew utf8 vlayout text /
647             ],
648             }
649             );
650              
651             __PACKAGE__->_make_imager_wrapper_method(
652             { name => 'align_string',
653             args => [
654             qw/ string font aa valign halign channel color size sizew utf8 vlayout text /
655             ],
656             }
657             );
658              
659             # TODO/FIXME - generic method with callbacks to apply a function to a
660             # all tiles on a level?
661              
662             =head2 C<filter>
663              
664             Apply a L<Imager::Filter> to every tile in the level.
665              
666             Only tiles that have been drawn to will have filters applied to them.
667              
668             =cut
669              
670             sub filter {
671             my ( $self, %args ) = @_;
672              
673             foreach my $key ( keys %{ $self->tiles } ) {
674              
675             my $tile = $self->tiles->{$key};
676              
677             unless ($tile) { # assume $self->in_memory
678              
679             my ( $tile_x, $tile_y )
680             = $self->_internal_key_to_tile_coords($key);
681              
682             # We assume that a tile should not be overwritten
683              
684             my $overwrite = $self->in_memory ? 0 : $self->overwrite;
685              
686             $tile = $self->_load_tile( $tile_x, $tile_y, $overwrite );
687              
688             }
689              
690             if ($tile) {
691              
692             $tile->image->filter(%args)
693             or confess $tile->image->errstr;
694              
695             # See comments abouve regarding autosave consistency.
696              
697             $tile->save;
698              
699             }
700             }
701              
702             }
703              
704             =head2 C<colourise>
705              
706             $level->colourise();
707              
708             Runs the C<colourise> method on tiles.
709              
710             This method is intended to be run for after rendering on the level is
711             completed, i.e. for post-processing of heatmap tiles.
712              
713             =cut
714              
715             sub colourise {
716             my ( $self, %args ) = @_;
717              
718             foreach my $key ( keys %{ $self->tiles } ) {
719              
720             my $tile = $self->tiles->{$key};
721              
722             unless ($tile) { # assume $self->in_memory
723              
724             my ( $tile_x, $tile_y )
725             = $self->_internal_key_to_tile_coords($key);
726              
727             # We assume that a tile should not be overwritten
728              
729             my $overwrite = $self->in_memory ? 0 : $self->overwrite;
730              
731             $tile = $self->_load_tile( $tile_x, $tile_y, $overwrite );
732              
733             }
734              
735             if ($tile) {
736              
737             $tile->colourise(%args);
738              
739             # See comments about regarding autosave consistency.
740              
741             $tile->save;
742              
743             }
744             }
745              
746             }
747              
748             =head2 C<save>
749              
750             $level->save();
751              
752             Saves the titles.
753              
754             If L<in_memory> is non-zero, tiles that have timed out are removed
755             from memory.
756              
757             =cut
758              
759             sub save {
760             my ( $self, @args ) = @_;
761              
762             $self->_cleanup_tiles();
763              
764             foreach my $tile ( values %{ $self->tiles } ) {
765             $tile->save(@args) if ($tile);
766             }
767             }
768              
769             use namespace::autoclean;
770              
771             1;