Diff for /loncom/xml/lontable.pm between versions 1.7 and 1.16

version 1.7, 2008/12/29 11:57:37 version 1.16, 2011/04/05 10:02:58
Line 38 Line 38
 #  #
   
 # This module is a support packkage that helps londefdef generate  # This module is a support packkage that helps londefdef generate
 # LaTeX tables using the LaTeX::Table package.  A prerequisite is that  # LaTeX tables using the Apache::lonlatextable package.  A prerequisite is that
 # the print generator must have added the following to the LaTeX   # the print generator must have added the following to the LaTeX 
 #  #
 #  \usepackage{xtab}  #  \usepackage{xtab}
Line 55 Line 55
   
 package Apache::lontable;  package Apache::lontable;
 use strict;  use strict;
 use LaTeX::Table;  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  =pod
   
 =head1  lontable Table generation assistant for the LaTeX target  =head1  lontable Table generation assistant for the LaTeX target
   
 This module contains support software for generating tables in LaTeX output mode   This module contains support software for generating tables in LaTeX output mode 
 In this implementation, we use the LaTeX::Table package to do the actual final formatting.  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.  Each table creates a new object.  Table objects can have global properties configured.
 The main operations on a table object are:  The main operations on a table object are:
   
Line 129  The table caption text. Line 131  The table caption text.
   
 The theme of the table to use.  Defaults to Zurich.  Themes we know about are:  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  NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris.  Other themes can be added
 to the LaTeX::Table package, and they will become supported automatically, as theme names are  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 LaTeX::Table package  not error checked.  Any use of a non-existent theme is reported by the Apache::lonlatextable package
 when the table text is generated.  when the table text is generated.
   
 =item width  =item width
   
 The width of the table.  This can be expressed as fractions of full width, or in any  The width of the table.   in any
 TeX unit measure e.g. 0.75 for 75% of the width, or 10.8cm  This forces the table to the  TeX unit measure e.g.  10.8cm  This forces the table to the
 tabularx environment.  tabularx environment.  It also forces the declarations for
   cells to be paragraph mode which supports more internal formatting.
   
 =back  =back
   
Line 252  The contents of the cell. Line 255  The contents of the cell.
 sub new {  sub new {
     my ($class, $configuration) = @_;      my ($class, $configuration) = @_;
   
   
     #  Initialize the object member data with the default values      #  Initialize the object member data with the default values
     #  then override with any stuff in $configuration.      #  then override with any stuff in $configuration.
   
Line 260  sub new { Line 264  sub new {
  outer_border   => 0,   outer_border   => 0,
  inner_border  => 0,   inner_border  => 0,
  caption        => "",   caption        => "",
  theme          => "Zurich",   theme          => "plain",
  column_count   => 0,   column_count   => 0,
  row_open       => 0,   row_open       => 0,
  rows           => [],   rows           => [],
    col_widths      => {}
     };      };
   
     foreach my $key (keys %$configuration) {      foreach my $key (keys %$configuration) {
Line 298  Regardless, the current alignment is use Line 303  Regardless, the current alignment is use
 sub alignment {  sub alignment {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
   
       if ($tracing) {&Apache::lonnet::logthis("alignment = $new_value");}
   
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'alignment'} = $new_value;   $self->{'alignment'} = $new_value;
     }      }
Line 323  the final value of the outer_border requ Line 330  the final value of the outer_border requ
 sub table_border {  sub table_border {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
   
       if ($tracing) {&Apache::lonnet::logthis("table_border $new_value");}
   
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'outer_border'} = $new_value;   $self->{'outer_border'} = $new_value;
     }      }
Line 338  Set or get the presence of a request for Line 347  Set or get the presence of a request for
 drawn around them.  If a paramter is passed, it will be treated as  drawn around them.  If a paramter is passed, it will be treated as
 a new value for the cell border configuration.  Regardless,the final  a new value for the cell border configuration.  Regardless,the final
 value of that configuration parameter is returned.  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.
   
   
   =over -2 
   
 =head3 Examples:  =head3 Examples:
   
Line 348  value of that configuration parameter is Line 371  value of that configuration parameter is
   
 sub cell_border {  sub cell_border {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
       if($tracing) {&Apache::lonnet::logthis("cell_border: $new_value"); }
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'inner_border'} = $new_value;   $self->{'inner_border'} = $new_value;
     }      }
Line 373  the table.  If a parameter is supplied i Line 396  the table.  If a parameter is supplied i
 sub caption {  sub caption {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
   
       if($tracing) {&Apache::lonnet::logthis("caption: $new_value"); }
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'caption'} = $new_value;   $self->{'caption'} = $new_value;
     }      }
Line 397  will be the new theme selection. Line 421  will be the new theme selection.
   
 sub theme {  sub theme {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
       if($tracing) {&Apache::lonnet::logthis("theme $new_value"); }
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'theme'} = $new_value;   $self->{'theme'} = $new_value;
     }      }
