File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.22: download - view: text, annotated - select for diffs
Mon Dec 15 00:52:40 2014 UTC (9 years, 4 months ago) by raeburn
Branches: MAIN
CVS tags: version_2_12_X, version_2_11_X, version_2_11_4_uiuc, version_2_11_4_msu, version_2_11_4, version_2_11_3_uiuc, version_2_11_3_msu, version_2_11_3, version_2_11_2_uiuc, version_2_11_2_msu, version_2_11_2_educog, version_2_11_2, version_2_11_1, HEAD
- Coding style: keys()

# The LearningOnline Network with CAPA
#  Generating TeX tables.
#
# $Id: lontable.pm,v 1.22 2014/12/15 00:52:40 raeburn Exp $
# 
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA 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.
#
# LON-CAPA 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 LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
## Copyright for TtHfunc and TtMfunc by Ian Hutchinson. 
# TtHfunc and TtMfunc (the "Code") may be compiled and linked into 
# binary executable programs or libraries distributed by the 
# Michigan State University (the "Licensee"), but any binaries so 
# distributed are hereby licensed only for use in the context
# of a program or computational system for which the Licensee is the 
# primary author or distributor, and which performs substantial 
# additional tasks beyond the translation of (La)TeX into HTML.
# The C source of the Code may not be distributed by the Licensee
# to any other parties under any circumstances.
#

# This module is a support packkage that helps londefdef generate
# LaTeX tables using the Apache::lonlatextable package.  A prerequisite is that
# the print generator must have added the following to the LaTeX 
#
#  \usepackage{xtab}
#  \usepackage{booktabs}
#  \usepackage{array}
#  \usepackage{colortbl}
#  \usepackage{xcolor}
#
#  These packages are installed in the packaged LaTeX distributions we know of as of
#  11/24/2008
#



package Apache::lontable;
use strict;
use Apache::lonlatextable;
use Apache::lonnet;		# for trace logging.

my $tracing = 0;		# Set to 1 to enable log tracing. 2 for local sub tracing.

=pod

=head1  lontable Table generation assistant for the LaTeX target

This module contains support software for generating tables in LaTeX output mode 
In this implementation, we use the Apache::lonlatextable package to do the actual final formatting.
Each table creates a new object.  Table objects can have global properties configured.
The main operations on a table object are:

=over 3

=item start_row  

Opens a new table row.

=item end_row

Closes a table row.

=item configure_row

Modifies a configuration item in the currently open row.

=item generate

Returns the generated table string.

=item configure

Configures a table's global configuration.

=item add_cell

Add and configure a cell to the current row.6

=back

=cut

=pod

=head2 new - create a new object.

Create a new table object.  Any of the raw table configuration items can be
modified by this.  These configuration items include:

  my $table = lontable::new(\%config_hash)

=over 3


=item alignment

Table alignment.  Some table styles support this but not all.

=item tableborder

If true, a border is drawn around the table.

=item cellborder

If true, borders are drawn around the cells inside a table.

=item caption

The table caption text.

=item theme

The theme of the table to use.  Defaults to Zurich.  Themes we know about are:
NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris.  Other themes can be added
to the Apache::lonlatextable package, and they will become supported automatically, as theme names are
not error checked.  Any use of a non-existent theme is reported by the Apache::lonlatextable package
when the table text is generated.

=item width

The width of the table.   in any
TeX unit measure e.g.  10.8cm  This forces the table to the
tabularx environment.  It also forces the declarations for
cells to be paragraph mode which supports more internal formatting.

=back

=head3 Member data

The object hash has the following members:

=over 3

=item column_count 

Maintained internally, the number of colums in the widest row.

=item alignment

Table alignment (configurable) "left", "center", or "right".

=item outer_border

True if a border should be drawn around the entire table (configurable)

=item inner_borders

True if a border should be drawn around all cells (configurable).

=item caption

Table caption (configurable).

=item theme

