From 126bb8cb6b93240bb4d3a2b816b74c286c3d422b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Sun, 6 Jul 2014 15:20:38 +0200 Subject: Imported Upstream version 1.7.0 --- lib/gcstar/GCStats.pm | 464 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 lib/gcstar/GCStats.pm (limited to 'lib/gcstar/GCStats.pm') diff --git a/lib/gcstar/GCStats.pm b/lib/gcstar/GCStats.pm new file mode 100644 index 0000000..439c6d1 --- /dev/null +++ b/lib/gcstar/GCStats.pm @@ -0,0 +1,464 @@ +package GCStats; + +################################################### +# +# Copyright 2005-2010 Christian Jodar +# +# This file is part of GCstar. +# +# GCstar 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 2 of the License, or +# (at your option) any later version. +# +# GCstar 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 GCstar; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +# +################################################### +use utf8; +use Gtk2; + +use strict; + +our $statisticsActivated; +BEGIN { + $statisticsActivated = 1; + # Trick for depencies checker. Don't remove these commented lines. + #eval 'use GD'; + #eval 'use GD::Graph::bars'; + #eval 'use GD::Graph::pie'; + #eval 'use GD::Graph::area'; + #eval 'use GD::Text'; + #eval 'use Date::Calc'; + foreach my $module (qw/GD GD::Graph::bars GD::Graph::pie GD::Graph::area GD::Text Date::Calc/) + { + eval "use $module"; + if ($@) + { + $statisticsActivated = 0; + last; + } + } +} + +{ + package GCStatsImageGenerator; + + sub new + { + my ($proto, $parent) = @_; + my $class = ref($proto) || $proto; + my $self = {parent => $parent}; + bless ($self, $class); + + GD::Text->font_path($ENV{GCS_SHARE_DIR}.'/fonts'); + + return $self; + } + + sub setData + { + my ($self, $items, $sort, $type, $useNumbers) = @_; + my %stats; + foreach my $item(@$items) + { + if (ref($item) eq 'ARRAY') + { + $stats{$_}++ foreach (@$item); + } + else + { + $stats{$item}++; + } + } + my (@val1, @val2); + my @sortedKeys; + if ($sort) + { + @sortedKeys = sort {$stats{$a} <=> $stats{$b}} keys %stats; + } + else + { + # TODO This sort should be more complicated, depending on the type of field + @sortedKeys = sort {$a cmp $b} keys %stats; + } + my $val; + foreach my $key(@sortedKeys) + { + $val = $key; + $val .= ' ('.$stats{$key}.')' if $useNumbers; + push @val1, $val; + push @val2, $stats{$key}; + } + $self->{data} = [\@val1, \@val2]; + } + + sub generate + { + my ($self, %options) = @_; + my $graph; + + # Plot the graph twice as large as desired, so we can resample it down later, and eliminate + # the jagged lines caused by GD::Graph's lack of anti-aliasing + my $scaledWidth = $options{width} * 2; + my $scaledHeight = $options{height} * 2; + my $scaledFontSize = $options{fontSize} * 2; + + if ($options{type} eq 'bars') + { + $graph = GD::Graph::bars->new($scaledWidth, $scaledHeight); + $graph->set( + x_labels_vertical => 1, + ); + $graph->set_x_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_x_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_legend_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_values_font('LiberationSans-Regular.ttf', $scaledFontSize); + } + elsif ($options{type} eq 'area') + { + $graph = GD::Graph::area->new($scaledWidth, $scaledHeight); + $graph->set( + x_labels_vertical => 1, + ); + $graph->set_x_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_x_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_legend_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_values_font('LiberationSans-Regular.ttf', $scaledFontSize); + } + elsif ($options{type} eq 'history') + { + $graph = GD::Graph::area->new($scaledWidth, $scaledHeight); + $graph->set_x_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_x_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_y_axis_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_legend_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_values_font('LiberationSans-Regular.ttf', $scaledFontSize); + + # Modify data to accumulate + if ($options{accumulate}) + { + my $prev = 0; + foreach (@{$self->{data}->[1]}) + { + $_ += $prev; + $prev = $_; + } + } + # Transform dates into numbers of days + # Reference is the 1st one as they are ordered + # Or the 2nd one if there are items without value + my @refDate; + if ($self->{data}->[0]->[0]) + { + @refDate = split m|/|, $self->{data}->[0]->[0]; + } + else + { + @refDate = split m|/|, $self->{data}->[0]->[1]; + # We consider items without value as being the day before + @refDate = Date::Calc::Add_Delta_Days(@refDate, -1); + $self->{data}->[0]->[0] = sprintf('%d/%02d/%02d', @refDate); + } + my $prev = -1; + my @newDates; + my $dateFormat = $self->{parent}->{options}->dateFormat; + foreach my $date(@{$self->{data}->[0]}) + { + push @newDates, GCUtils::timeToStr(GCPreProcess::restoreDate($date), $dateFormat); + if (!$date) + { + $prev = 0; + next; + } + my @cmpDate = split m|/|, $date; + my $diff = Date::Calc::Delta_Days(@refDate, @cmpDate); + if ($diff > $prev + 1) + { + my @filler; + $#filler = $diff - $prev - 2; + if (!$options{accumulate}) + { + @filler = map {''} @filler; + } + splice (@newDates, $prev + 1, 0, @filler); + splice (@{$self->{data}->[1]}, $prev + 1, 0, @filler); + } + $prev = $diff; + } + $self->{data}->[0] = \@newDates; + if (!$options{showAllDates}) + { + for my $idx(0..$#newDates) + { + my @date = Date::Calc::Add_Delta_Days(@refDate, $idx); + my $value; + if ($date[2] == 1) + { + my $dateStr = sprintf('%d/%02d/%02d', @date); + $value = GCUtils::timeToStr(GCPreProcess::restoreDate($dateStr), $dateFormat); + } + else + { + if ($self->{data}->[1]->[$idx]) + { + $value = ''; + } + } + $self->{data}->[0]->[$idx] = $value; + } + } + $graph->set( + #x_label_skip => 5, + x_labels_vertical => 1, + show_values => 1, + ); + } + else + { + $graph = GD::Graph::pie->new($scaledWidth, $scaledHeight); + $graph->set_value_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set_label_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set('3d' => ($options{type} eq '3dpie')); + } + $graph->set_title_font('LiberationSans-Regular.ttf', $scaledFontSize); + $graph->set( + title => $options{title}, + show_values => $options{showValues}, + transparent => 0, + bgclr => '#ffffff', + dclrs => ['#ffdf33', '#1c86ee', '#cdad00', '#6c7b8b', '#ffb618'], + cycle_clrs => 1, + t_margin => 20, + b_margin => 20, + l_margin => 20, + r_margin => 20, + text_space => 20, + ); + + my $gd = $graph->plot($self->{data}); + + # Now, resample the graph down to the desired size, effectively anti-aliasing the sharp edges + my $aaImage = GD::Image->new($options{width}, $options{height}, 1); + $aaImage->copyResampled($gd, 0, 0, 0, 0, $options{width}, $options{height}, $scaledWidth, $scaledHeight); + + return $aaImage->png; + } +} + +{ + package GCStatsDialog; + use base "Gtk2::Dialog"; + + use GCUtils; + use GCDialogs; + + sub new + { + my ($proto, $parent) = @_; + my $class = ref($proto) || $proto; + my $self = $class->SUPER::new($parent->{lang}->{MenuStatistics}, + $parent, + [qw/destroy-with-parent/], + $parent->{lang}->{StatsGenerate} => 'apply', + 'gtk-save-as' => 'ok', + 'gtk-close' => 'close' + ); + bless($self, $class); + + $self->{parent} = $parent; + $self->{generator} = new GCStatsImageGenerator($parent); + + $self->{choices} = [ + {value => 'bars', displayed => $parent->{lang}->{StatsBars}}, + {value => 'pie', displayed => $parent->{lang}->{StatsPie}}, + {value => '3dpie', displayed => $parent->{lang}->{Stats3DPie}}, + {value => 'area', displayed => $parent->{lang}->{StatsArea}}, + {value => 'history', displayed => $parent->{lang}->{StatsHistory}}, + ]; + $self->{typeOption} = new GCMenuList($self->{choices}, 4); + $self->{typeOption}->setValue('3dpie'); + my $typeLabel = new GCLabel($parent->{lang}->{StatsKindOfGraph}); + $self->{fieldLabel} = new GCLabel($parent->{lang}->{StatsFieldToUse}); + $self->{fieldOption} = new GCFieldSelector(0, undef, 1); + $self->{dateLabel} = new GCLabel($parent->{lang}->{StatsFieldDate}); + $self->{dateOption} = new GCFieldSelector(0, undef, 1, 0, 0, 'date'); + $self->{sortByNumberOption} = new GCCheckBox($parent->{lang}->{StatsSortByNumber}); + $self->{sortByNumberOption}->setValue(0); + $self->{accumulateOption} = new GCCheckBox($parent->{lang}->{StatsAccumulate}); + $self->{accumulateOption}->setValue(1); + $self->{useNumberOption} = new GCCheckBox($parent->{lang}->{StatsDisplayNumber}); + $self->{useNumberOption}->setValue(0); + + my $widthLabel = new GCLabel($parent->{lang}->{StatsWidth}); + $self->{widthOption} = new GCCheckedText('0-9'); + $self->{widthOption}->setValue(600); + my $heightLabel = new GCLabel($parent->{lang}->{StatsHeight}); + $self->{heightOption} = new GCCheckedText('0-9'); + $self->{heightOption}->setValue(600); + my $fontLabel = new GCLabel($parent->{lang}->{StatsFontSize}); + $self->{fontOption} = new GCNumeric(12, 1, 100, 1); + + $self->{typeOption}->signal_connect('changed' => sub { + $self->checkVisible; + }); + + my $table = new Gtk2::Table(3, 11, 0); + $table->set_row_spacings($GCUtils::margin); + $table->set_col_spacings($GCUtils::margin); + $table->set_border_width($GCUtils::halfMargin); + + $table->attach($typeLabel, 0, 1, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($self->{typeOption}, 1, 2, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($widthLabel, 3, 4, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($self->{widthOption}, 4, 5, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($heightLabel, 6, 7, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($self->{heightOption}, 7, 8, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($fontLabel, 9, 10, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($self->{fontOption}, 10, 11, 0, 1, 'fill', 'fill', 0, 0); + $table->attach($self->{fieldLabel}, 0, 1, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{fieldOption}, 1, 2, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{dateLabel}, 0, 1, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{dateOption}, 1, 2, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{sortByNumberOption}, 3, 5, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{accumulateOption}, 3, 5, 1, 2, 'fill', 'fill', 0, 0); + $table->attach($self->{useNumberOption}, 6, 8, 1, 2, 'fill', 'fill', 0, 0); + + $self->{image} = Gtk2::Image->new; + + $self->vbox->pack_start($self->{image},1,1,0); + $self->vbox->pack_start($table,0,0,0); + + ($self->action_area->get_children)[1]->set_sensitive(0); + + return $self; + } + + sub checkVisible + { + my $self = shift; + my $val = $self->{typeOption}->getValue; + if ($val eq 'history') + { + $self->{fieldLabel}->hide; + $self->{fieldOption}->hide; + $self->{dateLabel}->show_all; + $self->{dateOption}->show_all; + $self->{sortByNumberOption}->hide; + $self->{accumulateOption}->show_all; + } + else + { + $self->{fieldLabel}->show_all; + $self->{fieldOption}->show_all; + $self->{dateLabel}->hide; + $self->{dateOption}->hide; + $self->{sortByNumberOption}->show_all; + $self->{accumulateOption}->hide; + } + } + + sub setData + { + my ($self, $model, $data, $title) = @_; + $self->{model} = $model; + $self->{data} = $data; + $self->{title} = $title; + $self->{fieldOption}->setModel($model); + $self->{fieldOption}->setValue('genre'); + $self->{dateOption}->setModel($model); + $self->{dateOption}->setValue('added'); + } + + sub show + { + my ($self) = @_; + + #$self->generatePicture; + $self->SUPER::show(); + $self->show_all; + $self->checkVisible; + $self->set_position('center'); + my $response = 'cancel'; + while (!(($response eq 'close') || ($response eq 'delete-event'))) + { + $response = $self->run; + $self->generatePicture if $response eq 'apply'; + $self->save if $response eq 'ok'; + } + $self->hide; + } + + sub generatePicture + { + my $self = shift; + + my $graphType = $self->{typeOption}->getValue; + my $sortField = ($graphType eq 'history') + ? $self->{dateOption}->getValue + : $self->{fieldOption}->getValue; + my $sortByNumber = $self->{sortByNumberOption}->getValue; + my $accumulate = $self->{accumulateOption}->getValue; + my $useNumber = ($graphType =~ /pie/) && $self->{useNumberOption}->getValue; + my $showValues = ($graphType !~ /pie/) && $self->{useNumberOption}->getValue; + my $width = $self->{widthOption}->getValue; + my $height = $self->{heightOption}->getValue; + my $fontSize = $self->{fontOption}->getValue; + + my $type = $self->{parent}->{model}->{fieldsInfo}->{$sortField}->{type}; + my @valuesList; + if ($type eq 'date') + { + @valuesList = map {GCPreProcess::reverseDate($_->{$sortField})} @{$self->{data}}; + } + else + { + @valuesList = map {$self->{parent}->transformValue($_->{$sortField}, $sortField, 1)} @{$self->{data}}; + } + $self->{generator}->setData(\@valuesList, $sortByNumber, $type, $useNumber); + #$self->{generator}->setData($self->{data}, $sortField); + my $png = $self->{generator}->generate(type => $graphType, + showValues => $showValues, + accumulate => $accumulate, + title => $self->{title}, + width => $width, + height => $height, + fontSize => $fontSize, + showAllDates => 0); + + my $loader = Gtk2::Gdk::PixbufLoader->new; + $loader->write($png); + $loader->close; + $self->{pixbuf} = $loader->get_pixbuf; + $self->{image}->set_from_pixbuf($self->{pixbuf}); + ($self->action_area->get_children)[1]->set_sensitive(1); + } + + sub save + { + my $self = shift; + my $fileDialog = new GCFileChooserDialog($self->{parent}->{lang}->{StatsSave}, $self, 'save', 1); + $fileDialog->set_pattern_filter(['PNG (.png)', '*.png']); + $fileDialog->set_filename($self->{filename}); + my $response = $fileDialog->run; + if ($response eq 'ok') + { + $self->{filename} = $fileDialog->get_filename; + $self->{pixbuf}->save($self->{filename}, 'png'); + } + $fileDialog->destroy; + } +} + +1; -- cgit v1.2.3