package Lire::Report::GroupInfo;
use strict;
use Carp;
use POSIX qw/ ceil /;
use Lire::DataTypes qw/ check_xml_name /;
use Lire::Report::ColumnInfo;
use Lire::Utils qw/ check_param check_object_param /;
=pod
=head1 NAME
Lire::Report::GroupInfo - Object that groups columns created by the same aggregator. .
=head1 SYNOPSIS
my $info = $subreport->group_info( 0 );
print "Group's name: ", $info->name(), "\n";
print "Group's columns starts at: ", $info->index(), "\n";
=head1 DESCRIPTION
The Lire::Report::GroupInfo object holds the ColumnInfo objects that
were created by the same aggregator.
=head1 CONSTRUCTOR
=head2 new( $name )
You create a new Lire::Report::GroupInfo object by calling the
create_group_info() method on one Lire::Report::TableInfo object or
another Lire::Report::GroupInfo object.
=cut
sub new {
my ( $class, $parent, $name ) = @_;
check_param( $name, 'name', \&check_xml_name,
"name isn't a valid XML name" );
check_object_param( $parent, 'parent', 'Lire::Report::GroupInfo' );
return bless( { 'name' => $name,
'children' => [],
'parent' => $parent },
$class );
}
=pod
=head1 OBJECT METHODS
=pod
=head2 name()
Returns the name of this group. This name can be used to find the
aggregator (from the report specification) that is responsible for the
data held in this group.
=cut
sub name {
return $_[0]{'name'};
}
=pod
=head2 row_idx()
Returns the row index in which this group's categorical column's
labels should be displayed in the table header.
=cut
sub row_idx {
$_[0]{'row_idx'} = $_[1] if @_ == 2;
return $_[0]{'row_idx'};
}
=pod
=head2 max_row_idx()
=cut
sub max_row_idx {
my $self = $_[0];
my $max = $self->{'row_idx'};
foreach my $group ( $self->group_children() ) {
my $idx = $group->max_row_idx();
$max = $idx if ( $idx > $max );
}
return $max;
}
=pod
=head2 parent_group_info()
Returns the GroupInfo which contains this GroupInfo. This will be
undef for the top-level TableInfo object.
=cut
sub parent_group_info {
return $_[0]{'parent'};
}
=pod
=head2 index()
Returns the column's index in the table at which the columns of this
group starts. This will be undef until the group info object is added
to a Lire::Subreport or another Lire::Group object.
=cut
sub index {
my ( $self, $index ) = @_;
if ( @_ == 2 ) {
croak ( "can't modify starting column index once children were added" )
if $self->children;
$self->{'index'} = $index;
}
return $self->{'index'};
}
=pod
=head2 children()
Returns as an array, the Lire::Report::ColumnInfo and
Lire::Report::GroupInfo objects contained in this group.
=cut
sub children {
return @{$_[0]{'children'}};
}
=pod
=head2 info_by_index( $idx )
Returns the ColumnInfo or GroupInfo that is at index $idx among this
GroupInfo children.
=cut
sub info_by_index {
my ( $self, $idx ) = @_;
croak( "info index out of bounds" )
if $idx > $#{$self->{'children'}};
return $self->{'children'}[$idx];
}
=pod
=head2 group_children
Returns as an array, only the Lire::Report::GroupInfo contained in this
GroupInfo.
=cut
sub group_children {
return grep { $_->isa( 'Lire::Report::GroupInfo' ) } $_[0]->children;
}
=pod
=head2 column_children
Returns as an array, only the Lire::Report::ColumnInfo contained in this
GroupInfo.
=cut
sub column_children {
return grep { $_->isa( 'Lire::Report::ColumnInfo' ) } $_[0]->children();
}
=pod
=head2 categorical_names()
Returns the categorical columns' names as an array ref.
=cut
sub categorical_names {
my $self = $_[0];
my @names = ();
foreach my $col ( $self->column_children() ) {
push @names, $col->name()
if $col->class() eq 'categorical';
}
return \@names;
}
=pod
=head2 create_column_info( $name, $class, $type, [$label]])
Creates a new Lire::Report::ColumnInfo object as a child of this
GroupInfo. The column info attributes are initialized based on $name,
$class, $type and $label.
This will set the index of the ColumnInfo object.
Returns the created ColumnInfo object.
=cut
sub create_column_info {
my ( $self, $name, $class, $type, $label ) = @_;
my $info = new Lire::Report::ColumnInfo( $self, $name, $class,
$type, $label );
my $index = $self->next_column_index();
$info->index( $index );
push @{$self->{'children'}}, $info;
return $info;
}
=pod
=head2 create_group_info( $name )
Create a Lire::Report::GroupInfo which as a child of this group. This
will also set the starting column index on the GroupInfo object. The
$name parameter should contain the name of the report specification
aggregator that is responsible for the content of this group.
Returns the created GroupInfo object.
=cut
sub create_group_info {
my ( $self, $name ) = @_;
check_param( $name, 'name', \&check_xml_name,
"name isn't a valid XML name" );
my $info = new Lire::Report::GroupInfo( $self, $name );
my $index = $self->next_column_index();
$info->index( $index );
$info->row_idx( $self->row_idx + 1 );
push @{$self->{'children'}}, $info;
return $info;
}
#------------------------------------------------------------------------
# Methods next_column_index()
#
# Returns the next column's index that should be used when a ColumnInfo or
# GroupInfo object is added.
sub next_column_index {
my $self = $_[0];
croak ( "next_column_index: starting column index wasn't set" )
unless defined $self->{'index'};
return $self->index()
unless ( @{$self->{'children'}} );
my $last = $self->{'children'}[$#{$self->{'children'}}];
return ( UNIVERSAL::isa( $last, "Lire::Report::GroupInfo" )
? $last->next_column_index()
: $last->index() + 1 );
}
=pod
=head2 column_info_by_name( $name )
Returns the Lire::Report::ColumnInfo object that has the name $name. This
method searches in the current group as well as in its subgroups.
Returns undef if the column isn't found.
=cut
sub column_info_by_name {
my ( $self, $name ) = @_;
# Look first into our immediate children
my ( $col ) = grep { $_->name() eq $name } $self->column_children();
return $col if $col;
# Look into subgroups
foreach my $g ( $self->group_children() ) {
my $c = $g->column_info_by_name( $name );
return $c if $c;
}
return;
}
=pod
=head2 column_info_by_col_start( $col_idx )
Returns the Lire::Report::ColumnInfo object that starts in column index
$col_idx. This method searches in the current group as well as in its
subgroups.
Returns undef if the column isn't found in the current group.
=cut
sub column_info_by_col_start {
my ( $self, $idx ) = @_;
# Look first into our immediate children
my ( $col ) = grep { $_->col_start == $idx } $self->column_children();
return $col if $col;
# Look into subgroups
foreach my $g ( $self->group_children() ) {
$col = $g->column_info_by_col_start( $idx );
return $col if $col;
}
return;
}
=pod
=head2 group_info( $name )
Returns the Lire::Report::GroupInfo object that has the name $name.
This will be undef if it cannot be found in this group info.
=cut
sub group_info {
my ( $self, $name ) = @_;
# Look first into our immediate children
my ( $group ) = grep { $_->name() eq $name } $self->group_children();
# Look into subgroups
foreach my $g ( $self->group_children() ) {
$group = $g->group_info( $name );
return $group if $group;
}
return;
}
# ------------------------------------------------------------------------
# Method compute_group_layout( [$col_start])
#
# Calculates the col-start, col-end attributes of each of this
# group column-info.
#
# $col_start is the column at which the group should start assigning.
# Defaults to 0.
#
# Returns the column number that should be assigned next.
#
# The algorithm for the table layout is to assign all the categorical
# columns first. Rationale being this is that only those columns use
# spanning to show the grouping indentation. To achieve this, we also
# process the categorical columns of the first subgroup recursively.
#
# After that, the numerical columns (and recursively the first
# subgroup's numerical columns are assigned and the other groups are
# processed in order as if they were independant subtables (possibly
# also involving grouping
#
# For example, this XML report :
#
# Cat1
# Cat2
# Num1
#
#
# G1Cat1
# G1Num1
#
#
#
#
# G2Cat1
#
#
# G3Cat1
# G3Cat2
# G3num1
#
#
#
#
#
#
# would be rendered as :
#
# +-+--------+------+-----+-------+--+--------+------+---------+
# |Cat1 |Cat2 | Num1| | | |
# +-+--------+------+ + +--+--------+------+ |
# | |G1Cat1 | | G1Num1|G2Cat1 | |
# +-+--------+------+ + +--+--------+------+ |
# | | | | |G3Cat1 |G3Cat2| G3Num1|
# +-+--------+------+-----+-------+--+--------+------+---------+
sub compute_group_layout {
my ( $self, $col_start ) = @_;
$col_start = 0
unless defined $col_start;
# -1 because col_start is a 0-based index
my $cat_col_end = $col_start + $self->count_categorical_columns - 1;
# Assign the categorical columns
$self->assign_categorical_columns( $col_start, $cat_col_end );
# Process the other columns
$self->assign_numerical_columns( $cat_col_end + 1 );
return;
}
# ------------------------------------------------------------------------
# Method assign_categorical_columns( $col_start, $cat_col_end )
#
# Calculates the col-start, col-end attributes of each for this group's
# categorical columns.
#
# $col_start is the column at which the group should start assigning
# $cat_col_end is the last column which can be used for categorical data
#
sub assign_categorical_columns {
my ( $self, $col_start, $cat_col_end ) = @_;
# The categorical columns in this group to process
my @cat_columns = grep { $_->class() eq 'categorical' }
$self->column_children();
# The number of available categorical columns not assigned
my $cat_column_avail = $cat_col_end - $col_start + 1;
die "ASSERTION FAILED: this group has ", scalar @cat_columns,
" categorical columns but only $cat_column_avail are available"
if $cat_column_avail < @cat_columns;
my $col_no = $col_start;
while ( my $col = shift @cat_columns ) {
# + 1 since $col isn't in @cat_columns
my $span = ceil( $cat_column_avail / (@cat_columns + 1) );
$col->col_start( $col_no );
die "ASSERTION FAILED: span outside of the columns assigned to categorical data"
if $col_no + $span -1 > $cat_col_end;
$col->col_end( $col_no + $span - 1 );
$col_no += $span;
$cat_column_avail -= $span;
}
# Assign the categorical columns of the first group
my ($first_group) = $self->group_children();
$first_group->assign_categorical_columns( $col_start + 1, $cat_col_end )
if defined $first_group;
return;
}
# ------------------------------------------------------------------------
# Method count_columns
#
# Returns the number of columns in this group and its subgroups
sub count_columns {
my $self = $_[0];
my $count = $self->column_children();
foreach my $group ( $self->group_children() ) {
$count += $group->count_columns();
}
return $count;
}
# ------------------------------------------------------------------------
# Method count_categorical_columns
#
# Returns the number of categorical columns present in this
# group. This includes the categorical columns of the first subgroups.
#
sub count_categorical_columns {
my ($self) = @_;
my $count = grep { $_->class() eq 'categorical' } $self->column_children();
my @groups = $self->group_children();
$count += $groups[0]->count_categorical_columns()
if @groups;
return $count;
}
# ------------------------------------------------------------------------
# Method assign_numerical_columns( $col_start )
#
# Assigns the numerical columns of this group
#
# $col_start is the column number at which the assignment should
# start
#
# Returns the next column number to assign.
sub assign_numerical_columns {
my ( $self, $col_start ) = @_;
my $col_no = $col_start;
my $group_processed = 0;
foreach my $child ( $self->children() ) {
if ( $child->isa( 'Lire::Report::ColumnInfo' ) ) {
# Already assigned
next if $child->class() eq 'categorical';
# Numerical column are always 1 in width
$child->col_start( $col_no );
$child->col_end( $col_no );
$col_no++;
} else {
# This child is a GroupInfo
if ( $group_processed ) {
# If its not the first one, we have to assign both its
# categorical and numerical columns.
$col_no = $child->compute_group_layout( $col_no );
} else {
# If this is the first GroupInfo, we assign its numerical
# colum.
$col_no = $child->assign_numerical_columns( $col_no );
$group_processed++;
}
}
}
return $col_no;
}
# ------------------------------------------------------------------------
# Method assign_columns_width()
#
# Sets the width of each column based on columns which only uses one
# cell. The width is set to the max-chars value. If the column spans
# more than one cell, its width is set to 2, since this column is used
# as indentation for grouping purpose (consult the example in
# compute_group_layout).
sub assign_columns_width {
my $self = $_[0];
foreach my $col ( $self->column_children() ) {
$col->col_width( $col->col_start() == $col->col_end()
? $col->max_chars()
: 2 );
}
foreach my $group ( $self->group_children() ) {
$group->assign_columns_width();
}
return;
}
# ------------------------------------------------------------------------
# Method correct_spanning_column_width( $table_info)
#
# Make sure that the sum of the width spanned by spanning columns is
# enough to hold their value.
#
# $table_info is a reference to the TableInfo object which contains the
# all columns
sub correct_spanning_column_width {
my ( $self, $table_info ) = @_;
foreach my $col ( $self->column_children() ) {
my $start = $col->col_start();
my $end = $col->col_end();
next unless $start != $end;
# Check the width of the span
my $width = 0;
foreach ( my $i = $start; $i <= $end; $i++ ) {
$width += $table_info->column_info_by_col_start( $i )->col_width();
}
next if $col->max_chars <= $width;
# This column is bigger than the span, increase proportionally
# the columns width, except the first one which is used
# for indentation.
# (We don't need to subtract one, since we need a column count
# and not a column index)
my $extra = ceil( ($col->max_chars - $width) / ( $end - $start) );
foreach ( my $i = $start + 1; $i <= $end; $i++ ) {
my $col = $table_info->column_info_by_col_start( $i );
$col->col_width( $col->col_width + $extra );
}
}
foreach my $group ( $self->group_children() ) {
$group->correct_spanning_column_width( $table_info );
}
return;
}
#-----------------------------------------------------------------------
# Methods write_report( $fh, $index )
sub write_report {
my ( $self, $fh, $indent ) = @_;
$fh ||= *STDOUT;
my $pfx = ' ' x $indent;
print $fh $pfx, qq{\n};
foreach my $i ( $self->children() ) {
$i->write_report( $fh, $indent + 1 );
}
print $fh $pfx, "\n";
}
#------------------------------------------------------------------------
# Method delete()
#
# Remove circular references
sub delete {
my $self = $_[0];
foreach my $c ( $self->children() ) {
$c->delete();
}
%$self = ();
return;
}
# keep perl happy
1;
__END__
=pod
=head1 SEE ALSO
Lire::Report::Subreport(3pm) Lire::Report::ColumnInfo(3pm)
=head1 VERSION
$Id: GroupInfo.pm,v 1.22 2006/07/23 13:16:31 vanbaal Exp $
=head1 AUTHOR
Francis J. Lacoste
=head1 COPYRIGHT
Copyright (C) 2002 Stichting LogReport Foundation LogReport@LogReport.org
This file is part of Lire.
Lire 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.
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 (see COPYING); if not, check with
http://www.gnu.org/copyleft/gpl.html.
=cut