Theme desired (configurable).

=item width

If defined, the width of the table (should be supplied
in fraction of column width e.g. .75 for 75%.

=item row_open 

True if a row is open and not yet closed.

=item rows

Array of row data. This is an array of hashes described below.

=back

=head3 Row data.

Each row of table data is an element of the rows hash array.  Hash elements are

=over 3


=item default_halign 
0
Default horizontal alignment for cells in this row.

=item default_valign

Default vertical alignment for cells in this row (may be ignored).

=item cell_width
 
The width of the row in cells.  This is the sum of the column spans 
of the cells in the row.

=item cells


Array of hashes where each element represents the data for a cell.
The contents of each element of this hash are described below:

=item header

If present, the row is a 'header' that is it was made via the
<th> tag.

=item halign

If present, overrides the row default horizontal alignment.

=item valign

if present, override the row default vertical alignment.

=item rowspan

If present, indicates the number of rows this cell spans.

=item colspan

If present indicates the number of columns this cell spans.
Note that a cell can span both rows and columns.

=item start_col

The starting column of the cell in the table grid.

=item contents

The contents of the cell.

=back


=cut

sub new {
    my ($class, $configuration) = @_;

    if ($tracing) {&Apache::lonnet::logthis("new table"); }
    #  Initialize the object member data with the default values
    #  then override with any stuff in $configuration.

    my $self = {
	alignment      => "left",
	outer_border   => 0,
	inner_border  => 0,
	caption        => "",
	theme          => "plain",
	column_count   => 0,
	row_open       => 0,
	rows           => {
	    'body'     => [],
            'head'     => [],
	    'foot'     => []
	},
	col_widths      => {},
	part           => 'body',     # one of 'body', 'head', 'foot'.
	colgroups      => []	      # Stores information about column groups.

    };

    foreach my $key (keys(%$configuration)) {
	$self->{$key} = $$configuration{$key};
    }

    bless($self, $class);

    return $self;
}


#-------------------------------------------------------------------------
#
#  Methods that get/set table global configuration.
#

=pod

=head2 Gets/set alignment.  

If the method is passed a new alignment value, that replaces the current one.
Regardless, the current alignment is used:

=head3 Examples:

 my $align = $table->alignment(); # Return current alignment
 $table->alignment("center");     # Attempt centered alignment.

=cut

sub alignment {
    my ($self, $new_value) = @_;

    if ($tracing) {&Apache::lonnet::logthis("alignment = $new_value");}

    if (defined($new_value)) {
	$self->{'alignment'} = $new_value;
    }
    return $self->{'alignment'};
}

=pod

=head2 table_border

Set or get the presence of an outer border in the table.
If passed a parameter, that parameter replaces the current request
for or not for an outer border. Regardless, the function returns
the final value of the outer_border request.

=head3 Examples:

  $table->table_border(1);      # Request an outer border.
  my $outer_requested = $table->table_border();

=cut

sub table_border {
    my ($self, $new_value) = @_;

    if ($tracing) {&Apache::lonnet::logthis("table_border $new_value");}

    if (defined($new_value)) {
	$self->{'outer_border'} = $new_value;
    }
    return $self->{'outer_border'};
}


=pod

=head2 cell_border

Set or get the presence of a request for cells to have borders
drawn around them.  If a paramter is passed, it will be treated as
a new value for the cell border configuration.  Regardless,the final
value of that configuration parameter is returned.
Valid values for the parameter are:

=over 2

=item 0 - no borders present.

=item 1 - All borders (borders around all four sides of the cell.

=item 2 - Border at top and bottom of the cell.

=item 3 - Border at the left and right sides of the cell.

=item 4 - Border around groups (colgroups as well as thead/tfoot/tbody).


=back

=head3 Examples:

 my $cell_border = $table->cell_border(); # ask if cell borders are requested.
 $table->cell_border(1);	# Request cell borders.

=cut

sub cell_border {
    my ($self, $new_value) = @_;
    if($tracing) {&Apache::lonnet::logthis("cell_border: $new_value"); }
    if (defined($new_value)) {
	$self->{'inner_border'} = $new_value;
    }
    return $self->{'inner_border'};
}

=pod

=head2 caption

Gets and/or sets the caption string for the table.  The caption string appears to label
the table.  If a parameter is supplied it will become the new caption string.k

=head3 Examples:


  $my caption = $table->caption();
  $table->caption("This is the new table caption");

=cut

sub caption {
    my ($self, $new_value) = @_;

    if($tracing) {&Apache::lonnet::logthis("caption: $new_value"); }
    if (defined($new_value)) {
	$self->{'caption'} = $new_value;
    }

    return $self->{'caption'};
}

=pod

=head2 theme

Gets and optionally sets the table theme.  The table theme describes how the
table will be typset by the table package.  If a parameter is supplied it
will be the new theme selection.

=head3 Examples:

  my $theme = $table->theme();
  $table->theme("Dresden");

=cut

sub theme {
    my ($self, $new_value) = @_;
    if($tracing) {&Apache::lonnet::logthis("theme $new_value"); }
    if (defined($new_value)) {
	$self->{'theme'} = $new_value;
    }
    return $self->{'theme'};
}

=pod

=head2 width

Gets and optionally sets the width of the table.

=head3 Examples:

 my $newwidth = $table->width("10cm");   # 10cm width returns "10cm".

=cut
sub width {
    my ($self, $new_value) = @_;
    if($tracing) {&Apache::lonnet::logthis("width = $new_value"); }

    if (defined($new_value)) {
	$self->{'width'} = $new_value;
    }
    return $self->{'width'}; 	# Could be undef.
}

=pod

=head2 start_row

Begins a new row in the table.  If a row is already open, that row is
closed off prior to starting the new row.  Rows can have the following attributes
which are specified by an optional hash passed in to this function.

=over 3

=item default_halign

The default horizontal alignment of the row. This can be "left", "center", or "right"

=item default_valign

The default vertical alignment of the row.  This can be "top", "center", or "bottom"

=back

=head3 Examples:

  $table_start_row();                  # no attributes.
  $table_start({default_halign => "center",
                default_valign => "bottom"}); # Create setting the attrbutes.

=cut

sub start_row {
    my ($self, $config) = @_;
    if($tracing) {&Apache::lonnet::logthis("start_row"); }
    if ($self->{'row_open'}) { 
	$self->end_row();
    }
    my $row_hash = {
	default_halign => "left",
	default_valign => "top",
	cell_width     =>  0,
	cells          => []
    };

    # Override the defaults if the config hash is present:

    if (defined($config)) {
	foreach my $key  (keys(%$config)) {
	    $row_hash->{$key} = $config->{$key};
	}
    }

    
    my $rows = $self->{'rows'}->{$self->{'part'}};
    push(@$rows, $row_hash);

    $self->{"row_open"} = 1;	# Row is now open and ready for business.
}

=pod

=head2  end_row 

Closes off a row.  Once closed, cells cannot be added to this row again.

=head3 Examples:

   $table->end_row();


=cut

sub end_row {
    my ($self) = @_;
    if($tracing) {&Apache::lonnet::logthis("end_row"); }
    if ($self->{'row_open'}) {
	
	# Mostly we need to determine if this row has the maximum
	# cell count of any row in existence in the table:
	

	my $row        = $self->{'rows'}->{$self->{'part'}}->[-1];
	my $cells      = $row->{'cells'};

	if ($row->{'cell_width'} > $self->{'column_count'}) {
	    $self->{'column_count'} = $row->{'cell_width'};
	}

	$self->{'row_open'} = 0;;
    }
}

=pod

=head2 configure_row

Modify the configuration of a row.   If a row is not open, a new one will be opened.

=head3 Parameters:

config_hash - A hash that contains new values for the set of row confiuguration 
items to be modified.  There is currently no check/penalty for items that are not in
the set of defined configuration properties which are:

=over 2

=item default_halign

The default horizontal alignment for text in  cells in the row.  This can be any of:
"left", "right" or "center".

=item default_valign

The default vertical alignment for text in cells in the row.  This can be any of:

"top", "bottom" or "center"


=back 

=cut

sub configure_row {
    my ($self, $config) = @_;
    if($tracing) {&Apache::lonnet::logthis("configure_row");}
    if (!$self->{'row_open'}) {
	$self->start_row();
    }
    
    my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
    foreach my $config_item (keys(%$config)) {
	$row->{$config_item} = $config->{$config_item};
    }
}


=pod

=head2 add_cell

Add a new cell to a row.  If there is a row above us, we need to 
watch out for row spans that may force additional blank cell entries
to fill in the span. 

=head3 Parameters:

=over 2

=item text

Text to put in the cell.

=item cell_config

Hash of configuration options that override the defaults.   The recognized options,
and their defaults are:

=over 2

=item halign 

If nonblank overrides the row's default for the cell's horizontal alignment.

=item valign

If nonblank, overrides the row's default for the cdell's vertical alignment.

=item rowspan

Number of rows the cell spans.

=item colspan

Number of columns the cell spans.

=item width

LaTeX specification of the width of the cell.
Note that if there is a colspan this width is going to be equally divided
over the widths of the columnsn in the span.
Note as well that if width specification conflict, the last one specified wins...silently.

=back

=back 

=cut

sub add_cell {
    my ($self, $text, $config) = @_;

    if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); }

    # If a row is not open, we must open it:

    if (!$self->{'row_open'}) {
	$self->start_row();
    }
    my $rows          = $self->{'rows'}->{$self->{'part'}};
    my $current_row   = $rows->[-1];
    my $current_cells = $current_row->{'cells'}; 
    my $last_coord    = $current_row->{'cell_width'};

    #  We have to worry about row spans if there is a prior row:

    if (scalar(@$rows) > 1) {

	my $last_row = $rows->[-2];
	if ($last_coord < $last_row->{'cell_width'}) {
	    my $prior_coord       = 0;
	    my $prior_cell_index  = 0;
	    while ($prior_coord <= $last_coord) {
		
		# Pull a cell down if it's coord matches our start coord
		# And there's a row span > 1.
		# Having done so, we adjust our $last_coord to match the
		# end point of the pulled down cell.

		my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
		if (!defined($prior_cell)) {
		    last;
		}
		if (($prior_cell->{'start_col'} == $last_coord) &&
		    ($prior_cell->{'rowspan'}  > 1)) {
		    
		    #  Need to drop the cell down

		    my %dropped_down_cell = %$prior_cell;
		    $dropped_down_cell{'rowspan'}--;
		    $dropped_down_cell{'contents'} = '';

		    push(@$current_cells, \%dropped_down_cell);
		    $last_coord += $dropped_down_cell{'colspan'};
		    $current_row->{'cell_width'} = $last_coord;
		    
		}
		$prior_coord += $prior_cell->{'colspan'};
		$prior_cell_index++;
	    }
	}

    }

    #
    # Now we're ready to build up our cell:

    my $cell = {
	rowspan    => 1,
	colspan    => 1,
	start_col  => $last_coord,
	contents   => $text
    };
    
    if (defined($config)) {
	foreach my $key (keys(%$config)) {
            if ($key eq 'colspan') {
                next if ($config->{$key} == 0);
            }
	    $cell->{$key} = $config->{$key};
	}
    }

    $current_row->{'cell_width'} += $cell->{'colspan'};


    #
    # Process the width if it exists.  If supplied it must be of the form:
    #   float units
    # Where units can be in, cm or mm.
    # Regardless of the supplied units we will normalize to cm.
    # This allows computation on units at final table generation time.
    #

    if (exists($cell->{'width'})) {
	my $width;
	my $widthcm;
	$width   = $config->{'width'};
	$widthcm = $self->size_to_cm($width);
	
	# If there's a column span, the actual width is divided by the span
	# and applied to each of the columns in the span.

	$widthcm = $widthcm / $cell->{'colspan'};
	for (my $i = $last_coord; $i < $last_coord + $cell->{'colspan'}; $i++) {
	    $self->{'col_widths'}->{$i} = $widthcm; 
	}
	
    }

    push(@$current_cells, $cell);

    if ($tracing) { &Apache::lonnet::logthis("add_cell done"); }
}


