File Coverage

blib/lib/Image/Heatmap.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Image::Heatmap;
2              
3 1     1   19571 use strict;
  1         2  
  1         38  
4 1     1   4 use warnings;
  1         2  
  1         26  
5              
6 1     1   1324 use Image::Magick;
  0            
  0            
7              
8             our $VERSION = join( '.', 0, sprintf( '%03d', map{ $_ - 47 + 500 } ( '$Rev: 112 $' =~ /(\d+)/g ) ) );
9             our $DEBUG = 0;
10              
11             use constant {
12             TRANSPARENCY_V1 => 0x1,
13             TRANSPARENCY_V2 => 0x2,
14             };
15              
16             sub new {
17             my $self = bless( Image::Heatmap::private::next_oid(), shift );
18              
19             Image::Heatmap::private::init();
20              
21             # Defaults
22             $self->tmp_dir('/tmp/');
23             $self->transparent_bg(0);
24             $self->transparent_version(TRANSPARENCY_V2);
25             $self->processes(1);
26             $self->x_adjust(0);
27             $self->y_adjust(0);
28             $self->width_adjust(0);
29             $self->height_adjust(0);
30             $self->output('heatmap.png');
31             $self->colors('colors.png');
32             $self->plot_base('bolilla.png');
33              
34             return $self;
35             }
36              
37             sub process {
38             my ( $self ) = @_;
39              
40             if ( my $error = Image::Heatmap::private::validate($self) ) {
41             Image::Heatmap::private::throw($error);
42             }
43              
44             my $max_rep = 0;
45              
46             my $width = $self->width();
47             my $height = $self->height();
48             my $map = Image::Magick->new();
49             $map->Read( $self->map() );
50             $self->image_width( $map->Get('width') );
51             $self->image_height( $map->Get('height') );
52              
53             # If there is no width/height defined then we will default
54             # to what the image is defined to. We will trust the implementor
55             # of this module knows what they're doing, otherwise.
56             unless ( $width && $height ) {
57             $width = $self->image_width();
58             $height = $self->image_height();
59             }
60             note("W x H: $width x $height");
61              
62             unless ( $self->width_adjust() && $self->height_adjust() ) {
63             $self->width_adjust( $self->image_width() );
64             $self->height_adjust( $self->image_height() );
65             note('W x H Adjust: ' . join( ' x ', map{ $self->$_() } qw( width_adjust height_adjust ) ) );
66             }
67              
68             my $loader;
69              
70             if ( my $sth = $self->statement() ) {
71             my $sth = $self->statement();
72             $sth->execute();
73             $loader = sub {
74             return $sth->fetchrow_hashref();
75             };
76             }
77             elsif ( my $geo_list = $self->geo_list() ) {
78             $loader = sub {
79             return shift( @$geo_list );
80             }
81             }
82             else {
83             Image::Heatmap::private::throw(
84             'No value for "statement" or "geo_list"... not sure what you want from me.'
85             );
86             }
87              
88              
89             while ( my $point = $loader->() ) {
90             my ( $lat, $lng ) = @$point{ qw( latitude longitude ) };
91              
92             # Make sure a lat/lng exist
93             Image::Heatmap::private::throw(
94             'Invalid parameters in statement, must include "latitude" and "longitude"'
95             ) unless ( defined($lat) && defined($lng) );
96              
97             my $x = ( 180 + $lng ) * ( $width / 360 );
98             my $y = ( 90 - $lat ) * ( $height / 180 );
99              
100             $_ *= $self->zoom() || 1 for ( ( $x, $y ) );
101              
102             $x += $self->x_adjust();
103             $y += $self->y_adjust();
104              
105             my $coords = join( '|', $lat, $lng );
106             Image::Heatmap::private::shove( $self => 'coords', [ $x, $y ] );
107             my $reps = Image::Heatmap::private::get( $self => 'reps' => $coords );
108             $max_rep = $reps->{$coords} if ( ++$reps->{$coords} > $max_rep );
109             }
110              
111             my $x_canvas = $width * ( $self->width_adjust() / $width );
112             my $y_canvas = $height * ( $self->height_adjust() / $height );
113             note("$width x $height vs. $x_canvas x $y_canvas");
114              
115             my $kid_seed = int( rand( time() ) );
116             my $kid_layer = "layer_slice-%d-$kid_seed.png";
117             my $kids = $self->processes();
118             my @children = ();
119             foreach my $child_num ( 1 .. $kids ) {
120             $children[ $child_num - 1 ] = Image::Heatmap::private::distribute_work($self);
121              
122             Image::Heatmap::private::throw(
123             'Error when generating sub-process'
124             ) unless ( defined( $children[-1] ) );
125              
126             unless ( $children[-1] ) {
127              
128             note("Resize -geometry ${x_canvas}x${y_canvas}");
129             my $child_layer = Image::Magick->new( size => "${x_canvas}x${y_canvas}");
130             $child_layer->Read('pattern:gray100');
131              
132             my $cperc = int( 100 / ( $max_rep || 1 ) );
133             $cperc /= 2 if ( $cperc > 80 );
134             note("Colorize -fill white -opacity $cperc%");
135             my $plot = Image::Magick->new();
136             $plot->Read( $self->plot_base() );
137             $plot->Resize( $self->plot_size() ) if ( $self->plot_size() );
138             $plot->Colorize( fill => 'white', 'opacity' => "$cperc%" );
139              
140             my @coords = @{ Image::Heatmap::private::get( $self => 'coords' ) || [] };
141             my $bucket_size = scalar( @coords ) / $kids;
142             my $bucket_offset = ( $child_num - 1 ) * $bucket_size;
143             my @new_coords = splice( @coords, $bucket_offset, $bucket_size );
144              
145             foreach my $coordinate ( @new_coords ) {
146             my ( $x, $y ) = @$coordinate;
147             note("Composite -compose Multiply -geometry +$x+$y");
148              
149             $child_layer->Composite(
150             'image' => $plot,
151             'compose' => 'Multiply',
152             'geometry' => "+$x+$y",
153             );
154             }
155              
156             my $child_image = sprintf( $kid_layer, $child_num );
157             note("Write $child_image");
158             $child_layer->Write( $self->tmp_dir() . $child_image );
159              
160             Image::Heatmap::private::finish_work($self);
161             }
162             }
163              
164             foreach my $child ( @children ) {
165             note("Blocking wait on pid:$child");
166             my $pid_state = waitpid( $child, 0 );
167             note("pid:$child - $pid_state :: $?");
168             }
169              
170             my $layer = Image::Magick->new( size => "${x_canvas}x${y_canvas}");
171             $layer->Read('pattern:gray100');
172              
173             foreach my $child_num ( 1 .. $kids ) {
174             my $child_image = $self->tmp_dir() . sprintf( $kid_layer, $child_num );
175             my $child_slice = Image::Magick->new();
176             $child_slice->Read($child_image);
177              
178             note("Composite -image $child_image -compose Multiply -geometry +0+0");
179             $layer->Composite(
180             'image' => $child_slice,
181             'compose' => 'Multiply',
182             'geometry' => '+0+0',
183             );
184              
185             unlink($child_image);
186             }
187              
188             note("Negate && Fx -expression v.p{0,u*v.h}");
189             $layer->Negate();
190             $layer->Read( $self->colors() );
191             my $fx = $layer->Fx( 'expression' => 'v.p{0,u*v.h}' );
192              
193             note("Composite -image $map -compose Blend -blend 40%");
194             $fx->Composite(
195             'image' => $map,
196             'compose' => 'Multiply',
197             'blend' => '+0+0',
198             );
199              
200             if ( $self->transparent_bg() ) {
201              
202             if ( $self->transparent_version() == ( TRANSPARENCY_V1 | TRANSPARENCY_V2 ) ) {
203             throw('Only a single transparency version is allowed at one time.');
204             }
205             elsif ( $self->transparent_version() & TRANSPARENCY_V1 ) {
206             my ( $rx, $gx, $bx ) = $fx->GetPixel(
207             'x' => $fx->Get('width'),
208             'y' => $fx->Get('height'),
209             );
210              
211             my ( $r, $g, $b );
212             foreach my $x_new ( 0 .. $fx->Get('width') ) {
213             foreach my $y_new ( 0 .. $fx->Get('height') ) {
214             ( $r, $g, $b ) = $fx->GetPixel( 'x' => $x_new, 'y' => $y_new );
215             if ( $r == $rx && $b == $bx && $g == $gx ) {
216             $fx->SetPixel( 'x' => $x_new, 'y' => $y_new, 'color' => [ 1,1,1 ] );
217             }
218             }
219             }
220             $fx->Transparent( 'color' => '#FFFFFF' );
221             }
222             elsif ( $self->transparent_version() & TRANSPARENCY_V2 ) {
223             my $trans_width = $fx->Get('width') - 1;
224             my $trans_height = $fx->Get('height') - 1;
225              
226             foreach my $trans_coord (
227             [ 0, 0 ],
228             [ $trans_width, 0 ],
229             [ 0, $trans_height ],
230             [ $trans_width, $trans_height ],
231             ) {
232             my ( $x, $y ) = @$trans_coord;
233             $fx->MatteFloodfill(
234             'x' => $x,
235             'y' => $y,
236             'fill' => 'rgb(255,255,255,0)',
237             );
238             }
239             }
240             }
241              
242             if ( my $contrast = $self->contrast() ) {
243             eval { $fx->SigmoidalContrast(
244             'contrast' => $contrast,
245             'sharpen' => 'True',
246             'channel' => 'All',
247             'mid-point' => '50%',
248             ) };
249             }
250              
251             $fx->Write( $self->output() );
252              
253             if ( my $thumbnail = $self->thumbnail() ) {
254             note("Thumbnail : $thumbnail");
255             if ( my $scale = $self->thumbnail_scale() ) {
256             note('Scale to : ' . ( int( $scale * 100 ) ) . '%');
257             $fx->Resize( 'geometry' => join( 'x', ( $x_canvas * $scale ), ( $y_canvas * $scale ) ) );
258             $fx->Write($thumbnail);
259             }
260             }
261              
262             return;
263             }
264              
265             *note = \&Image::Heatmap::private::note;
266             # *poke = \&Image::Heatmap::private::poke;
267              
268             sub DESTROY {
269             my ($self) = @_;
270             Image::Heatmap::private::release_oid($self);
271             }
272              
273             1;
274              
275             package Image::Heatmap::private;
276              
277             use strict;
278             use warnings;
279              
280             use File::Find;
281              
282             use constant {
283             FOUND_INDICATION => 'FOUND',
284             };
285              
286             my %stash = ();
287              
288             {
289             my $no_op = 0;
290             sub init {
291             return if ( $no_op );
292             foreach my $accessor ( qw(
293              
294             processes
295             statement
296             geo_list
297              
298             map
299             tmp_dir
300             output
301             parent
302              
303             thumbnail
304             thumbnail_scale
305              
306             transparent_bg
307             transparent_version
308              
309             contrast
310              
311             colors
312             plot_base
313              
314             plot_size
315             image_width
316             image_height
317             zoom
318             x_adjust
319             y_adjust
320             width
321             width_adjust
322             height
323             height_adjust
324              
325             ) ) {
326             if ( Image::Heatmap->can($accessor) ) {
327             $no_op = 1;
328             return;
329             }
330              
331             {
332             no strict 'refs';
333             *{ "Image::Heatmap::$accessor" } = sub {
334             return &Image::Heatmap::private::accessor( shift, $accessor, @_ );
335             }
336             }
337             }
338             }
339             }
340              
341             sub distribute_work {
342             my ($self) = @_;
343              
344             $self->parent($$);
345              
346             return 0 if ( $self->processes() == 1 );
347             return fork();
348             }
349              
350             sub finish_work {
351             my ($self) = @_;
352             exit if ( $self->parent() != $$ );
353             return;
354             }
355              
356             {
357             my %validators = (
358             'map' => {
359             'valid' => sub{
360             my $self = shift;
361             $self->map() && -r $self->map() && -f $self->map()
362             },
363             'message' => 'Map image (map) must be defined and have accomidating file permissions.',
364             },
365             'tmp_dir' => {
366             'valid' => sub{
367             my $self = shift;
368             $self->tmp_dir() && -d $self->tmp_dir()
369             },
370             'message' => 'Working directory (tmp_dir) must be defined and have accomidating permissions.',
371             },
372             );
373             sub validate {
374             my ($self) = @_;
375              
376             foreach my $validator ( keys %validators ) {
377             note("Validating \"$validator\"");
378             unless ( &{ $validators{$validator}{'valid'} }( $self ) ) {
379             return $validators{$validator}{'message'};
380             }
381             }
382              
383             my $tmp_dir = $self->tmp_dir();
384             $tmp_dir .= '/' unless ( $tmp_dir =~ /\/$/ );
385             $self->tmp_dir($tmp_dir);
386              
387             foreach my $finder ( qw( colors plot_base ) ) {
388             my $file = $self->$finder();
389             note( join( ' :: ', map{ $_ || 'n/a' } ( $file, -r $file ) ) );
390             unless ( -r $file ) {
391              
392             my $file_location;
393             my $did_find = FOUND_INDICATION;
394             my ( $file_path ) = $INC{'Image/Heatmap.pm'} =~ /^(.*)\/Image\/Heatmap.pm$/ || '.';
395             $file_path = $1 || '.';
396              
397             eval{
398             File::Find::find(
399             {
400             'no_chdir' => 1,
401             'follow_fast' => 1,
402             'wanted' => sub {
403             return unless ( $_ =~ /.*\/$file$/ );
404             $file_location = $_;
405             throw($did_find);
406             },
407             },
408             $file_path,
409             );
410             };
411              
412             if ( my $e = $@ ) {
413             if ( $file_location && "$e" =~ /$did_find/ ) {
414             note("Setting \"$finder\" to \"$file_location\"");
415             $self->$finder($file_location);
416             next;
417             }
418             else {
419             throw($e);
420             }
421             }
422             else {
423             throw('Invalid return in seeking file: "' . ( $self->$finder() ) . '"');
424             }
425             }
426             }
427              
428             return undef;
429             }
430             }
431              
432             sub accessor {
433             my ( $self, $method, $content ) = @_;
434              
435             if ( defined($content) ) {
436             return set( $self => $method, $content );
437             }
438             else {
439             return get( $self => $method );
440             }
441             }
442              
443             sub throw {
444             my $caller = caller;
445             die(
446             "$caller :: " . ( join(
447             ' :: ',
448             map{
449             ( ref( $_ ) ) ? ref($_) : $_
450             } @_
451             ) )
452             );
453             }
454              
455             sub shove {
456             my ( $self, @depth ) = @_;
457              
458             my $content = pop(@depth);
459             my $key = pop(@depth);
460             my $depth = get_depth( ( $$self, @depth ) );
461             push( @{ $depth->{$key} ||= [] }, $content );
462             return $content;
463             }
464              
465             sub set {
466             my ( $self, @depth ) = @_;
467              
468             my $content = pop(@depth);
469             my $key = pop(@depth);
470             my $depth = get_depth( ( $$self, @depth ) );
471             $depth->{$key} = $content;
472             return $content;
473             }
474              
475             sub get {
476             my ( $self, @depth ) = @_;
477              
478             my $key = pop(@depth);
479             my $depth = get_depth( ( $$self, @depth ) );
480             return $depth->{$key};
481             }
482              
483             sub get_depth {
484             my $level = \%stash;
485             $level = $level->{$_} ||= {} foreach ( @_ );
486             return $level;
487             }
488              
489             sub note {
490             return unless ( $DEBUG );
491             return notify(@_);
492             }
493              
494             sub poke {
495             return notify(@_);
496             }
497              
498             sub notify {
499             my ( $message ) = @_;
500              
501             my $stringer = ( ref($message) )
502             ? sub{
503             require Data::Dumper;
504             return Data::Dumper::Dumper( $_[0] )
505             }
506             : sub{ return $_[0]; };
507              
508             warn( sprintf( "[%s] - %d - %s\n", scalar(localtime()), $$, &$stringer($message) ) );
509             }
510              
511             {
512             my @oids = ();
513             my $current_oid = 0;
514             sub release_oid {
515             my ($self) = @_;
516             push( @oids, $$self );
517             return;
518             }
519              
520             sub next_oid {
521             my $next = shift(@oids) || $current_oid++;
522             return \$next;
523             }
524             }
525              
526             sub death {
527             require Data::Dumper;
528             die( Data::Dumper::Dumper(\%stash) );
529             }
530              
531             1;
532              
533             __END__