File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.18: download - view: text, annotated - select for diffs
Wed Apr 13 10:44:26 2011 UTC (13 years, 2 months ago) by foxr
Branches: MAIN
CVS tags: HEAD
BZ6317 - Support (W/O debug spew) rules="groups" for tables with
row groups (tbody, thead, tfoot)...also get those sections out in the
right order.

    1: # The LearningOnline Network with CAPA
    2: #  Generating TeX tables.
    3: #
    4: # $Id: lontable.pm,v 1.18 2011/04/13 10:44:26 foxr Exp $
    5: # 
    6: #
    7: # Copyright Michigan State University Board of Trustees
    8: #
    9: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
   10: #
   11: # LON-CAPA is free software; you can redistribute it and/or modify
   12: # it under the terms of the GNU General Public License as published by
   13: # the Free Software Foundation; either version 2 of the License, or
   14: # (at your option) any later version.
   15: #
   16: # LON-CAPA is distributed in the hope that it will be useful,
   17: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   18: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   19: # GNU General Public License for more details.
   20: #
   21: # You should have received a copy of the GNU General Public License
   22: # along with LON-CAPA; if not, write to the Free Software
   23: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   24: #
   25: # /home/httpd/html/adm/gpl.txt
   26: #
   27: # http://www.lon-capa.org/
   28: ## Copyright for TtHfunc and TtMfunc by Ian Hutchinson. 
   29: # TtHfunc and TtMfunc (the "Code") may be compiled and linked into 
   30: # binary executable programs or libraries distributed by the 
   31: # Michigan State University (the "Licensee"), but any binaries so 
   32: # distributed are hereby licensed only for use in the context
   33: # of a program or computational system for which the Licensee is the 
   34: # primary author or distributor, and which performs substantial 
   35: # additional tasks beyond the translation of (La)TeX into HTML.
   36: # The C source of the Code may not be distributed by the Licensee
   37: # to any other parties under any circumstances.
   38: #
   39: 
   40: # This module is a support packkage that helps londefdef generate
   41: # LaTeX tables using the Apache::lonlatextable package.  A prerequisite is that
   42: # the print generator must have added the following to the LaTeX 
   43: #
   44: #  \usepackage{xtab}
   45: #  \usepackage{booktabs}
   46: #  \usepackage{array}
   47: #  \usepackage{colortbl}
   48: #  \usepackage{xcolor}
   49: #
   50: #  These packages are installed in the packaged LaTeX distributions we know of as of
   51: #  11/24/2008
   52: #
   53: 
   54: 
   55: 
   56: package Apache::lontable;
   57: use strict;
   58: use Apache::lonlatextable;
   59: use Apache::lonnet;		# for trace logging.
   60: 
   61: 
   62: my $tracing = 0;		# Set to 1 to enable log tracing. 2 for local sub tracing.
   63: 
   64: =pod
   65: 
   66: =head1  lontable Table generation assistant for the LaTeX target
   67: 
   68: This module contains support software for generating tables in LaTeX output mode 
   69: In this implementation, we use the Apache::lonlatextable package to do the actual final formatting.
   70: Each table creates a new object.  Table objects can have global properties configured.
   71: The main operations on a table object are:
   72: 
   73: =over 3
   74: 
   75: =item start_row  
   76: 
   77: Opens a new table row.
   78: 
   79: =item end_row
   80: 
   81: Closes a table row.
   82: 
   83: =item configure_row
   84: 
   85: Modifies a configuration item in the currently open row.
   86: 
   87: =item generate
   88: 
   89: Returns the generated table string.
   90: 
   91: =item configure
   92: 
   93: Configures a table's global configuration.
   94: 
   95: =item add_cell
   96: 
   97: Add and configure a cell to the current row.6
   98: 
   99: =back
  100: 
  101: =cut
  102: 
  103: =pod
  104: 
  105: =head2 new - create a new object.
  106: 
  107: Create a new table object.  Any of the raw table configuration items can be
  108: modified by this.  These configuration items include:
  109: 
  110:   my $table = lontable::new(\%config_hash)
  111: 
  112: =over 3
  113: 
  114: 
  115: =item alignment
  116: 
  117: Table alignment.  Some table styles support this but not all.
  118: 
  119: =item tableborder
  120: 
  121: If true, a border is drawn around the table.
  122: 
  123: =item cellborder
  124: 
  125: If true, borders are drawn around the cells inside a table.
  126: 
  127: =item caption
  128: 
  129: The table caption text.
  130: 
  131: =item theme
  132: 
  133: The theme of the table to use.  Defaults to Zurich.  Themes we know about are:
  134: NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris.  Other themes can be added
  135: to the Apache::lonlatextable package, and they will become supported automatically, as theme names are
  136: not error checked.  Any use of a non-existent theme is reported by the Apache::lonlatextable package
  137: when the table text is generated.
  138: 
  139: =item width
  140: 
  141: The width of the table.   in any
  142: TeX unit measure e.g.  10.8cm  This forces the table to the
  143: tabularx environment.  It also forces the declarations for
  144: cells to be paragraph mode which supports more internal formatting.
  145: 
  146: =back
  147: 
  148: =head3 Member data
  149: 
  150: The object hash has the following members:
  151: 
  152: =over 3
  153: 
  154: =item column_count 
  155: 
  156: Maintained internally, the number of colums in the widest row.
  157: 
  158: =item alignment
  159: 
  160: Table alignment (configurable) "left", "center", or "right".
  161: 
  162: =item outer_border
  163: 
  164: True if a border should be drawn around the entire table (configurable)
  165: 
  166: =item inner_borders
  167: 
  168: True if a border should be drawn around all cells (configurable).
  169: 
  170: =item caption
  171: 
  172: Table caption (configurable).
  173: 
  174: =item theme
  175: 
  176: Theme desired (configurable).
  177: 
  178: =item width
  179: 
  180: If defined, the width of the table (should be supplied
  181: in fraction of column width e.g. .75 for 75%.
  182: 
  183: =item row_open 
  184: 
  185: True if a row is open and not yet closed.
  186: 
  187: =item rows
  188: 
  189: Array of row data. This is an array of hashes described below.
  190: 
  191: =back
  192: 
  193: =head3 Row data.
  194: 
  195: Each row of table data is an element of the rows hash array.  Hash elements are
  196: 
  197: =over 3
  198: 
  199: 
  200: =item default_halign 
  201: 0
  202: Default horizontal alignment for cells in this row.
  203: 
  204: =item default_valign
  205: 
  206: Default vertical alignment for cells in this row (may be ignored).
  207: 
  208: =item cell_width
  209:  
  210: The width of the row in cells.  This is the sum of the column spans 
  211: of the cells in the row.
  212: 
  213: =item cells
  214: 
  215: 
  216: Array of hashes where each element represents the data for a cell.
  217: The contents of each element of this hash are described below:
  218: 
  219: =item header
  220: 
  221: If present, the row is a 'header' that is it was made via the
  222: <th> tag.
  223: 
  224: =item halign
  225: 
  226: If present, overrides the row default horizontal alignment.
  227: 
  228: =item valign
  229: 
  230: if present, override the row default vertical alignment.
  231: 
  232: =item rowspan
  233: 
  234: If present, indicates the number of rows this cell spans.
  235: 
  236: =item colspan
  237: 
  238: If present indicates the number of columns this cell spans.
  239: Note that a cell can span both rows and columns.
  240: 
  241: =item start_col
  242: 
  243: The starting column of the cell in the table grid.
  244: 
  245: =item contents
  246: 
  247: The contents of the cell.
  248: 
  249: =back
  250: 
  251: 
  252: =cut
  253: 
  254: sub new {
  255:     my ($class, $configuration) = @_;
  256: 
  257:     if ($tracing) {&Apache::lonnet::logthis("new table"); }
  258:     #  Initialize the object member data with the default values
  259:     #  then override with any stuff in $configuration.
  260: 
  261:     my $self = {
  262: 	alignment      => "left",
  263: 	outer_border   => 0,
  264: 	inner_border  => 0,
  265: 	caption        => "",
  266: 	theme          => "plain",
  267: 	column_count   => 0,
  268: 	row_open       => 0,
  269: 	rows           => {
  270: 	    'body'     => [],
  271:             'head'     => [],
  272: 	    'foot'     => []
  273: 	},
  274: 	col_widths      => {},
  275: 	part           => 'body'     # one of 'body', 'head', 'foot'.
  276: 
  277:     };
  278: 
  279:     foreach my $key (keys %$configuration) {
  280: 	$self->{$key} = $$configuration{$key};
  281:     }
  282: 
  283:     bless($self, $class);
  284: 
  285:     return $self;
  286: }
  287: 
  288: 
  289: #-------------------------------------------------------------------------
  290: #
  291: #  Methods that get/set table global configuration.
  292: #
  293: 
  294: =pod
  295: 
  296: =head2 Gets/set alignment.  
  297: 
  298: If the method is passed a new alignment value, that replaces the current one.
  299: Regardless, the current alignment is used:
  300: 
  301: =head3 Examples:
  302: 
  303:  my $align = $table->alignment(); # Return current alignment
  304:  $table->alignment("center");     # Attempt centered alignment.
  305: 
  306: =cut
  307: 
  308: sub alignment {
  309:     my ($self, $new_value) = @_;
  310: 
  311:     if ($tracing) {&Apache::lonnet::logthis("alignment = $new_value");}
  312: 
  313:     if (defined($new_value)) {
  314: 	$self->{'alignment'} = $new_value;
  315:     }
  316:     return $self->{'alignment'};
  317: }
  318: 
  319: =pod
  320: 
  321: =head2 table_border
  322: 
  323: Set or get the presence of an outer border in the table.
  324: If passed a parameter, that parameter replaces the current request
  325: for or not for an outer border. Regardless, the function returns
  326: the final value of the outer_border request.
  327: 
  328: =head3 Examples:
  329: 
  330:   $table->table_border(1);      # Request an outer border.
  331:   my $outer_requested = $table->table_border();
  332: 
  333: =cut
  334: 
  335: sub table_border {
  336:     my ($self, $new_value) = @_;
  337: 
  338:     if ($tracing) {&Apache::lonnet::logthis("table_border $new_value");}
  339: 
  340:     if (defined($new_value)) {
  341: 	$self->{'outer_border'} = $new_value;
  342:     }
  343:     return $self->{'outer_border'};
  344: }
  345: 
  346: 
  347: =pod
  348: 
  349: =head2 cell_border
  350: 
  351: Set or get the presence of a request for cells to have borders
  352: drawn around them.  If a paramter is passed, it will be treated as
  353: a new value for the cell border configuration.  Regardless,the final
  354: value of that configuration parameter is returned.
  355: Valid values for the parameter are:
  356: 
  357: =over 2
  358: 
  359: =item 0 - no borders present.
  360: 
  361: =item 1 - All borders (borders around all four sides of the cell.
  362: 
  363: =item 2 - Border at top and bottom of the cell.
  364: 
  365: =item 3 - Border at the left and right sides of the cell.
  366: 
  367: =item 4 - Border around groups (colgroups as well as thead/tfoot/tbody).
  368: 
  369: 
  370: =back
  371: 
  372: =head3 Examples:
  373: 
  374:  my $cell_border = $table->cell_border(); # ask if cell borders are requested.
  375:  $table->cell_border(1);	# Request cell borders.
  376: 
  377: =cut
  378: 
  379: sub cell_border {
  380:     my ($self, $new_value) = @_;
  381:     if($tracing) {&Apache::lonnet::logthis("cell_border: $new_value"); }
  382:     if (defined($new_value)) {
  383: 	$self->{'inner_border'} = $new_value;
  384:     }
  385:     return $self->{'inner_border'};
  386: }
  387: 
  388: =pod
  389: 
  390: =head2 caption
  391: 
  392: Gets and/or sets the caption string for the table.  The caption string appears to label
  393: the table.  If a parameter is supplied it will become the new caption string.k
  394: 
  395: =head3 Examples:
  396: 
  397: 
  398:   $my caption = $table->caption();
  399:   $table->caption("This is the new table caption");
  400: 
  401: =cut
  402: 
  403: sub caption {
  404:     my ($self, $new_value) = @_;
  405: 
  406:     if($tracing) {&Apache::lonnet::logthis("caption: $new_value"); }
  407:     if (defined($new_value)) {
  408: 	$self->{'caption'} = $new_value;
  409:     }
  410: 
  411:     return $self->{'caption'};
  412: }
  413: 
  414: =pod
  415: 
  416: =head2 theme
  417: 
  418: Gets and optionally sets the table theme.  The table theme describes how the
  419: table will be typset by the table package.  If a parameter is supplied it
  420: will be the new theme selection.
  421: 
  422: =head3 Examples:
  423: 
  424:   my $theme = $table->theme();
  425:   $table->theme("Dresden");
  426: 
  427: =cut
  428: 
  429: sub theme {
  430:     my ($self, $new_value) = @_;
  431:     if($tracing) {&Apache::lonnet::logthis("theme $new_value"); }
  432:     if (defined($new_value)) {
  433: 	$self->{'theme'} = $new_value;
  434:     }
  435:     return $self->{'theme'};
  436: }
  437: 
  438: =pod
  439: 
  440: =head2 width
  441: 
  442: Gets and optionally sets the width of the table.
  443: 
  444: =head3 Examples:
  445: 
  446:  my $newwidth = $table->width("10cm");   # 10cm width returns "10cm".
  447: 
  448: =cut
  449: sub width {
  450:     my ($self, $new_value) = @_;
  451:     if($tracing) {&Apache::lonnet::logthis("width = $new_value"); }
  452: 
  453:     if (defined($new_value)) {
  454: 	$self->{'width'} = $new_value;
  455:     }
  456:     return $self->{'width'}; 	# Could be undef.
  457: }
  458: 
  459: =pod
  460: 
  461: =head2 start_row
  462: 
  463: Begins a new row in the table.  If a row is already open, that row is
  464: closed off prior to starting the new row.  Rows can have the following attributes
  465: which are specified by an optional hash passed in to this function.
  466: 
  467: =over 3
  468: 
  469: =item default_halign
  470: 
  471: The default horizontal alignment of the row. This can be "left", "center", or "right"
  472: 
  473: =item default_valign
  474: 
  475: The default vertical alignment of the row.  This can be "top", "center", or "bottom"
  476: 
  477: =back
  478: 
  479: =head3 Examples:
  480: 
  481:   $table_start_row();                  # no attributes.
  482:   $table_start({default_halign => "center",
  483:                 default_valign => "bottom"}); # Create setting the attrbutes.
  484: 
  485: =cut
  486: 
  487: sub start_row {
  488:     my ($self, $config) = @_;
  489:     if($tracing) {&Apache::lonnet::logthis("start_row"); }
  490:     if ($self->{'row_open'}) { 
  491: 	$self->end_row();
  492:     }
  493:     my $row_hash = {
  494: 	default_halign => "left",
  495: 	default_valign => "top",
  496: 	cell_width     =>  0,
  497: 	cells          => []
  498:     };
  499: 
  500:     # Override the defaults if the config hash is present:
  501: 
  502:     if (defined($config)) {
  503: 	foreach my $key  (keys %$config) {
  504: 	    $row_hash->{$key} = $config->{$key};
  505: 	}
  506:     }
  507: 
  508:     
  509:     my $rows = $self->{'rows'}->{$self->{'part'}};
  510:     push(@$rows, $row_hash);
  511: 
  512:     $self->{"row_open"} = 1;	# Row is now open and ready for business.
  513: }
  514: 
  515: =pod
  516: 
  517: =head2  end_row 
  518: 
  519: Closes off a row.  Once closed, cells cannot be added to this row again.
  520: 
  521: =head3 Examples:
  522: 
  523:    $table->end_row();
  524: 
  525: 
  526: =cut
  527: 
  528: sub end_row {
  529:     my ($self) = @_;
  530:     if($tracing) {&Apache::lonnet::logthis("end_row"); }
  531:     if ($self->{'row_open'}) {
  532: 	
  533: 	# Mostly we need to determine if this row has the maximum
  534: 	# cell count of any row in existence in the table:
  535: 	
  536: 
  537: 	my $row        = $self->{'rows'}->{$self->{'part'}}->[-1];
  538: 	my $cells      = $row->{'cells'};
  539: 
  540: 	if ($row->{'cell_width'} > $self->{'column_count'}) {
  541: 	    $self->{'column_count'} = $row->{'cell_width'};
  542: 	}
  543: 
  544: 	$self->{'row_open'} = 0;;
  545:     }
  546: }
  547: 
  548: =pod
  549: 
  550: =head2 configure_row
  551: 
  552: Modify the configuration of a row.   If a row is not open, a new one will be opened.
  553: 
  554: =head3 Parameters:
  555: 
  556: config_hash - A hash that contains new values for the set of row confiuguration 
  557: items to be modified.  There is currently no check/penalty for items that are not in
  558: the set of defined configuration properties which are:
  559: 
  560: =over 2
  561: 
  562: =item default_halign
  563: 
  564: The default horizontal alignment for text in  cells in the row.  This can be any of:
  565: "left", "right" or "center".
  566: 
  567: =item default_valign
  568: 
  569: The default vertical alignment for text in cells in the row.  This can be any of:
  570: 
  571: "top", "bottom" or "center"
  572: 
  573: 
  574: =back 
  575: 
  576: =cut
  577: 
  578: sub configure_row {
  579:     my ($self, $config) = @_;
  580:     if($tracing) {&Apache::lonnet::logthis("configure_row");}
  581:     if (!$self->{'row_open'}) {
  582: 	$self->start_row();
  583:     }
  584:     
  585:     my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
  586:     foreach my $config_item (keys %$config) {
  587: 	$row->{$config_item} = $config->{$config_item};
  588:     }
  589: }
  590: 
  591: 
  592: =pod
  593: 
  594: =head2 add_cell
  595: 
  596: Add a new cell to a row.  If there is a row above us, we need to 
  597: watch out for row spans that may force additional blank cell entries
  598: to fill in the span. 
  599: 
  600: =head3 Parameters:
  601: 
  602: =over 2
  603: 
  604: =item text
  605: 
  606: Text to put in the cell.
  607: 
  608: =item cell_config
  609: 
  610: Hash of configuration options that override the defaults.   The recognized options,
  611: and their defaults are:
  612: 
  613: =over 2
  614: 
  615: =item halign 
  616: 
  617: If nonblank overrides the row's default for the cell's horizontal alignment.
  618: 
  619: =item valign
  620: 
  621: If nonblank, overrides the row's default for the cdell's vertical alignment.
  622: 
  623: =item rowspan
  624: 
  625: Number of rows the cell spans.
  626: 
  627: =item colspan
  628: 
  629: Number of columns the cell spans.
  630: 
  631: =item width
  632: 
  633: LaTeX specification of the width of the cell.
  634: Note that if there is a colspan this width is going to be equally divided
  635: over the widths of the columnsn in the span.
  636: Note as well that if width specification conflict, the last one specified wins...silently.
  637: 
  638: =back
  639: 
  640: =back 
  641: 
  642: =cut
  643: 
  644: sub add_cell {
  645:     my ($self, $text, $config) = @_;
  646: 
  647:     if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); }
  648: 
  649:     # If a row is not open, we must open it:
  650: 
  651:     if (!$self->{'row_open'}) {
  652: 	$self->start_row();
  653:     }
  654:     my $rows          = $self->{'rows'}->{$self->{'part'}};
  655:     my $current_row   = $rows->[-1];
  656:     my $current_cells = $current_row->{'cells'}; 
  657:     my $last_coord    = $current_row->{'cell_width'};
  658: 
  659:     #  We have to worry about row spans if there is a prior row:
  660: 
  661:     if (scalar(@$rows) > 1) {
  662: 
  663: 	my $last_row = $rows->[-2];
  664: 	if ($last_coord < $last_row->{'cell_width'}) {
  665: 	    my $prior_coord       = 0;
  666: 	    my $prior_cell_index  = 0;
  667: 	    while ($prior_coord <= $last_coord) {
  668: 		
  669: 		# Pull a cell down if it's coord matches our start coord
  670: 		# And there's a row span > 1.
  671: 		# Having done so, we adjust our $last_coord to match the
  672: 		# end point of the pulled down cell.
  673: 
  674: 		my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
  675: 		if (!defined($prior_cell)) {
  676: 		    last;
  677: 		}
  678: 		if (($prior_cell->{'start_col'} == $last_coord) &&
  679: 		    ($prior_cell->{'rowspan'}  > 1)) {
  680: 		    
  681: 		    #  Need to drop the cell down
  682: 
  683: 		    my %dropped_down_cell = %$prior_cell;
  684: 		    $dropped_down_cell{'rowspan'}--;
  685: 		    $dropped_down_cell{'contents'} = '';
  686: 
  687: 		    push(@$current_cells, \%dropped_down_cell);
  688: 		    $last_coord += $dropped_down_cell{'colspan'};
  689: 		    $current_row->{'cell_width'} = $last_coord;
  690: 		    
  691: 		}
  692: 		$prior_coord += $prior_cell->{'colspan'};
  693: 		$prior_cell_index++;
  694: 	    }
  695: 	}
  696: 
  697:     }
  698: 
  699:     #
  700:     # Now we're ready to build up our cell:
  701: 
  702:     my $cell = {
  703: 	rowspan    => 1,
  704: 	colspan    => 1,
  705: 	start_col  => $last_coord,
  706: 	contents   => $text
  707:     };
  708:     
  709:     if (defined($config)) {
  710: 	foreach my $key (keys(%$config)) {
  711:             if ($key eq 'colspan') {
  712:                 next if ($config->{$key} == 0);
  713:             }
  714: 	    $cell->{$key} = $config->{$key};
  715: 	}
  716:     }
  717: 
  718:     $current_row->{'cell_width'} += $cell->{'colspan'};
  719: 
  720: 
  721:     #
  722:     # Process the width if it exists.  If supplied it must be of the form:
  723:     #   float units
  724:     # Where units can be in, cm or mm.
  725:     # Regardless of the supplied units we will normalize to cm.
  726:     # This allows computation on units at final table generation time.
  727:     #
  728: 
  729:     if (exists($cell->{'width'})) {
  730: 	my $width;
  731: 	my $widthcm;
  732: 	$width   = $config->{'width'};
  733: 	$widthcm = $self->size_to_cm($width);
  734: 	
  735: 	# If there's a column span, the actual width is divided by the span
  736: 	# and applied to each of the columns in the span.
  737: 
  738: 	$widthcm = $widthcm / $cell->{'colspan'};
  739: 	for (my $i = $last_coord; $i < $last_coord + $cell->{'colspan'}; $i++) {
  740: 	    $self->{'col_widths'}->{$i} = $widthcm; 
  741: 	}
  742: 	
  743:     }
  744: 
  745:     push(@$current_cells, $cell);
  746: 
  747:     if ($tracing) { &Apache::lonnet::logthis("add_cell done"); }
  748: }
  749: 
  750: 
  751: =pod
  752: 
  753: =head2  append_cell_text
  754: 
  755: Sometimes it's necessary to create/configure the cell and then later add text to it.
  756: This sub allows text to be appended to the most recently created cell.
  757: 
  758: =head3 Parameters
  759: 
  760: The text to add to the cell.
  761: 
  762: =cut
  763: sub append_cell_text {
  764:     my ($this, $text) = @_;
  765: 
  766:     if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); }
  767:     my $rows         = $this->{'rows'}->{$this->{'part'}};
  768:     my $current_row  = $rows->[-1];
  769:     my $cells        = $current_row->{'cells'};
  770:     my $current_cell = $cells->[-1];
  771:     $current_cell->{'contents'} .= $text;
  772:     
  773: }
  774: #-------------------------- Support for row/column groups.   ----
  775: 
  776: =pod 
  777: 
  778: =head2 start_head 
  779: 
  780: starts the table head.  This corresponds to the <thead> tag in 
  781: html/xml.  All rows defined in this group will be
  782: collected and placed at the front of the table come rendering time.
  783: Furthermore, if the table has group borders enabled, a rule will be
  784: rendered following and preceding this group of rows.
  785: 
  786: =cut
  787: 
  788: sub start_head {
  789:     my ($this) = @_;
  790:     if ($tracing) { &Apache::lonnet::logthis("start_head"); }
  791:     $this->{'part'}  = 'head';
  792: }
  793: 
  794: =pod     
  795: 
  796: =head2 end_head   
  797: 
  798: Ends a table head.  This corresponds to the
  799: </thead> closing tag in html/xml.
  800: 
  801: =cut
  802: 
  803: sub end_head {
  804:     my ($this) = @_;
  805:     if ($tracing) { &Apache::lonnet::logthis("end_head"); }
  806:     $this->{'part'} = 'body';
  807: }
  808: 
  809: =pod
  810: 
  811: =head2 start_foot
  812: 
  813: Starts the table footer.  All of the rows generated in the footer will
  814: be rendered at the bottom of the table.  This sub corresponds to the
  815: <tfoot> tag in html/xml.  If the table has group borders enabled, a rule
  816: will be rendered at the top and bottom of the set of columns in this
  817: group
  818: 
  819: =cut
  820: 
  821: sub start_foot {
  822:     my ($this) = @_;
  823:     if ($tracing) { &Apache::lonnet::logthis("start_foot"); }
  824:     $this->{'part'}   = 'foot';
  825: }
  826: 
  827: =pod
  828: 
  829: =head2 end_foot
  830: 
  831: Ends the set of rows in the table footer.  This corresponds to the
  832: </tfoot> end tag in xml/html.
  833: 
  834: =cut
  835: 
  836: sub end_foot {
  837:     my ($this) = @_;
  838:     if ($tracing) { &Apache::lonnet::logthis("end_foot") }
  839:     $this->{'part'}  = 'body';
  840: }
  841: 
  842: =pod
  843: 
  844: =head2 start_body
  845: 
  846: Starts the set of rows that will be in the table body.   Note that if
  847: we are not in the header or footer, body rows are implied.
  848: This correspondes to the presence of a <tbody> tag in html/xml.
  849: If group borders are on, a rule will be rendered at the top and bottom
  850: of the body rows.
  851: 
  852: =cut
  853: 
  854: sub start_body {
  855:     my ($this) = @_;
  856:     if ($tracing) { &Apache::lonnet::logthis("start_body"); }
  857:     $this->{'part'}  = 'body';
  858: }
  859: 
  860: =pod
  861:  
  862: =head2 end_body
  863: 
  864: Ends the set of rows in a table body.  Note that in the event we are not
  865: in  the header or footer groups this code assumes we are in the body
  866: group.  I believe this is a good match to how mot browsers render.
  867: 
  868: =cut
  869: 
  870: sub end_body {
  871:     my ($this) = @_;
  872:     if ($tracing) { &Apache::lonnet::logthis("end_body"); }
  873: 
  874: }
  875: 
  876: =pod
  877: 
  878: =head2 define_colgroup
  879: 
  880: Define a column group  a column group corresponds to the
  881: <cgroup> tag in Html/Xml. A column group as we implement it has
  882: the following properties tht will be shared amongst all cells in the
  883: columns in the group unless overidden in the specific oell definition:
  884: 
  885: =over 2
  886: 
  887: =item span 
  888: 
  889: The number of columns in the column group.  This defaults to 1.
  890: 
  891: =item halign
  892: 
  893: Horizontal alignment of the cells.  This defaults to left.
  894: Other values are left, center, right (justify and char are 
  895: accepted but treated as left).
  896:   
  897: =item valign
  898: 
  899: Vertical alignment of the cells.  This defaults to middle.
  900: Other values are top middle, bottom, (baseline is accepted and
  901: treated as top).
  902: 
  903: =back   
  904: 
  905: If group borders are turned on, a rule will be rendered
  906: at the left and right side of the column group.
  907: 
  908: =head3 parameters
  909: 
  910: =over 2
  911: 
  912: =item definition
  913: 
  914: This is a hash that contains any of the keys described above that
  915: define the column group.
  916: 
  917: =back
  918: 
  919: 
  920: =head3 Example
  921: 
  922:  $table->define_colgroup({
  923:     'span'    => 2,
  924:     'halign'  => 'center'
  925:                          })
  926: 
  927: 
  928: 
  929: =cut
  930: 
  931: sub define_colgroup {
  932:     my ($this, $attributes)  = @_;
  933:     if ($tracing) { &Apache::lonnet::logthis("col_group"); }
  934:     
  935: 
  936: }
  937: 
  938: #------------------------- Render the table ---------------------
  939: 
  940: =pod
  941: 
  942: =head2 generate
  943: 
  944: Call this when the structures for the table have been built.
  945: This will generate and return the table object that can be used
  946: to generate the table.  Returning the table object allows for
  947: a certain amount of testing to be done on the generated table.
  948: The caller can then ask the table object to generate LaTeX.
  949: 
  950: =cut
  951: 
  952: sub generate {
  953:     my ($this) = @_;
  954:     my $useP   = 0;
  955: 
  956:     my $colunits = 'cm';	# All widths get normalized to cm.
  957:     my $tablewidth;
  958: 
  959:     if($tracing) {&Apache::lonnet::logthis("generate"); }
  960:     my $table = Apache::lonlatextable->new();
  961: 
  962:     my $inner_border = $this->{'inner_border'};
  963:     my $outer_border = $this->{'outer_border'};
  964:     my $column_count = $this->{'column_count'};
  965: 
  966:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
  967:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
  968:     my $part_border   = ($inner_border == 4);
  969:  
  970:     # Add the caption if supplied.
  971: 
  972:     if ($this->{'caption'} ne "") {
  973: 	$table->set_caption($this->caption);
  974:     }
  975:     
  976:     # Set the width if defined:
  977: 
  978:     my $default_width;
  979:     my $colwidths        = $this->{'col_widths'};
  980:     if (defined ($this->{'width'})) {
  981: 	$tablewidth = $this->{'width'};
  982: 	$tablewidth = $this->size_to_cm($tablewidth);
  983: 
  984: 	$useP = 1;
  985: 
  986: 	# Figure out the default width for a column with unspecified
  987: 	# We take the initially specified widths and sum them up.
  988: 	# This is subtracted from total width  above.
  989: 	# If the result is negative we're going to allow a minimum of 2.54cm for
  990: 	# each column and make the table spill appropriately.  
  991: 	# This (like a riot) is an ugly thing but I'm open to suggestions about
  992: 	# how to handle it better (e.g. scaling down requested widths?).
  993: 
  994: 	my $specified_width = 0.0;
  995: 	my $specified_cols   = 0;
  996: 	foreach my $col (keys %$colwidths) {
  997: 	    $specified_width = $specified_width + $colwidths->{$col};
  998: 	    $specified_cols++;
  999: 	}
 1000: 	my $unspecified_cols = $this->{'column_count'} - $specified_cols;
 1001: 
 1002: 	#  If zero unspecified cols, we are pretty much done... just have to
 1003: 	#  adjust the total width to be specified  width. Otherwise we
 1004: 	#  must figure out the default width and total width:
 1005: 	#
 1006: 	my $total_width;
 1007: 	if($unspecified_cols == 0) {
 1008: 	    $total_width = $specified_width;
 1009: 	} else {
 1010: 	    $default_width = ($tablewidth - $specified_width)/$unspecified_cols; #  Could be negative....
 1011: 	    $total_width   = $default_width * $unspecified_cols + $specified_width;
 1012: 	}
 1013: 	
 1014: 	# if the default_width is < 0.0 the user has oversubscribed the width of the table with the individual
 1015: 	# column.  In this case, we're going to maintain the desired proportions of the user's columns, but 
 1016: 	# ensure that the unspecified columns get a fair share of the width..where a fair share is defined as
 1017: 	# the total width of the table / unspecified column count.
 1018: 	# We figure out what this means in terms of reducing the specified widths by dividing by a constant proportionality.
 1019: 	# Note that this cannot happen if the user hasn't specified anywidths as the computation above would then
 1020: 	# just make all columns equal fractions of the total table width.
 1021: 
 1022: 	if ($default_width < 0) {
 1023: 	    $default_width = ($tablewidth/$unspecified_cols);                     # 'fair' default width.
 1024: 	    my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols.
 1025: 	    my $reduction       = $tablewidth/$width_remaining;                    # Reduction fraction for specified cols
 1026: 	    foreach my $col (keys %$colwidths) {
 1027: 		$colwidths->{$col} = $colwidths->{$col}/$reduction;
 1028: 	    }
 1029: 	    
 1030:         }
 1031:     }
 1032: 
 1033:     if ($tracing) { &Apache::lonnet::logthis("rendering head"); }
 1034:     $this->render_part('head', $table, $useP, $default_width);
 1035:     if ($tracing) { &Apache::lonnet::logthis("rendering body"); }
 1036:     $this->render_part('body', $table, $useP, $default_width);
 1037:     if ($tracing) { &Apache::lonnet::logthis("rendering footer"); }
 1038:     $this->render_part('foot', $table, $useP, $default_width);
 1039: 
 1040: 
 1041: 
 1042: 
 1043:     
 1044:     my $coldef = "";
 1045:     if ($outer_border || $cell_lr_border) {
 1046: 	$coldef .= '|';
 1047:     }
 1048:     for (my $i =0; $i < $column_count; $i++) {
 1049: 	if ($useP) {
 1050: 	    $coldef .= "p{$default_width $colunits}";
 1051: 	} else {
 1052: 	    $coldef .= 'l';
 1053: 	}
 1054: 	if ($cell_lr_border || 
 1055: 	    ($outer_border && ($i == $column_count-1))) {
 1056: 	    $coldef .= '|';
 1057: 	}
 1058:     }
 1059:     $table->{'coldef'} = $coldef;
 1060: 
 1061:     # Return the table:
 1062: 
 1063:     if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }
 1064: 
 1065: 
 1066:     return $table;
 1067: 
 1068: }
 1069: 
 1070: 
 1071: #---------------------------------------------------------------------------
 1072: #
 1073: #  Private methods:
 1074: #
 1075: 
 1076: # 
 1077: # Convert size with units -> size in cm.
 1078: # The resulting size is floating point with no  units so that it can be used in
 1079: # computation.  Note that an illegal or missing unit is treated silently as
 1080: #  cm for now.
 1081: #
 1082: sub size_to_cm {
 1083:     my ($this, $size_spec) = @_;
 1084:     my ($size, $units) = split(/ /, $size_spec);
 1085:     if (lc($units) eq 'mm') {
 1086: 	return $size / 10.0;
 1087:     }
 1088:     if (lc($units) eq 'in') {
 1089: 	return $size * 2.54;
 1090:     }
 1091:     
 1092:     return $size;		# Default is cm.
 1093: }
 1094: 
 1095: #
 1096: #  Render a part of the table.  The valid table parts are
 1097: #  head, body and foot.  These corresopnd to the set of rows
 1098: #  define within <thead></thead>, <tbody></tbody> and <tfoot></tfoot>
 1099: #  respectively.
 1100: #
 1101: sub render_part {
 1102:     my ($this, $part, $table, $useP, $default_width) = @_;
 1103: 
 1104:     if ($tracing) { &Apache::lonnet::logthis("render_part: $part") };
 1105: 
 1106:     # Do nothing if that part of the table is empty:
 1107: 
 1108:     if ($this->{'rows'}->{$part} == undef) {
 1109: 	if ($tracing) {&Apache::lonnet::logthis("$part is empty"); }
 1110: 	return;
 1111:     }
 1112: 
 1113: 
 1114:     # Build up the data:
 1115: 
 1116:     my @data;
 1117:     my $colwidths        = $this->{'col_widths'};
 1118:     my $rows      = $this->{'rows'}->{$part}; # TODO: Render header, body footer as groups.
 1119:     my $row_count = scalar(@$rows);
 1120:     my $inner_border = $this->{'inner_border'};
 1121:     my $outer_border = $this->{'outer_border'};
 1122:     my $column_count = $this->{'column_count'};
 1123: 
 1124:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
 1125:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
 1126:     my $part_border   = ($inner_border == 4);
 1127:     my $colunits    = 'cm';	# All units in cm.
 1128: 
 1129:     # Add a top line if the outer or inner border is enabled:
 1130:     # or if group rules are on.
 1131:     #
 1132: 
 1133:     if ($outer_border || $cell_ul_border || $part_border) {
 1134: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1135: 
 1136:     }
 1137: 
 1138:     for (my $row = 0; $row < $row_count; $row++) {
 1139: 	my @row;
 1140: 	my $cells      = $rows->[$row]->{'cells'};
 1141: 	my $def_halign = $rows->[$row]->{'default_halign'};
 1142: 	my $cell_count = scalar(@$cells);
 1143: 	my $startcol   = 1;
 1144: 	my @underlines;		# Array of \cline cells if cellborder on.
 1145: 
 1146: 
 1147: 
 1148: 	for (my $cell  = 0; $cell < $cell_count; $cell++) {
 1149: 	    my $contents = $cells->[$cell]->{'contents'};
 1150: 
 1151: 	    #
 1152: 	    #  Cell alignment is the default alignment unless
 1153: 	    #  explicitly specified in the cell.
 1154: 	    #  NOTE: at this point I don't know how to do vert alignment.
 1155: 	    #
 1156: 
 1157: 	    my $halign   = $def_halign;
 1158: 	    if (defined ($cells->[$cell]->{'halign'})) {
 1159: 		$halign = $cells->[$cell]->{'halign'};
 1160: 	    }
 1161: 
 1162: 	    # Create the horizontal alignment character:
 1163: 
 1164: 	    my $col_align = 'l';
 1165: 	    my $embeddedAlignStart = "";
 1166: 	    my $embeddedAlignEnd   = "";
 1167: 
 1168: 	    if ($halign eq 'right') {
 1169: 		$col_align = 'r';
 1170:                 $embeddedAlignStart = '\raggedleft';
 1171: 	    }
 1172: 	    if ($halign eq 'center') {
 1173: 		$col_align = 'c';
 1174: 		$embeddedAlignStart = '\begin{center}';
 1175: 		$embeddedAlignEnd   = '\end{center}';
 1176: 	    }
 1177: 
 1178: 	    # If the width has been specified, turn these into
 1179: 	    # para mode; and wrap the contents in the start/stop stuff:
 1180: 
 1181: 	    if ($useP) {
 1182: 		my $cw;
 1183: 		if (defined($colwidths->{$cell})) {
 1184: 		    $cw = $colwidths->{$cell};
 1185: 		} else {
 1186: 		    $cw = $default_width;
 1187: 		}
 1188: 		$cw = $cw * $cells->[$cell]->{'colspan'};
 1189: 		$col_align = "p{$cw $colunits}";
 1190: 		$contents = $embeddedAlignStart . $contents .  $embeddedAlignEnd;
 1191: 	    }
 1192: 
 1193: 	    if ($cell_lr_border || ($outer_border && ($cell == 0))) {
 1194: 		$col_align = '|'.$col_align;
 1195: 	    }
 1196: 	    if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) {
 1197: 		$col_align = $col_align.'|';
 1198: 	    }
 1199: 
 1200: 	    #factor in spans:
 1201: 
 1202: 	    my $cspan    = $cells->[$cell]->{'colspan'};
 1203: 	    my $nextcol  = $startcol + $cspan;
 1204: 
 1205: 	    # If we can avoid the \multicolumn directive that's best as
 1206: 	    # that makes some things like \parpic invalid in LaTeX which
 1207:             # screws everything up.
 1208: 
 1209: 	    if (($cspan > 1) || !($col_align =~ /l/)) {
 1210: 
 1211: 		$contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';
 1212: 
 1213: 		# A nasty edge case.  If there's only one cell, the software will assume
 1214: 		# we're in complete control of the row so we need to end the row ourselves.
 1215: 		
 1216: 		if ($cell_count == 1) {
 1217: 		    $contents .= '  \\\\';
 1218: 		}
 1219: 	    }
 1220: 	    if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) {
 1221: 		my $lastcol = $nextcol -1;
 1222: 		push(@underlines, "\\cline{$startcol-$lastcol}");
 1223: 	    }
 1224: 	    $startcol = $nextcol;
 1225: 	    # Rowspans should take care of themselves.
 1226: 	    
 1227: 	    push(@row, $contents);
 1228: 
 1229: 	}
 1230: 	push(@data, \@row);
 1231: 	if ($cell_ul_border) {
 1232: 	    for (my $i =0; $i < scalar(@underlines); $i++) {
 1233: 		push(@data, [$underlines[$i]]);
 1234: 	    }
 1235: 	}
 1236: 
 1237:     }
 1238:     #
 1239:     # Add bottom border if necessary: if the inner border was on, the loops above
 1240:     # will have done a bottom line under the last cell.
 1241:     #
 1242:     if (($outer_border || $part_border) && !$cell_ul_border) {
 1243: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1244: 
 1245:     }
 1246:     $table->set_data(\@data);    
 1247: }
 1248: 
 1249: #----------------------------------------------------------------------------
 1250: # The following methods allow for testability.
 1251: 
 1252: 
 1253: sub get_object_attribute {
 1254:     my ($self, $attribute) = @_;
 1255:     if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
 1256:     return $self->{$attribute};
 1257: }
 1258: 
 1259: sub get_row {
 1260:     my ($self, $row) = @_;
 1261:     if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }
 1262: 
 1263:     my $rows = $self->{'rows'}->{$self->{'part'}};	  # ref to an array....
 1264:     return $rows->[$row];         # ref to the row hash for the selected row.
 1265: }
 1266: 
 1267: #   Mandatory initialization.
 1268: BEGIN{
 1269: }
 1270: 
 1271: 1;
 1272: __END__
 1273: 

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