=pod

=head2  append_cell_text

Sometimes it's necessary to create/configure the cell and then later add text to it.
This sub allows text to be appended to the most recently created cell.

=head3 Parameters

The text to add to the cell.

=cut
sub append_cell_text {
    my ($this, $text) = @_;

    if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); }
    my $rows         = $this->{'rows'}->{$this->{'part'}};
    my $current_row  = $rows->[-1];
    my $cells        = $current_row->{'cells'};
    my $current_cell = $cells->[-1];
    $current_cell->{'contents'} .= $text;
    
}
#-------------------------- Support for row/column groups.   ----

=pod 

=head2 start_head 

starts the table head.  This corresponds to the <thead> tag in 
html/xml.  All rows defined in this group will be
collected and placed at the front of the table come rendering time.
Furthermore, if the table has group borders enabled, a rule will be
rendered following and preceding this group of rows.

=cut

sub start_head {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("start_head"); }
    $this->{'part'}  = 'head';
}

=pod     

=head2 end_head   

Ends a table head.  This corresponds to the
</thead> closing tag in html/xml.

=cut

sub end_head {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("end_head"); }
    $this->{'part'} = 'body';
}

=pod

=head2 start_foot

Starts the table footer.  All of the rows generated in the footer will
be rendered at the bottom of the table.  This sub corresponds to the
<tfoot> tag in html/xml.  If the table has group borders enabled, a rule
will be rendered at the top and bottom of the set of columns in this
group

