File Coverage

blib/lib/Imager/Bing/MapLayer/Image.pm
Criterion Covered Total %
statement 83 115 72.1
branch 11 20 55.0
condition 2 5 40.0
subroutine 38 41 92.6
pod 3 3 100.0
total 137 184 74.4


line stmt bran cond sub pod time code
1             package Imager::Bing::MapLayer::Image;
2              
3 5     5   2195 use v5.10.1;
  5         19  
  5         228  
4              
5 5     5   28 use Moose;
  5         7  
  5         41  
6 5     5   34804 use MooseX::StrictConstructor;
  5         13  
  5         48  
7              
8 5     5   15743 use Moose::Util::TypeConstraints;
  5         12  
  5         87  
9              
10 5     5   11236 use Class::MOP::Method;
  5         11  
  5         154  
11 5     5   29 use Const::Fast;
  5         11  
  5         55  
12 5     5   449 use Imager;
  5         10  
  5         45  
13 5     5   275 use Imager::Color;
  5         8  
  5         122  
14 5     5   3252 use Imager::Fill;
  5         12633  
  5         208  
15 5     5   4674 use Imager::Fountain;
  5         14957  
  5         200  
16 5     5   43 use List::Util 1.30 qw/ min pairmap /;
  5         177  
  5         404  
17              
18 5     5   35 use namespace::autoclean;
  5         11  
  5         58  
19              
20 5     5   529 use version 0.77; our $VERSION = version->declare('v0.1.9');
  5         120  
  5         51  
