summaryrefslogtreecommitdiff
path: root/lib/gcstar/GCStats.pm
diff options
context:
space:
mode:
authorJörg Frings-Fürst <jff@merkur>2014-07-06 15:20:38 +0200
committerJörg Frings-Fürst <jff@merkur>2014-07-06 15:20:38 +0200
commit126bb8cb6b93240bb4d3a2b816b74c286c3d422b (patch)
treee66e1dfe77d53a52539489765c88d23e4423ae27 /lib/gcstar/GCStats.pm
Imported Upstream version 1.7.0upstream/1.7.0
Diffstat (limited to 'lib/gcstar/GCStats.pm')
-rw-r--r--lib/gcstar/GCStats.pm464
1 files changed, 464 insertions, 0 deletions
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;