=cut

sub start_foot {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("start_foot"); }
    $this->{'part'}   = 'foot';
}

=pod

=head2 end_foot

Ends the set of rows in the table footer.  This corresponds to the
</tfoot> end tag in xml/html.

=cut

sub end_foot {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("end_foot") }
    $this->{'part'}  = 'body';
}

=pod

=head2 start_body

Starts the set of rows that will be in the table body.   Note that if
we are not in the header or footer, body rows are implied.
This correspondes to the presence of a <tbody> tag in html/xml.
If group borders are on, a rule will be rendered at the top and bottom
of the body rows.

=cut

sub start_body {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("start_body"); }
    $this->{'part'}  = 'body';
}

=pod
 
=head2 end_body

Ends the set of rows in a table body.  Note that in the event we are not
in  the header or footer groups this code assumes we are in the body
group.  I believe this is a good match to how mot browsers render.

=cut

sub end_body {
    my ($this) = @_;
    if ($tracing) { &Apache::lonnet::logthis("end_body"); }

}

=pod

=head2 define_colgroup

Define a column group  a column group corresponds to the
<cgroup> tag in Html/Xml. A column group as we implement it has
the following properties tht will be shared amongst all cells in the
columns in the group unless overidden in the specific oell definition:

=over 2

=item span 