21              
22             =head1 NAME
23              
24             Imager::Bing::MapLayer::Image - a wrapper for L<Imager> objects
25              
26             =head1 SYNOPSIS
27              
28             my $image = Imager::Bing::MapLayer::Image->new(
29             pixel_origin => [ $left, $top ],
30             width => 1 + $right - $left,
31             height => 1 + $bottom - $top,
32             );
33              
34             =head1 DESCRIPTION
35              
36             This module is for internal use by L<Imager::Bing::MapLayer>.
37              
38             =begin :internal
39              
40             This is a base class for images that acts as a wrapper around
41             L<Imager> but automatically translates coordinates from the pixel
42             origin.
43              
44             This is mainly used for rendering a large polyline so that sections of
45             it can be composed onto tiles.
46              
47             =head1 ATTRIBUTES
48              
49             =head2 C<pixel_origin>
50              
51             The coordinates of the top-left point on the image.
52              
53             =cut
54              
55             has 'pixel_origin' => (
56             is => 'ro',
57             isa => 'ArrayRef',
58             );
59              
60             =head2 C<width>
61              
62             The width of the image.
63              
64             =cut
65              
66             has 'width' => (
67             is => 'ro',
68             isa => subtype( as 'Int', where { $_ >= 1 }, ),
69             required => 1,
70             );
71              
72             =head2 C<height>
73              
74             The height of the image.
75              
76             =cut
77              
78             has 'height' => (
79             is => 'ro',
80             isa => subtype( as 'Int', where { $_ >= 1 }, ),
81             required => 1,
82             );
83              
84             =head2 C<left>
85              
86             The left-most point of the C<x> axis of the image. This corresponds to
87             the C<x> coordinate of the C</pixel_origin>.
88              
89             =cut
90              
91             has 'left' => (
92             is => 'ro',
93             isa => 'Int',
94             default => sub {
95             my ($self) = @_;
96             my $origin = $self->pixel_origin;
97             return $origin->[0];
98             },
99             lazy => 1,
100             init_arg => undef,
101             );
102              
103             =head2 C<top>
104              
105             The top-most point of the C<y> axis on the image. This corresponds to
106             the C<y> coordinate of the C</pixel_origin>.
107              
108             =cut
109              
110             has 'top' => (
111             is => 'ro',
112             isa => 'Int',
113             default => sub {
114             my ($self) = @_;
115             my $origin = $self->pixel_origin;
116             return $origin->[1];
117             },
118             lazy => 1,
119             init_arg => undef,
120             );
121              
122             =head2 C<right>
123              
124             The rightmost point on the C<x> axis.
125              
126             =cut
127              
128             has 'right' => (
129             is => 'ro',
130             isa => 'Int',
131             default => sub {
132             my ($self) = @_;
133             return $self->left + $self->width - 1;
134             },
135             lazy => 1,
136             init_arg => undef,
137             );
138              
139             =head2 C<bottom>
140              
141             The bottom-most point of the C<y> axis.
142              
143             =cut
144              
145             has 'bottom' => (
146             is => 'ro',
147             isa => 'Int',
148             default => sub {
149             my ($self) = @_;
150             return $self->top + $self->height - 1;
151             },
152             lazy => 1,
153             init_arg => undef,
154             );
155              
156             =head2 C<image>
157              
158             The L<Imager> object.
159              
160             =cut
161              
162             has 'image' => (
163             is => 'ro',
164             isa => 'Imager',
165             lazy => 1,
166             default => sub {
167             my ($self) = @_;
168              
169             my $image = Imager->new(
170             xsize => $self->width,
171             ysize => $self->height,
172             channels => 4,
173             );
174              
175             # We draw a transparent white box on the image so as to fix
176             # any issues with colour composition.
177              
178             $image->box(
179             color => Imager::Color->new( 255, 255, 255, 0 ),
180             box => [ 0, 0, $self->width - 1, $self->height - 1 ],
181             );
182              
183             return $image;
184             },
185             init_arg => undef,
186             handles => [qw/ errstr getwidth getheight /],
187             );
188              
189             =head1 METHODS
190              
191             =head2 C<errstr>
192              
193             The L<Imager> error string.
194              
195             =cut
196              
197             sub _translate_x {
198 93     93   139 my ( $self, $x ) = @_;
199              
200 93         3824 my $left = $self->left;
201              
202 93 100       230 if ( ref $x ) {
203 4         11 return [ map { $_ - $left } @{$x} ];
  10         35  
  4         11  
204             } else {
205 89         325 return $x - $left;
206             }
207             }
208              
209             sub _translate_y {
210 93     93   143 my ( $self, $y ) = @_;
211              
212 93         4438 my $top = $self->top;
213              
214 93 100       229 if ( ref $y ) {
215 4         9 return [ map { $_ - $top } @{$y} ];
  10         31  
  4         11  
216             } else {
217 89         419 return $y - $top;
218             }
219             }
220              
221             sub _translate_points {
222 2     2   7 my ( $self, $points ) = @_;
223             return [
224 8         30 map {
225 2         8 [ $self->_translate_x( $_->[0] ), $self->_translate_y( $_->[1] ) ]
226 2         6 } @{$points}
227             ];
228             }
229              
230             sub _translate_coords {
231 2     2   8 my ( $self, $points ) = @_;
232 5     5   4372 no warnings 'once';
  5         10  
  5         7729  
233 4     4   25 return [ pairmap { ( $self->_translate_x($a), $self->_translate_y($b) ) }
234 2         19 @{$points} ];
  2         49  
235             }
236              
237             const my %ARG_TO_METHOD => (
238             points => '_translate_points',
239              
240             box => '_translate_coords',
241              
242             x => '_translate_x',
243             'y' => '_translate_y',
244              
245             x1 => '_translate_x',
246             y1 => '_translate_y',
247             x2 => '_translate_x',
248             y2 => '_translate_y',
249              
250             xmin => '_translate_x',
251             ymin => '_translate_y',
252             xmax => '_translate_x',
253             ymax => '_translate_y',
254              
255             left => '_translate_x',
256             top => '_translate_y',
257              
258             right => '_translate_x',
259             bottom => '_translate_y',
260              
261             );
262              
263             sub _translate_point_arguments {
264 83     83   235 my ( $self, %args ) = @_;
265              
266 83         113 my %i_args;
267              
268 83         444 foreach my $arg ( keys %ARG_TO_METHOD ) {
269              
270 1328 50       5344 if ( my $method = $self->can( $ARG_TO_METHOD{$arg} ) ) {
271              
272 1328 100       3343 $i_args{$arg} = $self->$method( $args{$arg} )
273             if ( exists $args{$arg} );
274              
275             }
276              
277             }
278              
279 83         504 return %i_args;
280             }
281              
282             =head2 C<_make_imager_wrapper_method>
283              
284             Rather than have a lot of cut-and-paste code for wrappers to L<Imager>
285             methods, we have a L<Moose> method for creating new methods.
286              
287             These methods translate the C<points>, C<x> and C<y> arguments for the
288             level into coordinates on the tile, and then run the corresponding
289             L<Imager> methods on the tile.
290              
291             =cut
292              
293             sub _make_imager_wrapper_method {
294 75     75   128 my ( $class, $opts ) = @_;
295              
296 75   100     237 $opts->{args} //= [];
297              
298             $class->meta->add_method(
299              
300             $opts->{name} => sub {
301              
302 83     83   27685 my ( $self, %args ) = @_;
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
        83      
303              
304 83         492 my %imager_args = $self->_translate_point_arguments(%args);
305              
306 83         165 foreach my $arg ( @{ $opts->{args} } ) {
  83         266  
307 256 100       691 $imager_args{$arg} = $args{$arg} if ( exists $args{$arg} );
308             }
309              
310 83         618 my $method = Imager->can( $opts->{name} );
311              
312             return wantarray
313 83 100       3560 ? ( $self->image->$method(%imager_args) )
314             : $self->image->$method(%imager_args);
315              
316             },
317 75         271 );
318              
319             }
320              
321             # TODO test copy, crop, paste and compose etc.
322              
323             =head2 C<copy>
324              
325             =cut
326              
327             __PACKAGE__->_make_imager_wrapper_method( { name => 'copy', } );
328              
329             =head2 C<crop>
330              
331             =cut
332              
333             __PACKAGE__->_make_imager_wrapper_method(
334             { name => 'crop',
335             args => [qw/ width height /],
336             }
337             );
338              
339             =head2 C<paste>
340              
341             =cut
342              
343             __PACKAGE__->_make_imager_wrapper_method(
344             { name => 'paste',
345             args => [
346             qw/ width height src img combine src_minx src_miny src_maxx src_maxy /
347             ],
348             }
349             );
350              
351             =head2 C<compose>
352              
353             =cut
354              
355             __PACKAGE__->_make_imager_wrapper_method(
356             { name => 'compose',
357             args => [
358             qw/ width height src combine opacity mask src_minx src_miny src_maxx src_maxy /
359             ],
360             }
361             );
362              
363             =head2 C<getpixel>
364              
365             This method used mainly for testing, and may not be usable from the
366             L<Imager::Bing::MapLayer::Level> and
367             L<Imager::Bing::MapLayer> objects that this tile belongs to.
368              
369             =cut
370              
371             __PACKAGE__->_make_imager_wrapper_method( { name => 'getpixel', } );
372              
373             =head2 C<setpixel>
374              
375             =cut
376              
377             __PACKAGE__->_make_imager_wrapper_method(
378             { name => 'setpixel',
379             args => [qw/ color /],
380             }
381             );
382              
383             =head2 C<line>
384              
385             =cut
386              
387             __PACKAGE__->_make_imager_wrapper_method(
388             { name => 'line',
389             args => [qw/ color endp aa antialias /],
390             }
391             );
392              
393             =head2 C<box>
394              
395             =cut
396              
397             __PACKAGE__->_make_imager_wrapper_method(
398             { name => 'box',
399             args => [qw/ color filled fill /],
400             }
401             );
402              
403             =head2 C<polyline>
404              
405             =cut
406              
407             __PACKAGE__->_make_imager_wrapper_method(
408             { name => 'polyline',
409             args => [qw/ color aa antialias /],
410             }
411             );
412              
413             =head2 C<polygon>
414              
415             =cut
416              
417             __PACKAGE__->_make_imager_wrapper_method(
418             { name => 'polygon',
419             args => [qw/ color fill /],
420             }
421             );
422              
423             =head2 C<arc>
424              
425             =cut
426              
427             __PACKAGE__->_make_imager_wrapper_method(
428             { name => 'arc',
429             args => [qw/ r d1 d2 color fill aa filled /],
430             }
431             );
432              
433             =head2 C<circle>
434              
435             =cut
436              
437             __PACKAGE__->_make_imager_wrapper_method(
438             { name => 'circle',
439             args => [qw/ r color fill aa filled /],
440             }
441             );
442              
443             =head2 C<flood_fill>
444              
445             =cut
446              
447             __PACKAGE__->_make_imager_wrapper_method(
448             { name => 'flood_fill',
449             args => [qw/ color border fill /],
450             }
451             );
452              
453             =head2 C<string>
454              
455             =cut
456              
457             __PACKAGE__->_make_imager_wrapper_method(
458             { name => 'string',
459             args => [
460             qw/ string font aa align channel color size sizew utf8 vlayout text /
461             ],
462             }
463             );
464              
465             =head2 C<align_string>
466              
467             =cut
468              
469             __PACKAGE__->_make_imager_wrapper_method(
470             { name => 'align_string',
471             args => [
472             qw/ string font aa valign halign channel color size sizew utf8 vlayout text /
473             ],
474             }
475             );
476              
477             =head2 C<radial_circle>
478              
479             Draw a fuzzy, "radial" greyscale circle: used for plotting points in a
480             heatmap. When all radial circles have been plotted, the L</colourise>
481             method should be run.
482              
483             =cut
484              
485             sub radial_circle {
486 0     0 1   my ( $self, %args ) = @_;
487              
488 0           my $center_x = $args{x};
489 0           my $center_y = $args{y};
490 0           my $radius = $args{r};
491              
492 0           state $palette;
493              
494 0 0         unless ($palette) {
495              
496 0           my $shades = 20;
497              
498 0           my ( @palette, @positions );
499              
500 0           foreach my $i ( 0 .. $shades ) {
501 0 0         my $alpha = $i ? int( sqrt( ( $i / $shades ) ) * 96 ) : 0;
502 0 0         my $val = $i ? int( ( 1 - $i / $shades ) * 128 ) + 128 : 255;
503 0           unshift @palette, Imager::Color->new( ($val) x 3, $alpha, );
504 0           push @positions, ( $i / $shades );
505             }
506              
507 0           $palette = Imager::Fountain->simple(
508             positions => \@positions,
509             colors => \@palette,
510             );
511              
512             }
513              
514 0           my $fill = Imager::Fill->new(
515             fountain => 'radial',
516             segments => $palette,
517             xa => $radius,
518             ya => $radius,
519             xb => 0,
520             yb => $radius,
521             super_sample => 'circle',
522             );
523              
524 0 0         if ( my $diam = ( $radius + $radius ) ) {
525              
526 0           my $circle = Imager->new(
527             xsize => $diam,
528             ysize => $diam,
529             channels => 4
530             );
531              
532 0           $circle->circle(
533             r => $radius,
534             x => $radius,
535             'y' => $radius,
536             aa => 1,
537             filled => 1,
538             fill => $fill,
539             );
540              
541 0           $self->compose(
542             src => $circle,
543             tx => $center_x - $radius,
544             ty => $center_y - $radius,
545             combine => 'normal', # TODO change this?
546             );
547             }
548             }
549              
550             # TODO/FIXME - generic method with callbacks to apply a function to a tile?
551              
552             =head2 C<colourise>
553              
554             =head2 C<colorize>
555              
556             $tile->colourise();
557              
558             The method colourises greyscale tiles.
559              
560             It is intended to be run for all tiles on a map when the rendering is
561             completed.
562              
563             Note that the the color of a pixel is determined by the opacity of the
564             the pixel, and not the gray level.
565              
566             =cut
567              
568             sub colourise {
569 0     0 1   my ( $self, %args ) = @_;
570              
571 0           state $colorize = {};
572              
573 0           my $img = $self->image;
574              
575 0           foreach my $y ( 0 .. $img->getheight - 1 ) {
576              
577 0           my @colors = $img->getscanline( 'y' => $y );
578 0           for ( my $i = 0; $i < @colors; $i++ ) {
579              
580 0           my $a = ( $colors[$i]->rgba )[-1];
581              
582 0   0       $colorize->{$a} //= Imager::Color->new(
583             hue => int( ( ( 255 - $a ) / 255 ) * 240 ),
584             saturation => 1.0,
585             value => 1.0,
586             alpha => min( $a, 128 ),
587             );
588              
589 0           $colors[$i] = $colorize->{$a};
590              
591             }
592              
593 0           $img->setscanline( 'y' => $y, pixels => \@colors );
594              
595             }
596              
597 0           return 1;
598             }
599              
600             sub colorize {
601 0     0 1   my ( $self, %args ) = @_;
602 0           $self->colourise(%args);
603             }
604              
605             =end :internal
606              
607             =cut
608              
609 5     5   36 use namespace::autoclean;
  5         8  
  5         26  
610              
611             1;