Diff for /loncom/xml/lontable.pm between versions 1.3 and 1.22

version 1.3, 2008/12/01 12:25:06 version 1.22, 2014/12/15 00:52:40
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 header:  # the print generator must have added the following to the LaTeX 
 #  #
 #  \usepackage{xtab}  #  \usepackage{xtab}
 #  \usepackage{booktabs}  #  \usepackage{booktabs}
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 106  modified by this.  These configuration i Line 108  modified by this.  These configuration i
   
   my $table = lontable::new(\%config_hash)    my $table = lontable::new(\%config_hash)
   
 =over3  =over 3
   
   
 =item alignment  =item alignment
   
Line 128  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
   
   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  =back
   
 =head3 Member data  =head3 Member data
Line 164  Table caption (configurable). Line 174  Table caption (configurable).
   
 Theme desired (configurable).  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   =item row_open 
   
 True if a row is open and not yet closed.  True if a row is open and not yet closed.
Line 189  Default horizontal alignment for cells i Line 204  Default horizontal alignment for cells i
   
 Default vertical alignment for cells in this row (may be ignored).  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  =item cells
   
   
 Array of hashes where each element represents the data for a cell.  Array of hashes where each element represents the data for a cell.
 The contents of each element of this hash are described below:  The contents of each element of this hash are described below:
   
 =over 3  
   
 =item header  =item header
   
 If present, the row is a 'header' that is it was made via the  If present, the row is a 'header' that is it was made via the
Line 218  If present, indicates the number of rows Line 237  If present, indicates the number of rows
 If present indicates the number of columns this cell spans.  If present indicates the number of columns this cell spans.
 Note that a cell can span both rows and columns.  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  =item contents
   
 The contents of the cell.  The contents of the cell.
   
 =back  =back
   
 =back  
   
 =cut  =cut
   
 sub new {  sub new {
     my ($class, $configuration) = @_;      my ($class, $configuration) = @_;
   
       if ($tracing) {&Apache::lonnet::logthis("new table"); }
     #  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 239  sub new { Line 262  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           => {
       'body'     => [],
               'head'     => [],
       'foot'     => []
    },
    col_widths      => {},
    part           => 'body',     # one of 'body', 'head', 'foot'.
    colgroups      => []      # Stores information about column groups.
   
     };      };
   
     foreach my $key (keys %$configuration) {      foreach my $key (keys(%$configuration)) {
  $self->{$key} = $$configuration{$key};   $self->{$key} = $$configuration{$key};
     }      }
   
Line 277  Regardless, the current alignment is use Line 308  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;
     }      }
     return $self->{alignment};      return $self->{'alignment'};
 }  }
   
 =pod  =pod
Line 302  the final value of the outer_border requ Line 335  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;
     }      }
     return $self->{outer_border};      return $self->{'outer_border'};
 }  }
   
   
Line 317  Set or get the presence of a request for Line 352  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.
   
   =item 4 - Border around groups (colgroups as well as thead/tfoot/tbody).
   
   
   =back
   
 =head3 Examples:  =head3 Examples:
   
  my $cell_borders = $table->cell_border(); # ask if cell borders are requested.   my $cell_border = $table->cell_border(); # ask if cell borders are requested.
  $table->cell_border(1); # Request cell borders.   $table->cell_border(1); # Request cell borders.
   
 =cut  =cut
   
 sub cell_borders {  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;
     }      }
     reurn $self->{inner_border};      return $self->{'inner_border'};
 }  }
   
 =pod  =pod
Line 352  the table.  If a parameter is supplied i Line 403  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->catpion = $new_value;   $self->{'caption'} = $new_value;
     }      }
   
     return $self->caption;      return $self->{'caption'};
 }  }
   
 =pod  =pod
Line 376  will be the new theme selection. Line 428  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)) {
    $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)) {      if (defined($new_value)) {
  $self->theme = $new_value;   $self->{'width'} = $new_value;
     }      }
     return $self->theme;      return $self->{'width'}; # Could be undef.
 }  }
   
 =pod  =pod