The number of columns in the column group.  This defaults to 1.

=item halign

Horizontal alignment of the cells.  This defaults to left.
Other values are left, center, right (justify and char are 
accepted but treated as left).
  
=item valign

Vertical alignment of the cells.  This defaults to middle.
Other values are top middle, bottom, (baseline is accepted and
treated as top).

=back   

If group borders are turned on, a rule will be rendered
at the left and right side of the column group.

=head3 parameters

=over 2

=item definition

This is a hash that contains any of the keys described above that
define the column group.

=back


=head3 Example

 $table->define_colgroup({
    'span'    => 2,
    'halign'  => 'center'
                         })



=cut

sub define_colgroup {
    my ($this, $attributes)  = @_;
    if ($tracing) { &Apache::lonnet::logthis("col_group"); }
    my $colgroups = $this->{'colgroups'};
    push(@$colgroups, $attributes); # Colgroups always add at end.


}

#------------------------- Render the table ---------------------

=pod

=head2 generate

Call this when the structures for the table have been built.
This will generate and return the table object that can be used
to generate the table.  Returning the table object allows for
a certain amount of testing to be done on the generated table.
The caller can then ask the table object to generate LaTeX.

=cut

sub generate {
    my ($this) = @_;
    my $useP   = 0;

    my $colunits = 'cm';	# All widths get normalized to cm.
    my $tablewidth;

    if($tracing) {&Apache::lonnet::logthis("generate"); }
    my $table = Apache::lonlatextable->new();

    my $inner_border = $this->{'inner_border'};
    my $outer_border = $this->{'outer_border'};
    my $column_count = $this->{'column_count'};

    my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
    my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
    my $part_border   = ($inner_border == 4);
 
 
      # Add the caption if supplied.

    if ($this->{'caption'} ne "") {
	$table->set_caption($this->caption);
    }
    
    # Set the width if defined:

    my $default_width;
    my $colwidths        = $this->{'col_widths'};
    if (defined ($this->{'width'})) {
	$tablewidth = $this->{'width'};
	$tablewidth = $this->size_to_cm($tablewidth);

	$useP = 1;

	# Figure out the default width for a column with unspecified
	# We take the initially specified widths and sum them up.
	# This is subtracted from total width  above.
	# If the result is negative we're going to allow a minimum of 2.54cm for
	# each column and make the table spill appropriately.  
	# This (like a riot) is an ugly thing but I'm open to suggestions about
	# how to handle it better (e.g. scaling down requested widths?).

	my $specified_width = 0.0;
	my $specified_cols   = 0;
	foreach my $col (keys(%$colwidths)) {
	    $specified_width = $specified_width + $colwidths->{$col};
	    $specified_cols++;
	}
	my $unspecified_cols = $this->{'column_count'} - $specified_cols;

	#  If zero unspecified cols, we are pretty much done... just have to
	#  adjust the total width to be specified  width. Otherwise we
	#  must figure out the default width and total width:
	#
	my $total_width;
	if($unspecified_cols == 0) {
	    $total_width = $specified_width;
	} else {
	    $default_width = ($tablewidth - $specified_width)/$unspecified_cols; #  Could be negative....
	    $total_width   = $default_width * $unspecified_cols + $specified_width;
	}
	
	# if the default_width is < 0.0 the user has oversubscribed the width of the table with the individual
	# column.  In this case, we're going to maintain the desired proportions of the user's columns, but 
	# ensure that the unspecified columns get a fair share of the width..where a fair share is defined as
	# the total width of the table / unspecified column count.
	# We figure out what this means in terms of reducing the specified widths by dividing by a constant proportionality.
	# Note that this cannot happen if the user hasn't specified anywidths as the computation above would then
	# just make all columns equal fractions of the total table width.

	if ($default_width < 0) {
	    $default_width = ($tablewidth/$unspecified_cols);                     # 'fair' default width.
	    my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols.
	    my $reduction       = $tablewidth/$width_remaining;                    # Reduction fraction for specified cols
	    foreach my $col (keys(%$colwidths)) {
		$colwidths->{$col} = $colwidths->{$col}/$reduction;
	    }
	    
        }
    }
    # If rule is groups. we need to have a 
    # list of the column numbers at which a column ends...
    # and the coldef needs to start with a |
    #
    my @colgroup_ends;
    my $colgroup_col = 0;
    my $group = 0;
    my $coldef = "";
    if ($outer_border || $cell_lr_border) {
	$coldef .= '|';
    }
    if ($part_border) {
	$coldef .= '|';
	my $colgroup_col = 0;
	my $colgroups = $this->{'colgroups'};
	foreach my $group (@$colgroups) {
	    if (defined $group->{'span'}) {
		$colgroup_col += $group->{'span'};
	    } else {
		$colgroup_col++;
	    }
	    push(@colgroup_ends, $colgroup_col);
	}
				 
    }
    $this->render_part('head', $table, $useP, $default_width, 
		       \@colgroup_ends);
    $this->render_part('body', $table, $useP, $default_width,
	\@colgroup_ends);
    $this->render_part('foot', $table, $useP, $default_width,
	\@colgroup_ends);




    
    for (my $i =0; $i < $column_count; $i++) {
	if ($useP) {
	    $coldef .= "p{$default_width $colunits}";
	} else {
	    $coldef .= 'l';
	}
	if ($cell_lr_border || 
	    ($outer_border && ($i == $column_count-1))) {
	    $coldef .= '|';
	}
	if ($part_border && ($i == ($colgroup_ends[$group]-1)))  {
	    $coldef .= '|';
	    $group++;
	}
    }
    $table->{'coldef'} = $coldef;

    # Return the table:

    if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }


    return $table;

}


