#!/usr/bin/perl -w # Copyright (c) 2011 Mathieu Alorent # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . package Graphics::HotMap; use strict; =head1 NAME Graphics::HotMap -- generate thermographic images. =head1 SYNOPSIS =for example use Graphics::HotMap; # Create a new HotMap my $hotMap = Graphics::HotMap->new( minValue => 1, maxValue => 50, ); # Define scale $hotMap->scale(20); # Show legend $hotMap->legend(1); # Show CrossMarks and values $hotMap->crossMark(1,1); # Define a new size $hotMap->mapSize({ sizeX => 15, sizeY => 15 }); # Add time $hotMap->addHorodatage(time, 15, 30); # Add layer $hotMap->addLayer({ layerName => '10_back', visibility => 1, sliceColor => 1 }); # Add a zone $hotMap->addZone({ zoneName => 'AllMap', layerName => '10_back', coordonates => [0,0,14,14], border => 1, }); # And add some points $hotMap->addPoint({ layerName => '10_back', x => 2, y => 2, value => 15 }); $hotMap->addPoint({ layerName => '10_back', x => 1, y => 6, value => 5 }); $hotMap->addPoint({ layerName => '10_back', x => 9, y => 13, value => 25 }); $hotMap->addLayer({ layerName => '20_inner' }); # Add a zone $hotMap->addZone({ zoneName => 'innerZone', layerName => '20_inner', coordonates => [4,0,9,6], border => 1, text => 'Inner Zone', }); # And some points $hotMap->addPoint({ layerName => '20_inner', x => 5, y => 1, value => 1 }); $hotMap->addPoint({ layerName => '20_inner', x => 6, y => 5, value => 9 }); # You can also prepare conf as a Hash, ... my %other = ( layers => { '30_anotherLayer' => { visibility => 0, sliceColors => 1, }, '40_anotherLayer' => { visibility => 0, sliceColors => 0, }, }, zones => { anotherZone => { layerName => '30_anotherLayer', coordonates => [7,4,10,9], border => 1, text => 'other layer', textSize => 8, textColor => 'magenta', }, zoneA => { layerName => '40_anotherLayer', coordonates => [0,10,1,12], border => 1, text => 'black', textSize => 8, textColor => 'white', }, zoneB => { layerName => '40_anotherLayer', coordonates => [1,10,2,12], border => 1, text => 'blue', textSize => 8, textColor => 'white', }, zoneC => { layerName => '40_anotherLayer', coordonates => [2,10,3,12], border => 1, text => 'green', textSize => 8, textColor => 'white', }, zoneD => { layerName => '40_anotherLayer', coordonates => [3,10,4,12], border => 1, text => 'cyan', textSize => 8, textColor => 'white', }, }, points => { '30_anotherLayer' => [ [8,5,46], [10,9,22], ], '10_back' => [ [13,1,50], ], '40_anotherLayer' => [ [0,10,1], [1,10,2], [2,10,3], [3,10,4], ], }, ); # ..., and import/add it $hotMap->addConfs(\%other); # Run the interpolation and generate and image $hotMap->genImage; # Save the image a a PNG file $hotMap->genImagePng('MyTest.png'); # print the text representation of the map print $hotMap->toString('floor') if $hotMap->scale < 3; =head1 DESCRIPTION Generate thermographic images from a few know points. Others values are interpolated. Graphics::HotMap use PDL to work on matrix. PDL can compute very very large matrix in a few seconds. See L =head2 FUNCTIONS =over 4 =cut use Data::Dumper; use Image::Magick; use Math::Gradient qw(multi_array_gradient); use PDL; use PDL::NiceSlice; use PDL::IO::Pic; use POSIX qw(strftime); use File::Temp qw/ :POSIX /; use File::Temp qw/ tempfile tempdir /; use constant { PALETTE_SLICE => 35, }; our $VERSION = '0.0001'; =item new() =for ref Construct and return a new HotMap Object; =for usage Graphics::HotMap->new( outfileGif => , # file to write GIF outfilePng => , # file to write PNG legend => [0|1], # activate lengend legendNbGrad => , # Number a graduation cross => , # activate crossing of known values crossValues => , # activate values printing whith cross minValue => , # minimum value maxValue => , # maximum value font => , fontSize => , # font size scale => , # scale values and coordonates sizeX => , # X size sizeY => , # Y size ); =for exemple my $hotMap = Graphics::HotMap->new( sizeX => 10, sizeY => 10, minValue => 1, maxvalue => 50, ); =cut sub new { my ($class, %params) = (@_); my $self={}; $self->{_outfileGif} = $params{outfileGif} || undef; $self->{_outfilePng} = $params{outfilePng} || undef; $self->{_legend} = $params{legend} || 0; $self->{_legendNbGrad} = $params{legendNbGrad} || 7; $self->{_crossMark} = $params{cross} || 0; $self->{_crossMarkTemp}= $params{crossTemp} || 0; #$self->{_minValue} = $params{minValue} || 0; #$self->{_maxValue} = $params{maxValue} || 70; $self->{_font} = $params{font} || '/usr/share/fonts/truetype/freefont/FreeSans.ttf'; $self->{_fontSize} = $params{fontSize} || 15; $self->{_text} = (); $self->{_horodatage} = $params{horodatage} || [0, 0, 0]; $self->{_scale} = $params{echelle} || 1; $self->{_verbose} = $params{verbose} || 0; $self->{_mapSize}{x} = $params{sizeX} || 30; $self->{_mapSize}{y} = $params{sizeY} || 20; $self->{_knownPoints} = {}; $self->{_mapPoints} = PDL->zeroes(1); bless $self, $class; #$self->gradient(20, ([0,0,255],[0,255,255],[0,255,0],[255,255,0],[255,0,0])); return $self; } =item initKnownPoints() =for ref Reset all know points. =cut sub initKnownPoints { my $self = shift; $self->{_knownPoints} = {}; } =item mapSize() =for ref Set or Return mapSize =for exemple $hotMap->mapSize({sizeX => 15, sizeY => 15}); # Set map size @size = $hotMap->mapSize; # Return the actual map size =cut sub mapSize { my $self = shift; my ($dimentions) = @_; if (defined $dimentions) { die ("mapSize: You must set sizeX and sizeY.",$/) unless (defined $dimentions->{sizeX} && defined $dimentions->{sizeY}); $self->{_mapSize}{x} = ($dimentions->{sizeX} ) * $self->{_scale}; $self->{_mapSize}{y} = ($dimentions->{sizeY} ) * $self->{_scale}; $self->{_mapPoints} = PDL->zeroes($self->{_mapSize}{x}, $self->{_mapSize}{y}); $self->initKnownPoints; } else { return [$self->{_mapSize}{x}, $self->{_mapSize}{y}]; } } =item scale() =for ref Set or Return current scale factor. =for exemple $hotMap->scale(2); $scale = hotMap->scale; =cut sub scale { my $self = shift; my ($scaleFactor) = @_; if (defined $scaleFactor) { die ("setScale: scaleFactor must be > 0. => '$scaleFactor'",$/) unless $scaleFactor > 0; $self->{_scale} = $scaleFactor; } else { return $self->{_scale}; } } =for exemple Internal function to scale point coordonates =cut sub _scalePoint { my $self = shift; my ($x, $y) = @_; return ( (0.5+$x) * $self->{_scale} -0.5, (0.5+$y) * $self->{_scale} -0.5, ); } =for exemple Internal function to verify that point is inside matrix =cut sub _isPointInside { my $self = shift; my ($x, $y) = @_; return 0 <= $x && $x <= $self->{_mapSize}{x} && 0 <= $y && $y <= $self->{_mapSize}{y}; } =item addLayer() =for ref Define a new Layer to store values. Layers are parsed by alphabetical order. * visibility (default: 1) : allow crossMarks to be displayed for this layer. * sliceColor (default: 1) : colors are looked up in the gradient. If set to 0, values between 0 to 16 are fixed colors (LUT). =for exemple $hotMap->addLayer({ layerName => 'Layer1' }); $hotMap->addLayer({ layerName => 'Layer2', visibility => 1, sliceColor => 1 }); =cut sub addLayer { my $self = shift; my ($params) = @_; my $layerName = $params->{layerName}; my $visibility = $params->{visibility}; my $sliceColors = $params->{sliceColors}; my $gradientName = $params->{gradientName}; my $maskIfNoValue= $params->{maskIfNoValue}; $sliceColors = 1 unless defined $sliceColors; $visibility = 1 unless defined $visibility; $maskIfNoValue = 0 unless defined $maskIfNoValue; die ("addLayer: You must provide a layer name.",$/) unless defined $layerName; die ("addLayer: Gradient must be defined: '$gradientName'.",$/) unless (defined $gradientName && defined $self->{_gradient}{$gradientName}); $self->{_layers}{$layerName}{sliceColors} = $sliceColors; $self->{_layers}{$layerName}{visibility} = $visibility; $self->{_layers}{$layerName}{gradientName} = $gradientName; $self->{_layers}{$layerName}{maskIfNoValue}= $maskIfNoValue; $self->{_knownPoints}{$layerName} = PDL->zeroes($self->{_mapSize}{x}, $self->{_mapSize}{y}); } =item addZone() =for ref Define a new zone to interpolate over a layer. =for exemple $hotMap->addZone({ zoneName => 'AllMap', # zone name layerName => 'Layer1', # layer from which zone belongs coordonates => [0,0,9,9], # coordonates [startX, startY, endX, endY] border => 1, # border color (LUT) or undef for none text => "your text", # textSize => 10, # textColor => 'red', # noScale = 0, # if true, coordonates will not be auto-scaled }); =cut sub addZone { my $self = shift; my ($params) = @_; my $layerName = $params->{layerName}; my $zoneName = $params->{zoneName}; my $coordonates = $params->{coordonates}; my $border = $params->{border}; my $text = $params->{text}; my $textSize = $params->{textSize}; my $textColor = $params->{textColor}; my $noScale = $params->{noScale}; if (!defined $noScale && !$noScale) { ($coordonates->[0], $coordonates->[1]) = $self->_scalePoint($coordonates->[0], $coordonates->[1]); ($coordonates->[2], $coordonates->[3]) = $self->_scalePoint($coordonates->[2], $coordonates->[3]); } die ("addZone: You must provide a layer name.",$/) unless defined $layerName; die ("addZone: You must provide a zone name.",$/) unless defined $zoneName; die ("addZone: Zone must not be over mapSize limits. '$coordonates->[0],$coordonates->[1];$coordonates->[2],$coordonates->[3]'",$/) unless ( $self->_isPointInside($coordonates->[0], $coordonates->[1]) && $self->_isPointInside($coordonates->[2], $coordonates->[3]) ); $self->{_zones}{$layerName}{$zoneName}{coordonates} = $coordonates; $self->{_zones}{$layerName}{$zoneName}{border} = $border; $self->addText ( { x => $coordonates->[0] + ($coordonates->[2] - $coordonates->[0])/2, y => $coordonates->[1] + ($coordonates->[3] - $coordonates->[1])/2, text => $text, size => $textSize, align => 'center', color => $textColor, } ) if (defined $text); } =item addPoint() =for ref Add a know point to a zone. Zone should first be declared with addZone. =for exemple $hotMap->addPoint({ layerName => 'AllMap', x => 7, y => 8, value => 25, noScale => 0, }); =cut sub addPoint { my $self = shift; my ($params) = @_; my $layerName = $params->{layerName}; my $x = $params->{x}; my $y = $params->{y}; my $value = $params->{value}; my $noScale = $params->{noScale}; my $gradientName = $self->{_layers}{$layerName}{gradientName}; ($x, $y) = $self->_scalePoint($x, $y) unless (defined $noScale && $noScale); if ( ! $value > 0) { warn ("addPoint: Only values > 0 accepted. => '$value'",$/); return; } #if ( ! $value <= ) { # warn ("addPoint: Only values > 0 accepted. => '$value'",$/); # return; #} die ("addPoint: Point should be inside ;) => '$x:$y' : '$self->{_mapSize}{x}:$self->{_mapSize}{y}' ($params->{x}, $params->{y}) $params->{layerName}",$/) unless $self->_isPointInside($x, $y); die ("addPoint: layer must exists. => '$layerName'",$/) unless defined $self->{_knownPoints}{$layerName}; die ("addPoint: Value outside gradient Limits. Value => $value : Limits => ". $self->{_gradient}{$gradientName}{minValue} .' <=> '. $self->{_gradient}{$gradientName}{maxValue},$/) unless ($self->{_gradient}{$gradientName}{minValue} <= $value && $value <= $self->{_gradient}{$gradientName}{maxValue}); $self->{_knownPoints}{$layerName}->set($x, $y, $value); } =item addHorodatage($timestamp, $x, $y) =for ref Timestamp on the image. =for exemple $hotMap->addHorodatage(time, 10, 10); $hotMap->addHorodatage(1269122338, 10, 10); =cut sub addHorodatage { my $self = shift; my ($time, $x, $y) = @_; die ("You must set time and coordonates.",$/) unless (defined $time && defined $x && defined $y); $self->{_horodatage} = [$time, $x, $y]; } =item addText() =for ref Add text on the image. =for exemple $hotMap->addText({ text => "your text", x => $x, y => $y, font => pointsize => 10, fill => 'black', align => '[left|center|right], }); =cut sub addText { my $self = shift; my ($params) = @_; push (@{$self->{_text}}, $params); } =item addConfs() =for ref Add Layers/Zones/Point from a hash config. =for exemple my %other = ( layers => { '30_anotherLayer' => { visibility => 0, sliceColors => 1, }, }, zones => { anotherZone => { layerName => '30_anotherLayer', coordonates => [7,4,10,9], border => 1, text => 'other layer', textSize => 8, textColor => 'magenta', }, }, points => { '30_anotherLayer' => [ [8,5,46], [10,9,22], ], }, ); $hotMap->addConfs(\%other); =cut sub addConfs { my $self = shift; my ($params) = @_; foreach my $layerName (keys %{$params->{layers}}) { $self->addLayer({ layerName => $layerName, visibility => $params->{layers}{$layerName}{visibility}, sliceColors => $params->{layers}{$layerName}{sliceColors}, gradientName => $params->{layers}{$layerName}{gradientName}, maskIfNoValue=> $params->{layers}{$layerName}{maskIfNoValue}, }); } foreach my $zoneName (keys %{$params->{zones}}) { $self->addZone({ layerName => $params->{zones}{$zoneName}{layerName}, zoneName => $zoneName, coordonates => $params->{zones}{$zoneName}{coordonates}, border => $params->{zones}{$zoneName}{border}, text => $params->{zones}{$zoneName}{text}, textSize => $params->{zones}{$zoneName}{textSize}, textColor => $params->{zones}{$zoneName}{textColor}, noScale => $params->{zones}{$zoneName}{noScale}, }); } foreach my $layerName (keys %{$params->{points}}) { foreach my $point (@{$params->{points}{$layerName}}) { $self->addPoint({ layerName => $layerName, x => $point->[0], y => $point->[1], value => $point->[2], noScale => $point->[3], }); } } } =item getPoint() =for ref Return a point value at coordonate x/y. Without a zone name, it returns a point from the interpolated table. With zone name, it returns a point from that zone. =for exemple $hotMap->getPoint(6, 2, 'Zone1') $hotMap->getPoint(6, 2) =cut sub getPoint { my $self = shift; my ($x, $y, $layerName) = @_; if (defined $layerName) { die ("getPoint: layer must be defined. => '$layerName'",$/) unless defined $self->{_knownPoints}{$layerName}; return $self->{_knownPoints}{$layerName}->at($x,$y); } return $self->{_mapPoints}->at($x,$y); } =item fusionLayers() =for ref Fusion the second layer to the first one. =for exemple $hotMap->fusionLayers('AllMap', 'Zone1'); =cut sub fusionLayers { my $self = shift; my ($dest, $orig) = @_; die ("fusionLayers: layers must be defined. => '$dest, $orig'",$/) unless (defined $self->{_knownPoints}{$dest} && defined $self->{_knownPoints}{$orig}); $self->{_knownPoints}{$dest}->inplace->or2($self->{_knownPoints}{$orig}, 0); #$self->{_knownPoints}{$dest} += $self->{_knownPoints}{$orig}; } =item getLayer() =for ref Return all values from a layer. =for exemple my $piddleVal = $hotMap->getLayer('AllMap'); =cut sub getLayer { my $self = shift; my ($layer) = @_; die ("getLayer: layer must be defined. => '$layer'",$/) unless (defined $self->{_knownPoints}{$layer}); return $self->{_knownPoints}{$layer}; } =item setLayer() =for ref Define all values from a layer. =for exemple $hotMap->setLayer('AllMap', $piddleVal); =cut sub setLayer { my $self = shift; my ($dest, $values) = @_; die ("setLayer: layers must be defined. => '$dest'",$/) unless (defined $self->{_knownPoints}{$dest}); $self->{_knownPoints}{$dest} += $values; } =item toString() =for ref Convert the interpolated table to text. The parameter 'floor' can be added to return rounded values. =for exemple print $hotMap->toString('floor'); [ [ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] [ 1 14 14 14 14 13 13 13 13 13 14 14 14 15 1] [ 1 14 15 14 13 13 13 13 13 13 14 14 15 15 1] [ 1 13 14 13 13 12 12 13 13 14 14 15 15 15 1] [ 1 9 10 11 11 12 12 13 13 14 15 15 16 16 1] [ 1 6 7 8 10 11 12 13 14 15 15 16 16 17 1] [ 1 5 5 7 9 11 12 13 14 15 16 17 17 17 1] [ 1 5 6 7 9 11 13 14 16 17 17 18 18 18 1] [ 1 6 7 8 10 12 14 16 17 18 19 19 19 19 1] [ 1 8 8 10 11 14 16 18 19 20 21 21 20 20 1] [ 1 9 10 11 13 16 18 20 21 22 22 22 21 21 1] [ 1 11 12 13 15 17 20 22 23 23 23 23 22 22 1] [ 1 12 13 15 17 19 21 23 24 24 24 24 23 22 1] [ 1 13 15 16 18 20 22 23 24 25 24 24 23 22 1] [ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1] ] =cut sub toString { my $self = shift; my $function = shift; my $tmpPiddle = $self->{_mapPoints}; #$tmpPiddle->where($tmpPiddle > PALETTE_SLICE) -= PALETTE_SLICE; if (defined $function) { return scalar (floor($tmpPiddle)) if ($function eq 'floor'); die "toString: Unknown Function. => '$function'",$/; } return scalar ($tmpPiddle); } =item legend() =for ref Set or Return legend status. When enabled, the legend gradient will be drawn on the image. =cut sub legend { my $self = shift; my ($value) = @_; if (defined $value) { $self->{_legend} = 1; } else { return $self->{_legend}; } } =item crossMark() =for ref Set or Return cross marks status. When enabled, a cross will be drawn on the image where points have been defined. =cut sub crossMark { my $self = shift; my ($mark, $value) = @_; if (defined $mark) { $self->{_crossMark} = 1; $self->{_crossMarkTemp} = 1 if defined $value; } else { return $self->{_crossMark}; } } =for exemple Internal function for base colors table =cut sub _genLut { my $self = shift; my ($lut) = @_; $lut = [ [255, 255, 255], # 0 white [ 0, 0, 0], # 1 black [ 0, 0, 255], # 2 blue [ 0, 255, 0], # 3 green [ 0, 255, 255], # 4 cyan [255, 0, 0], # 5 red [255, 0, 255], # 6 magenta [255, 255, 0], # 7 yellow [153, 204, 0], # 8 Green 1 [128, 128, 0], # 9 Green 2 [128, 0, 128], # 10 purple [255, 255, 153], # 11 light yellow [204, 153, 255], # 12 light purple [ 0, 204, 255], # 13 cool blue [228, 109, 10], # 14 orange [255, 204, 153], # 15 peal [246, 96, 134], # 16 rose1 [ 96, 118, 246], # 17 blue2 [152, 18, 13], # 18 red2 [153, 102, 204], # 19 violet2 [123, 160, 91], # 20 asperge ] unless defined $lut; for (@$lut..PALETTE_SLICE-1) { push (@{$lut}, [100+$_, 100+$_, 100+$_]); } return $lut; } =for item gradient() Set the gradient. Parameter must be an array of RGB array. See Math::Gradient::multi_array_gradient() =cut sub gradient { my $self = shift; my ($params) = @_; my $nbColors = $params->{nbColors}; my $colorsPoints = $params->{colorsPoints}; my $gradientName = $params->{gradientName}; my $minValue = $params->{minValue}; my $maxValue = $params->{maxValue}; my $unit = $params->{unit}; my $visibility = $params->{visibility}; $self->{_gradient}{$gradientName}{colorsPoints} = $colorsPoints; $self->{_gradient}{$gradientName}{nbColors} = $nbColors; $self->{_gradient}{$gradientName}{minValue} = $minValue; $self->{_gradient}{$gradientName}{maxValue} = $maxValue; $self->{_gradient}{$gradientName}{unit} = $unit; $self->{_gradient}{$gradientName}{visibility} = $visibility; } =for comment Internal function for generation LUT =cut sub _genGradient { my $self = shift; my $nextPaletteStart = PALETTE_SLICE; my @gradients = (); foreach my $gradientName (sort keys %{$self->{_gradient}}) { my $nbColors = 1+$self->{_gradient}{$gradientName}{nbColors}; my @grad = multi_array_gradient($nbColors, @{$self->{_gradient}{$gradientName}{colorsPoints}}); push (@gradients, @grad); $self->{_gradient}{$gradientName}{start} = $nextPaletteStart; $nextPaletteStart += $nbColors; } my $lut = byte pdl((@{$self->_genLut}, @gradients)); $self->{_gradient}{colors} = PDL::cat ($lut); } =for item getColor($level) Return the lut color from the specified level =cut sub getColor { my $self = shift; my ($level) = @_; my $lut = $self->{_gradient}{colors}; return '#'. sprintf("%02x",$lut->at(0,$level,0)). sprintf("%02x",$lut->at(1,$level,0)). sprintf("%02x",$lut->at(2,$level,0)); } =for comment Internal function for writing text on the image =cut sub _printText { my $self = shift; my ($im, $textHash) = @_; my $text = $textHash->{text}; my $x = $textHash->{x}; my $y = $textHash->{y}; my $color = $textHash->{color} || 'black'; my $align = $textHash->{align} || 'left'; my $size = $textHash->{size} || $self->{_fontSize}; my $font = $textHash->{font} || $self->{_font}; my $rotate = $textHash->{rotate} || 0; $im->Annotate( font=>$self->{_font}, pointsize=>$size, fill=>$color, text=>$text, align=>$align, x=>$x, y=>$y, rotate=>$rotate, ); } =for comment Internal function for generating legend bar on the image =cut sub _drawLegendBar { my $self = shift; my ($gradientName, $i, $im) = @_; my $repere = 10; my $legendBar = Graphics::HotMap->new( wall => 1, ); $legendBar->{_gradient} = $self->{_gradient}; $legendBar->mapSize({ sizeX => 10, sizeY => 500, }); $legendBar->addLayer({ layerName => '_Legend'.$gradientName, visibility => 1, gradientName => $gradientName }); my $nbGrad = $self->{_gradient}{$gradientName}{nbColors}; #$self->{_legendNbGrad}-1; my $min = $self->{_gradient}{$gradientName}{minValue}; my $max = $self->{_gradient}{$gradientName}{maxValue}; for (0..$nbGrad) { my $x = $legendBar->{_mapSize}{x}-1; my $y = $_/$nbGrad*($legendBar->{_mapSize}{y}-1); my $valeur = $max-(int(($nbGrad-$_)/$nbGrad*($max-$min))); my $unit = $legendBar->{_gradient}{$gradientName}{unit}; $legendBar->addPoint({ layerName => '_Legend'.$gradientName, #x => $_/$nbGrad*($legendBar->{_mapSize}{x}-1), #y => $legendBar->{_mapSize}{y}-1-$repere*$i, x => $x, y => $y, value => $valeur, noScale => 1, unit => $unit, }); $legendBar->addText ( { x => $x+15, y => $y+10, text => int($valeur).$unit, size => 10, align => 'center' } ) if ($nbGrad < 11 || $_%5 == 0); } $legendBar->addZone({ layerName =>'_Legend'.$gradientName, zoneName => '_Legend'.$gradientName, coordonates => [ 1, 1, $legendBar->{_mapSize}{x}-1, $legendBar->{_mapSize}{y}-1, ], noScale => 1 }); $legendBar->_genDegradZone('_Legend'.$gradientName, $legendBar->{_zones}{'_Legend'.$gradientName}{'_Legend'.$gradientName}, 1); my $imag = byte $legendBar->{_mapPoints}; my $tmpName = new File::Temp( TEMPLATE => 'generated-XXXXX', DIR => '/tmp/', SUFFIX => '.png', OPEN => 0); #my $tmpName = tmpnam().'.png'; my $cptLoop = 0; do { eval {$imag->wpic($tmpName, { LUT => $legendBar->{_gradient}{colors} }); }; # $imag->wpic($tmpName, { LUT => $legendBar->{_gradient}{colors} }); ++$cptLoop; } while ($@ && $cptLoop < 10); if ($cptLoop > 2) { print "ARgh ! Function: _saveImage ; nbErr for wpic:$cptLoop\n"; exit; } # read the temporary File in PerlMagick my $status = $im->ReadImage($tmpName); warn $status if $status; #unlink $tmpName; # Flip the image $im->[$i+1]->Flip; $im->[$i+1]->Border(fill=>'black', width=>-1, height=>-1); $im->[$i+1]->Extent( background => 'white', geometry => ($legendBar->{_mapSize}{x}+35).'x'.($legendBar->{_mapSize}{y}+15), gravity => 'West', ); $legendBar->_genText($im->[$i+1]); $im->[$i+1]->Extent( background => 'white', geometry => ($legendBar->{_mapSize}{x}+35).'x'.$self->{_mapSize}{y}, gravity => 'Center', ); $im->[$i+1]->Extent( background => 'white', geometry => ($legendBar->{_mapSize}{x}+35+20).'x'.$self->{_mapSize}{y}, gravity => 'East', ); $im->[$i+1]->Annotate( font=>$self->{_font}, pointsize=>10, fill=>'black', text=>$gradientName, align=>'right', x=>10, y=>35, rotate=>270, ); $self->{_im} = $im->Append(stack=>'false'); } =for comment Internal function for generating legend on the image =cut sub _genLegende { my $self = shift; my ($im) = @_; my $i=0; #print "Printing Gradient Bars",$/; foreach my $gradientName (sort keys %{$self->{_gradient}}) { #print "* $gradientName",$/; next if $gradientName eq 'colors'; next if defined $self->{_gradient}{$gradientName}{visibility} && !$self->{_gradient}{$gradientName}{visibility}; $self->_drawLegendBar($gradientName, $i, $im); $i++; } } =for comment Internal function for generating one mark on the image =cut sub _drawMark { my $self = shift; my ($im, $x, $y, $valeur, $unit) = @_; my $red = '#FF0000'; my $white = '#FFFFFF'; my %cross = ( -2 => { 0 => $red, }, -1 => { 0 => $white, }, 0 => {-2 => $red, -1 => $white, 0 => $white, 1 => $white, 2 => $red, }, 1 => { 0 => $white, }, 2 => { 0 => $red, }, ); foreach my $i (sort keys %cross) { foreach my $j (sort keys %cross) { for (0..2) { next unless defined $cross{$i}{$j}; my $ix = $i * $_ + $x; my $jy = $j * $_ + $y; next unless (0 < $ix && $ix < $self->{_mapSize}{x}-1); next unless (0 < $jy && $jy < $self->{_mapSize}{y}-1); $im->Set("pixel[$ix,$jy]" => $cross{$i}{$j}); } } } $self->addText ( { x => $x, y => $y, text => int($valeur).$unit, size => 10, align => 'center' } ); } =for comment Internal function for generating all marks on the image =cut sub _genCrossMark { my $self = shift; my $im = shift; foreach my $layer (sort keys %{$self->{_knownPoints}}) { my ($d0,$d1) = whichND $self->{_knownPoints}{$layer}; my $nbValues = nelem($d0); for (0..$nbValues-1) { next unless ( defined $self->{_layers}{$layer}{visibility} && $self->{_layers}{$layer}{visibility} ); $self->_drawMark( $im, $d0(($_)), $d1(($_)), $self->{_knownPoints}{$layer}->at($d0(($_)),$d1(($_))), $self->{_gradient}{$self->{_layers}{$layer}{gradientName}}{unit} ); } } } =for comment Internal function for writing timestamp on the image =cut sub _drawTime { my $self = shift; my $im = shift; my ($time, $x, $y) = @{$self->{_horodatage}}; return unless $time; $self->addText ( { x => $x, y => $y, text => strftime ("%d-%m-%Y %H:%M:%S", localtime $time), } ); } =for comment generate text on the image =cut sub _genText { my $self = shift; my ($im) = @_; foreach my $text (@{$self->{_text}}) { $self->_printText($im, $text); } } =for comment generate the image from the interpolated map. =cut sub _genPicture { my $self = shift; my $image = $self->{_im} = new Image::Magick(); # write a temporary image of the piddle my $imag = byte $self->{_mapPoints}; my $tmpName = tmpnam().'.png'; #eval { $self->{_hotMap}->genImage }; #print STDERR "error: _genTemperatureImage: $@" if $@; my $cptLoop = 0; do { eval {$imag->wpic($tmpName, { LUT => $self->{_gradient}{colors} }); }; ++$cptLoop; } while ($@ && $cptLoop < 10); if ($cptLoop > 2) { print "ARgh ! Function: _genPicture; nbErr for wpic:$cptLoop\n"; exit; } # read the temporary File in PerlMagick my $status = $image->ReadImage($tmpName); warn $status if $status; unlink $tmpName; # Flip the image my $im = $image; #my $im = $image->[0]; $im->Flip; # Gen CrossMarks $self->_genCrossMark($im) if $self->{_crossMark}; # Draw time on image $self->_drawTime($im) if $self->{_horodatage}[0]; # Draw texts $self->_genText($im); # Gen legend in piddle $self->_genLegende($im) if $self->{_legend}; } =for comment Really compute the interpolation from known points. =cut sub _pdlDegrad { my $self = shift; my ($input, $output, $sliceColors, $gradientName) = @_; my ($d0,$d1) = whichND $input; my $nbValues = nelem($d0); my $norm = pdl->zeroes($input->dims); $output .= 0; my $t0r2; my $t0r2inv; if ($nbValues > 1) { for (0..$nbValues-1) { my $indice = $_; $t0r2 = $input->rvals({ center=>[$d0($indice), $d1($indice)], squared=>1 } ); $t0r2->where($t0r2==0) .= -1; $t0r2inv = 1/$t0r2; $norm += $t0r2inv; $output += $input($d0($indice), $d1($indice);-)*$t0r2inv; } $output->where($output < 0) .= 0; $output /= $norm; $output += $input; } elsif ($nbValues == 1) { $output->where($output==0) .= $input->at($d0->at(0),$d1->at(0)); } else { # do not slice if there is no values return; } if (defined $sliceColors && $sliceColors) { my $minValue = $self->{_gradient}{$gradientName}{minValue}; my $maxValue = $self->{_gradient}{$gradientName}{maxValue}; my $ratio = $self->{_gradient}{$gradientName}{nbColors}/(1+$maxValue-$minValue); $output *= $ratio; $output += $self->{_gradient}{$gradientName}{start}-$minValue*$ratio; } } =for comment Fetch zones, get a slice from coordonates, then generate the interpolation. If border is defined, place the points too. =cut sub _genDegradZone { my $self = shift; my ($layerName, $zoneHash) = @_; my ($sX, $sY, $eX, $eY) = @{$zoneHash->{coordonates}}; my $sliceColors = $self->{_layers}{$layerName}{sliceColors}; my $gradientName= $self->{_layers}{$layerName}{gradientName}; # define slices from zones my $mapPointsSlice = $self->{_mapPoints}->($sX:$eX,$sY:$eY); my $knownPointsSlice = $self->{_knownPoints}{$layerName}->($sX:$eX,$sY:$eY); # don't show invisible layers if no points inside return if ($self->{_layers}{$layerName}{maskIfNoValue} && !$knownPointsSlice->max); # generate gradient interpolation $self->_pdlDegrad($knownPointsSlice, $mapPointsSlice, $sliceColors, $gradientName); # draw walls if (defined $zoneHash->{border}) { $mapPointsSlice->(0) .= $zoneHash->{border}; $mapPointsSlice->(-1) .= $zoneHash->{border}; $mapPointsSlice->(:,0) .= $zoneHash->{border}; $mapPointsSlice->(:,-1) .= $zoneHash->{border}; } #$mapPointsSlice = byte($mapPointsSlice); } =for comment Fetch zone =cut sub _genDegrad { my $self = shift; $self->_genGradient; foreach my $layerName (sort keys %{$self->{_zones}}) { foreach my $zoneName (sort keys %{$self->{_zones}{$layerName}}) { $self->_genDegradZone($layerName, $self->{_zones}{$layerName}{$zoneName}); } } } =item genImage() =for ref Calculate the interpolation of all Zones. =cut sub genImage { my $self = shift; $self->{_mapPoints} = PDL->zeroes($self->{_mapSize}{x}, $self->{_mapSize}{y}); $self->_genDegrad; $self->_genPicture; } =for comment This function will write image to disk. =cut sub _saveImg { my $self = shift; my ($outfile, $im) = @_; print $im->Write(filename=>$outfile); #, compression=>'JPEG', type => 'Palette'); } =item genImagePng() =for ref Write a PNG image from the interpolated table. =for exemple $hotMap->genImagePng('); =cut sub genImagePng { my $self = shift; my $fileName = shift || $self->{_outfilePng} || die "No output PNG specified"; $self->_saveImg($fileName,$self->{_im}); return { width => $self->{_im}->Get('width'), height => $self->{_im}->Get('height'), filesize => $self->{_im}->Get('filesize'), mime => $self->{_im}->Get('mime'), image => $self->{_im}, }; } =item genImageGif() =for ref Add a GIF image to the annimation from the interpolated table. =for exemple $hotMap->genImageGif('); =cut sub genImageGif { my $self = shift; my $fileName = shift || $self->{_outfileGif} || die "No output GIF specified"; my $image = shift; my $im = $self->{_im}; unless (defined $image) { $image = new Image::Magick(size => "$self->{_mapSize}{x}x$self->{_mapSize}{y}"); $image->Read($fileName); } $image->Set(magick=>'GIF', loop=> 100); $im->Set(magick=>'GIF', delay=>100); push (@$image, $im); $self->_saveImg($fileName, $image); return { width => $image->Get('width'), height => $image->Get('height'), filesize => $image->Get('filesize'), mime => $image->Get('mime'), image => $image, }; } =back =head1 SEE ALSO PDL Math::Gradient =head1 AUTHOR Mathieu Alorent (cpan@kumy.net) =cut 1;