Line 412  The default vertical alignment of the ro Line 485  The default vertical alignment of the ro
 =cut  =cut
   
 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();
     }      }
     my $row_hash = {      my $row_hash = {
  default_halign => "left",   default_halign => "left",
  default_valign => "top",   default_valign => "top",
    cell_width     =>  0,
  cells          => []   cells          => []
     };      };
   
     # Override the defaults if the config hash is present:      # Override the defaults if the config hash is present:
   
     if (defined(%config)) {      if (defined($config)) {
  foreach my $key  (keys %config) {   foreach my $key  (keys(%$config)) {
     $row_hash->{$key} = $config{$key};      $row_hash->{$key} = $config->{$key};
  }   }
     }      }
   
           
     my $rows = $self->{rows};      my $rows = $self->{'rows'}->{$self->{'part'}};
     push(@$rows, $row_hash);      push(@$rows, $row_hash);
   
     $self->row_open = 1; # Row is now open and ready for business.      $self->{"row_open"} = 1; # Row is now open and ready for business.
 }  }
   
 =pod  =pod
Line 445  Closes off a row.  Once closed, cells ca Line 520  Closes off a row.  Once closed, cells ca
   
 =head3 Examples:  =head3 Examples:
   
    $table->close_row();     $table->end_row();
   
   
 =cut  =cut
   
 sub close_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
  # cell count of any row in existence in the table:   # cell count of any row in existence in the table:
   
   
  my $row        = $self->{rows}[-1];   my $row        = $self->{'rows'}->{$self->{'part'}}->[-1];
  my $cells      = $row->{cells};   my $cells      = $row->{'cells'};
  my $raw_cell_count = scalar(@$cells);  
   
  # Need to iterate through the columns as    if ($row->{'cell_width'} > $self->{'column_count'}) {
  # colspans affect the count:      $self->{'column_count'} = $row->{'cell_width'};
  #  
  my $cell_count = 0;  
  for (my $i =0; $i < $raw_cell_count; $i++) {  
     $cell_count = $cell_count + $cells->[$i]->{colspan};  
  }  
  if ($cell_count > $self->{column_count}) {  
     $self->{column_count} = $cell_count;  
  }   }
   
  $self->row_closed;   $self->{'row_open'} = 0;;
     }      }
 }  }
   
Line 502  The default vertical alignment for text Line 570  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();
     }      }
           
     my $row = $self->{rows}[-1];      my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
     foreach my $config_item (keys %$config) {      foreach my $config_item (keys(%$config)) {
  $row->{$config_item} = $config->{$config_item};   $row->{$config_item} = $config->{$config_item};
     }      }
 }  }
Line 559  Number of rows the cell spans. Line 628  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
   
   =back 
   
 =cut  =cut
   
 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'}) {
  $self->start_row();   $self->start_row();
     }      }
       my $rows          = $self->{'rows'}->{$self->{'part'}};
     my $current_row   = $self->{rows}->[-1];      my $current_row   = $rows->[-1];
     my $current_cells = $current_row->{cells};       my $current_cells = $current_row->{'cells'}; 
       my $last_coord    = $current_row->{'cell_width'};
     # The way we handle row spans is to insert additional  
     # blank cells as needed to reach this column.  Each      #  We have to worry about row spans if there is a prior row:
     # cell that is inserted is empty, but has a row span decreased by one  
     # from the row above.  Column spans are propagated down from the row above      if (scalar(@$rows) > 1) {
     # and handled when the table's LaTeX is generated.  
     # There must be at least two rows in the row table to need to do this:   my $last_row = $rows->[-2];
    if ($last_coord < $last_row->{'cell_width'}) {
     my $row_count = scalar(@$self->{rows});      my $prior_coord       = 0;
     if ($row_count > 1) {      my $prior_cell_index  = 0;
  my $prior_row      = $self->{rows}->[-2];      while ($prior_coord <= $last_coord) {
  my $curr_colcount  = scaler(@$current_row->{cells});  
  my $prior_colcount = scaler(@$prior_row->{cells});   # Pull a cell down if it's coord matches our start coord
    # And there's a row span > 1.
  while (($curr_colcount < $prior_colcount) &&   # Having done so, we adjust our $last_coord to match the
        $prior_row->{cells}->[$curr_colcount]->{rowspan} > 1) {   # end point of the pulled down cell.
     my %cell = $prior_row->{cells}->[$curr_colcount];  
     %cell->{rowspan}--;   my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
     %cell->{contents} = "";   if (!defined($prior_cell)) {
     push(@$current_cells, \%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:      # Now we're ready to build up our cell:
   
     my $cell = {      my $cell = {
  rowspan    => 1,   rowspan    => 1,
  colspan    => 1,   colspan    => 1,
    start_col  => $last_coord,
  contents   => $text   contents   => $text
     };      };
           
     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'};
   
   
       #
       # 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'}->{$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.  #   Mandatory initialization.
   BEGIN{
   }
   
 1;  1;
 __END__  __END__
   

Removed from v.1.3  
changed lines
  Added in v.1.22


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