#---------------------------------------------------------------------------
#
#  Private methods:
#

# 
# Convert size with units -> size in cm.
# The resulting size is floating point with no  units so that it can be used in
# computation.  Note that an illegal or missing unit is treated silently as
#  cm for now.
#
sub size_to_cm {
    my ($this, $size_spec) = @_;
    my ($size, $units) = split(/ /, $size_spec);
    if (lc($units) eq 'mm') {
	return $size / 10.0;
    }
    if (lc($units) eq 'in') {
	return $size * 2.54;
    }
    
    return $size;		# Default is cm.
}

#
#  Render a part of the table.  The valid table parts are
#  head, body and foot.  These corresopnd to the set of rows
#  define within <thead></thead>, <tbody></tbody> and <tfoot></tfoot>
#  respectively.
#
sub render_part {
    my ($this, $part, $table, $useP,
	$default_width, $colgroup_ends) = @_;

    if ($tracing) { &Apache::lonnet::logthis("render_part: $part") };

    # Do nothing if that part of the table is empty:

    if ($this->{'rows'}->{$part} == undef) {
	if ($tracing) {&Apache::lonnet::logthis("$part is empty"); }
	return;
    }

    my @cgends = @$colgroup_ends;
    # Build up the data:

    my @data;
    my $colwidths        = $this->{'col_widths'};
    my $rows      = $this->{'rows'}->{$part}; 
    my $row_count = scalar(@$rows);
    my $inner_border = $this->{'inner_border'};
    my $outer_border = $this->{'outer_border'};
    my $column_count = $this->{'column_count'};

    my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
    my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
    my $part_border   = ($inner_border == 4);
    my $colunits    = 'cm';	# All units in cm.

    # Add a top line if the outer or inner border is enabled:
    # or if group rules are on.
    #

    if ($outer_border || $cell_ul_border || $part_border) {
	push(@data, ["\\cline{1-$column_count}"]);	     

    }

    for (my $row = 0; $row < $row_count; $row++) {
	my @row;
	my $cells      = $rows->[$row]->{'cells'};
	my $def_halign = $rows->[$row]->{'default_halign'};
	my $cell_count = scalar(@$cells);
	my $startcol   = 1;
	my @underlines;		# Array of \cline cells if cellborder on.

	my $colgroup_count = @cgends; # Number of column groups.
	my $cgroup         = 0;	     # Group we are on.
	my $cgstart        = 0;	     # Where the next cgroup starts.

	for (my $cell  = 0; $cell < $cell_count; $cell++) {
	    my $contents = $cells->[$cell]->{'contents'};
	    
	    #
	    #  Cell alignment is the default alignment unless
	    #  explicitly specified in the cell.
	    #  NOTE: at this point I don't know how to do vert alignment.
	    #

	    my $halign   = $def_halign;
	    if (defined ($cells->[$cell]->{'halign'})) {
		$halign = $cells->[$cell]->{'halign'};
	    }

	    # Create the horizontal alignment character:

	    my $col_align = 'l';
	    my $embeddedAlignStart = "";
	    my $embeddedAlignEnd   = "";

	    if ($halign eq 'right') {
		$col_align = 'r';
                $embeddedAlignStart = '\raggedleft';
	    }
	    if ($halign eq 'center') {
		$col_align = 'c';
		$embeddedAlignStart = '\begin{center}';
		$embeddedAlignEnd   = '\end{center}';
	    }

	    # If the width has been specified, turn these into
	    # para mode; and wrap the contents in the start/stop stuff:

	    if ($useP) {
		my $cw;
		if (defined($colwidths->{$cell})) {
		    $cw = $colwidths->{$cell};
		} else {
		    $cw = $default_width;
		}
		$cw = $cw * $cells->[$cell]->{'colspan'};
		$col_align = "p{$cw $colunits}";
		$contents = $embeddedAlignStart . $contents .  $embeddedAlignEnd;
	    }

	    if ($cell_lr_border || ($outer_border && ($cell == 0))) {
		$col_align = '|'.$col_align;
	    }
	    if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) {
		$col_align = $col_align.'|';
	    }
	    if ($part_border)  {
		if ($cell == $cgstart) {
		    $col_align = '|' . $col_align;
		    if ($cgroup < $colgroup_count) {
			$cgstart = $cgends[$cgroup];
			$cgroup++;
		    } else {
			$cgstart = 1000000; # TODO: Get this logic right
		    }
		    if ($cell == ($cell_count - 1) &&
			($cell == ($cgstart-1))) {
			$col_align = $col_align . '|'; # last col ends colgrp.
		    }
		}
	    }

	    #factor in spans:

	    my $cspan    = $cells->[$cell]->{'colspan'};
	    my $nextcol  = $startcol + $cspan;
	    
	    # At this point this col is the start of the span.
	    # nextcol is the end of the span.

	    # If we can avoid the \multicolumn directive that's best as
	    # that makes some things like \parpic invalid in LaTeX which
            # screws everything up.

	    if (($cspan > 1) || !($col_align =~ /l/)) {

		$contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';

		# A nasty edge case.  If there's only one cell, the software will assume
		# we're in complete control of the row so we need to end the row ourselves.
		
		if ($cell_count == 1) {
		    $contents .= '  \\\\';
		}
	    }
	    if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) {
		my $lastcol = $nextcol -1;
		push(@underlines, "\\cline{$startcol-$lastcol}");
	    }
	    $startcol = $nextcol;

	    # Rowspans should take care of themselves.
	    
	    push(@row, $contents);

	}
	push(@data, \@row);
	if ($cell_ul_border) {
	    for (my $i =0; $i < scalar(@underlines); $i++) {
		push(@data, [$underlines[$i]]);
	    }
	}

    }
    #
    # Add bottom border if necessary: if the inner border was on, the loops above
    # will have done a bottom line under the last cell.
    #
    if (($outer_border || $part_border) && !$cell_ul_border) {
	push(@data, ["\\cline{1-$column_count}"]);	     

    }
    $table->set_data(\@data);    
}

#----------------------------------------------------------------------------
# The following methods allow for testability.


sub get_object_attribute {
    my ($self, $attribute) = @_;
    if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
    return $self->{$attribute};
}

sub get_row {
    my ($self, $row) = @_;
    if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }

    my $rows = $self->{'rows'}->{$self->{'part'}};	  # ref to an array....
    return $rows->[$row];         # ref to the row hash for the selected row.
}

#   Mandatory initialization.
BEGIN{
}

1;
__END__


FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>