--- loncom/xml/lontable.pm 2010/08/27 09:42:49 1.12 +++ loncom/xml/lontable.pm 2011/04/13 10:08:06 1.17 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Generating TeX tables. # -# $Id: lontable.pm,v 1.12 2010/08/27 09:42:49 foxr Exp $ +# $Id: lontable.pm,v 1.17 2011/04/13 10:08:06 foxr Exp $ # # # Copyright Michigan State University Board of Trustees @@ -57,6 +57,7 @@ package Apache::lontable; use strict; use Apache::lonlatextable; use Apache::lonnet; # for trace logging. +use Data::Dumper; my $tracing = 1; # Set to 1 to enable log tracing. 2 for local sub tracing. @@ -108,7 +109,7 @@ modified by this. These configuration i my $table = lontable::new(\%config_hash) -=over3 +=over 3 =item alignment @@ -211,11 +212,10 @@ 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: -=over 3 - =item header If present, the row is a 'header' that is it was made via the @@ -248,15 +248,13 @@ The contents of the cell. =back -=back =cut sub new { my ($class, $configuration) = @_; - if($tracing) {&Apache::lonnet::logthis("new table object");} - + if ($tracing) {&Apache::lonnet::logthis("new table"); } # Initialize the object member data with the default values # then override with any stuff in $configuration. @@ -268,7 +266,14 @@ sub new { theme => "plain", column_count => 0, row_open => 0, - rows => [], + rows => { + 'body' => [], + 'head' => [], + 'foot' => [] + }, + col_widths => {}, + part => 'body' # one of 'body', 'head', 'foot'. + }; foreach my $key (keys %$configuration) { @@ -347,6 +352,22 @@ Set or get the presence of a request for 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: @@ -416,11 +437,11 @@ sub theme { =pod -=head 2 width +=head2 width Gets and optionally sets the width of the table. -=head 3 Examples: +=head3 Examples: my $newwidth = $table->width("10cm"); # 10cm width returns "10cm". @@ -485,7 +506,7 @@ sub start_row { } - my $rows = $self->{'rows'}; + my $rows = $self->{'rows'}->{$self->{'part'}}; push(@$rows, $row_hash); $self->{"row_open"} = 1; # Row is now open and ready for business. @@ -511,8 +532,10 @@ sub end_row { # 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'}->[-1]; + + &Apache::lonnet::logthis($self->{'part'}); + &Apache::lonnet::logthis(Dumper($self->{'rows'})); + my $row = $self->{'rows'}->{$self->{'part'}}->[-1]; my $cells = $row->{'cells'}; if ($row->{'cell_width'} > $self->{'column_count'}) { @@ -548,6 +571,7 @@ The default vertical alignment for text "top", "bottom" or "center" + =back =cut @@ -559,7 +583,7 @@ sub configure_row { $self->start_row(); } - my $row = $self->{'rows'}[-1]; + my $row = $self->{'rows'}->{$self->{'part'}}->[-1]; foreach my $config_item (keys %$config) { $row->{$config_item} = $config->{$config_item}; } @@ -605,8 +629,17 @@ Number of rows the cell spans. 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 { @@ -619,7 +652,7 @@ sub add_cell { if (!$self->{'row_open'}) { $self->start_row(); } - my $rows = $self->{'rows'}; + my $rows = $self->{'rows'}->{$self->{'part'}}; my $current_row = $rows->[-1]; my $current_cells = $current_row->{'cells'}; my $last_coord = $current_row->{'cell_width'}; @@ -676,11 +709,40 @@ sub add_cell { 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"); } @@ -703,14 +765,178 @@ sub append_cell_text { my ($this, $text) = @_; if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); } - my $rows = $this->{'rows'}; + 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 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 + 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 + 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 + 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 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 + 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"); } + + +} + +#------------------------- Render the table --------------------- =pod @@ -723,17 +949,25 @@ a certain amount of testing to be done o The caller can then ask the table object to generate LaTeX. =cut + sub generate { my ($this) = @_; my $useP = 0; - my $colwidth; - my $colunits; + + 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 "") { @@ -742,27 +976,163 @@ sub generate { # Set the width if defined: + my $default_width; + my $colwidths = $this->{'col_widths'}; if (defined ($this->{'width'})) { -# $table->set_width($this->{'width'}); -# $table->set_width_environment('tabularx'); + $tablewidth = $this->{'width'}; + $tablewidth = $this->size_to_cm($tablewidth); + $useP = 1; - ($colwidth, $colunits) = split(/ /, $this->{'width'}); - $colwidth = $colwidth/$this->{'column_count'}; + # 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 ($tracing) { &Apache::lonnet::logthis("rendering head"); } + $this->render_part('head', $table, $useP, $default_width); + if ($tracing) { &Apache::lonnet::logthis("rendering body"); } + $this->render_part('body', $table, $useP, $default_width); + if ($tracing) { &Apache::lonnet::logthis("rendering footer"); } + $this->render_part('foot', $table, $useP, $default_width); + + + + + + my $coldef = ""; + if ($outer_border || $cell_lr_border) { + $coldef .= '|'; + } + 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 .= '|'; + } + } + $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 , and +# respectively. +# +sub render_part { + my ($this, $part, $table, $useP, $default_width) = @_; + + if ($tracing) { &Apache::lonnet::logthis("render_part: $part") }; + + # Do nothing if that part of the table is empty: + + &Apache::lonnet::logthis(Dumper($this->{'rows'})); + if ($this->{'rows'}->{$part} == undef) { + if ($tracing) {&Apache::lonnet::logthis("$part is empty"); } + return; + } + # Build up the data: my @data; - my $rows = $this->{'rows'}; + my $colwidths = $this->{'col_widths'}; + my $rows = $this->{'rows'}->{$part}; # TODO: Render header, body footer as groups. 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 || $inner_border) { + if ($outer_border || $cell_ul_border || $part_border) { push(@data, ["\\cline{1-$column_count}"]); } @@ -799,8 +1169,7 @@ sub generate { if ($halign eq 'right') { $col_align = 'r'; - $embeddedAlignStart = '\begin{flushright} '; - $embeddedAlignEnd = ' \end{flushright}'; + $embeddedAlignStart = '\raggedleft'; } if ($halign eq 'center') { $col_align = 'c'; @@ -812,15 +1181,21 @@ sub generate { # para mode; and wrap the contents in the start/stop stuff: if ($useP) { - my $cw = $colwidth * $cells->[$cell]->{'colspan'}; + 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 ($inner_border || ($outer_border && ($cell == 0))) { + if ($cell_lr_border || ($outer_border && ($cell == 0))) { $col_align = '|'.$col_align; } - if ($inner_border || ($outer_border && ($cell == ($cell_count -1)))) { + if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) { $col_align = $col_align.'|'; } @@ -844,7 +1219,7 @@ sub generate { $contents .= ' \\\\'; } } - if ($inner_border && ($cells->[$cell]->{'rowspan'} == 1)) { + if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) { my $lastcol = $nextcol -1; push(@underlines, "\\cline{$startcol-$lastcol}"); } @@ -855,7 +1230,7 @@ sub generate { } push(@data, \@row); - if ($inner_border) { + if ($cell_ul_border) { for (my $i =0; $i < scalar(@underlines); $i++) { push(@data, [$underlines[$i]]); } @@ -866,37 +1241,13 @@ sub generate { # 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 && !$inner_border) { + if (($outer_border || $part_border) && !$cell_ul_border) { push(@data, ["\\cline{1-$column_count}"]); } - $table->set_data(\@data); - - my $coldef = ""; - if ($outer_border || $inner_border) { - $coldef .= '|'; - } - for (my $i =0; $i < $column_count; $i++) { - if ($useP) { - $coldef .= "p{$colwidth $colunits}"; - } else { - $coldef .= 'l'; - } - if ($inner_border || - ($outer_border && ($i == $column_count-1))) { - $coldef .= '|'; - } - } - $table->{'coldef'} = $coldef; - - # Return the table: - - if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); } - - - return $table; - + $table->set_data(\@data); } + #---------------------------------------------------------------------------- # The following methods allow for testability. @@ -911,12 +1262,14 @@ sub get_row { my ($self, $row) = @_; if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); } - my $rows = $self->{'rows'}; # ref to an array.... + 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__ +