Line 412  Gets and optionally sets the width of th Line 436  Gets and optionally sets the width of th
   
 =head 3 Examples:  =head 3 Examples:
   
  $table->width("0.8");    # 80% of the column width.  
  my $newwidth = $table->width("10cm");   # 10cm width returns "10cm".   my $newwidth = $table->width("10cm");   # 10cm width returns "10cm".
   
 =cut  =cut
 sub width {  sub width {
     my ($self, $new_value) = @_;      my ($self, $new_value) = @_;
       if($tracing) {&Apache::lonnet::logthis("width = $new_value"); }
   
     if (defined($new_value)) {      if (defined($new_value)) {
  $self->{'width'} = $new_value;   $self->{'width'} = $new_value;
     }      }
Line 454  The default vertical alignment of the ro Line 479  The default vertical alignment of the ro
   
 sub start_row {  sub start_row {
     my ($self, $config) = @_;      my ($self, $config) = @_;
       if($tracing) {&Apache::lonnet::logthis("start_row"); }
     if ($self->{'row_open'}) {       if ($self->{'row_open'}) { 
  $self->end_row();   $self->end_row();
     }      }
Line 495  Closes off a row.  Once closed, cells ca Line 520  Closes off a row.  Once closed, cells ca
   
 sub end_row {  sub end_row {
     my ($self) = @_;      my ($self) = @_;
       if($tracing) {&Apache::lonnet::logthis("end_row"); }
     if ($self->{'row_open'}) {      if ($self->{'row_open'}) {
   
  # Mostly we need to determine if this row has the maximum   # Mostly we need to determine if this row has the maximum
Line 537  The default vertical alignment for text Line 562  The default vertical alignment for text
   
 "top", "bottom" or "center"  "top", "bottom" or "center"
   
   
 =back   =back 
   
 =cut  =cut
   
 sub configure_row {  sub configure_row {
     my ($self, $config) = @_;      my ($self, $config) = @_;
       if($tracing) {&Apache::lonnet::logthis("configure_row");}
     if (!$self->{'row_open'}) {      if (!$self->{'row_open'}) {
  $self->start_row();   $self->start_row();
     }      }
Line 594  Number of rows the cell spans. Line 620  Number of rows the cell spans.
   
 Number of columns 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  =cut
Line 601  Number of columns the cell spans. Line 634  Number of columns the cell spans.
 sub add_cell {  sub add_cell {
     my ($self, $text, $config) = @_;      my ($self, $text, $config) = @_;
   
       if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); }
   
     # If a row is not open, we must open it:      # If a row is not open, we must open it:
   
     if (!$self->{'row_open'}) {      if (!$self->{'row_open'}) {
Line 627  sub add_cell { Line 662  sub add_cell {
  # end point of the pulled down cell.   # end point of the pulled down cell.
   
  my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];   my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
    if (!defined($prior_cell)) {
       last;
    }
  if (($prior_cell->{'start_col'} == $last_coord) &&   if (($prior_cell->{'start_col'} == $last_coord) &&
     ($prior_cell->{'rowspan'}  > 1)) {      ($prior_cell->{'rowspan'}  > 1)) {
           
Line 660  sub add_cell { Line 698  sub add_cell {
           
     if (defined($config)) {      if (defined($config)) {
  foreach my $key (keys(%$config)) {   foreach my $key (keys(%$config)) {
               if ($key eq 'colspan') {
                   next if ($config->{$key} == 0);
               }
     $cell->{$key} = $config->{$key};      $cell->{$key} = $config->{$key};
  }   }
     }      }
   
     $current_row->{'cell_width'} += $cell->{'colspan'};      $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);      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'};
       my $current_row  = $rows->[-1];
       my $cells        = $current_row->{'cells'};
       my $current_cell = $cells->[-1];
       $current_cell->{'contents'} .= $text;
       
   }
   
   
 =pod  =pod
   
 =head2 generate  =head2 generate
Line 681  The caller can then ask the table object Line 776  The caller can then ask the table object
 =cut  =cut
 sub generate {  sub generate {
     my ($this) = @_;      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 $table = LaTeX::Table->new();  
   
     # Add the caption if supplied.      # Add the caption if supplied.
   
     if ($this->{'caption'} ne "") {      if ($this->{'caption'} ne "") {
  $table->set_caption($this->caption);   $table->set_caption($this->caption);
     }      }
   
           
     # Set the width if defined:      # Set the width if defined:
   
       my $default_width;
       my $colwidths        = $this->{'col_widths'};
     if (defined ($this->{'width'})) {      if (defined ($this->{'width'})) {
  $table->set_width($this->{'width'});   $tablewidth = $this->{'width'};
  $table->set_width_environment('tabularx');   $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;
       }
       
           }
     }      }
   
   
   
   
     # Build up the data:      # Build up the data:
   
     my @data;      my @data;
Line 707  sub generate { Line 860  sub generate {
     my $outer_border = $this->{'outer_border'};      my $outer_border = $this->{'outer_border'};
     my $column_count = $this->{'column_count'};      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;
    
       # Add a top line if the outer or inner border is enabled:
   
       if ($outer_border || $cell_ul_border) {
    push(@data, ["\\cline{1-$column_count}"]);     
   
       }
   
     for (my $row = 0; $row < $row_count; $row++) {      for (my $row = 0; $row < $row_count; $row++) {
  my @row;   my @row;
  my $cells      = $rows->[$row]->{'cells'};   my $cells      = $rows->[$row]->{'cells'};
Line 715  sub generate { Line 878  sub generate {
  my $startcol   = 1;   my $startcol   = 1;
  my @underlines; # Array of \cline cells if cellborder on.   my @underlines; # Array of \cline cells if cellborder on.
   
   
   
  for (my $cell  = 0; $cell < $cell_count; $cell++) {   for (my $cell  = 0; $cell < $cell_count; $cell++) {
     my $contents = $cells->[$cell]->{'contents'};      my $contents = $cells->[$cell]->{'contents'};
   
Line 732  sub generate { Line 897  sub generate {
     # Create the horizontal alignment character:      # Create the horizontal alignment character:
   
     my $col_align = 'l';      my $col_align = 'l';
       my $embeddedAlignStart = "";
       my $embeddedAlignEnd   = "";
   
     if ($halign eq 'right') {      if ($halign eq 'right') {
  $col_align = 'r';   $col_align = 'r';
                   $embeddedAlignStart = '\raggedleft';
     }      }
     if ($halign eq 'center') {      if ($halign eq 'center') {
  $col_align = 'c';   $col_align = 'c';
    $embeddedAlignStart = '\begin{center}';
    $embeddedAlignEnd   = '\end{center}';
     }      }
     if ($inner_border || ($outer_border && ($cell == 0))) {  
       # 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;   $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.'|';   $col_align = $col_align.'|';
     }      }
   
Line 749  sub generate { Line 936  sub generate {
   
     my $cspan    = $cells->[$cell]->{'colspan'};      my $cspan    = $cells->[$cell]->{'colspan'};
     my $nextcol  = $startcol + $cspan;      my $nextcol  = $startcol + $cspan;
     $contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';  
     if ($inner_border && ($cells->[$cell]->{'rowspan'} == 1)) {      # 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;   my $lastcol = $nextcol -1;
  push(@underlines, "\\cline{$startcol-$lastcol}");   push(@underlines, "\\cline{$startcol-$lastcol}");
     }      }
     $startcol = $nextcol;      $startcol = $nextcol;
     # Rowspans should take care of themselves.      # Rowspans should take care of themselves.
           
   
     push(@row, $contents);      push(@row, $contents);
   
  }   }
  push(@data, \@row);   push(@data, \@row);
  if ($inner_border) {   if ($cell_ul_border) {
     for (my $i =0; $i < scalar(@underlines); $i++) {      for (my $i =0; $i < scalar(@underlines); $i++) {
  push(@data, [$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 && !$cell_ul_border) {
    push(@data, ["\\cline{1-$column_count}"]);     
   
       }
     $table->set_data(\@data);      $table->set_data(\@data);
           
     my $coldef = "";      my $coldef = "";
     if ($outer_border || $inner_border) {      if ($outer_border || $cell_lr_border) {
  $coldef .= '|';   $coldef .= '|';
     }      }
     for (my $i =0; $i < $column_count; $i++) {      for (my $i =0; $i < $column_count; $i++) {
  $coldef .= 'l';   if ($useP) {
  if ($inner_border ||       $coldef .= "p{$default_width $colunits}";
    } else {
       $coldef .= 'l';
    }
    if ($cell_lr_border || 
     ($outer_border && ($i == $column_count-1))) {      ($outer_border && ($i == $column_count-1))) {
     $coldef .= '|';      $coldef .= '|';
  }   }
Line 786  sub generate { Line 999  sub generate {
   
     # Return the table:      # Return the table:
   
       if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }
   
   
     return $table;      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.
   }
 #----------------------------------------------------------------------------  #----------------------------------------------------------------------------
 # The following methods allow for testability.  # The following methods allow for testability.
   
   
 sub get_object_attribute {  sub get_object_attribute {
     my ($self, $attribute) = @_;      my ($self, $attribute) = @_;
       if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
     return $self->{$attribute};      return $self->{$attribute};
 }  }
   
 sub get_row {  sub get_row {
     my ($self, $row) = @_;      my ($self, $row) = @_;
       if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }
   
     my $rows = $self->{'rows'};  # ref to an array....      my $rows = $self->{'rows'};  # ref to an array....
     return $rows->[$row];         # ref to the row hash for the selected row.      return $rows->[$row];         # ref to the row hash for the selected row.
 }  }
   
 #   Mandatory initialization.  #   Mandatory initialization.
 BEGIN{  BEGIN{
 }  }
   
 1;  1;
 __END__  __END__
    

Removed from v.1.7  
changed lines
  Added in v.1.16


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