File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.17: download - view: text, annotated - select for diffs
Wed Apr 13 10:08:06 2011 UTC (13 years, 2 months ago) by foxr
Branches: MAIN
CVS tags: HEAD
BZ 6317 - NOTE This commit contains over-extensive debugging output and should not be used in production.
- This commit includes initial work to support the rule="groups" option in
<table> for printing.  Commit because some drastic re-factoring and some data
structure re-engineering was requuired to capture some of the group information.
Anticipate the next commit will support <thead><tbody><tfoot> properly
both with respect to the order in which they appear in the output and
with respect to rules="groups"...if this version does not already do that
correctly...at that time the debug spew will be removed as well.

    1: # The LearningOnline Network with CAPA
    2: #  Generating TeX tables.
    3: #
    4: # $Id: lontable.pm,v 1.17 2011/04/13 10:08:06 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: use Data::Dumper;
   61: 
   62: my $tracing = 1;		# 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: 	&Apache::lonnet::logthis($self->{'part'});
  537: 	&Apache::lonnet::logthis(Dumper($self->{'rows'}));
  538: 	my $row        = $self->{'rows'}->{$self->{'part'}}->[-1];
  539: 	my $cells      = $row->{'cells'};
  540: 
  541: 	if ($row->{'cell_width'} > $self->{'column_count'}) {
  542: 	    $self->{'column_count'} = $row->{'cell_width'};
  543: 	}
  544: 
  545: 	$self->{'row_open'} = 0;;
  546:     }
  547: }
  548: 
  549: =pod
  550: 
  551: =head2 configure_row
  552: 
  553: Modify the configuration of a row.   If a row is not open, a new one will be opened.
  554: 
  555: =head3 Parameters:
  556: 
  557: config_hash - A hash that contains new values for the set of row confiuguration 
  558: items to be modified.  There is currently no check/penalty for items that are not in
  559: the set of defined configuration properties which are:
  560: 
  561: =over 2
  562: 
  563: =item default_halign
  564: 
  565: The default horizontal alignment for text in  cells in the row.  This can be any of:
  566: "left", "right" or "center".
  567: 
  568: =item default_valign
  569: 
  570: The default vertical alignment for text in cells in the row.  This can be any of:
  571: 
  572: "top", "bottom" or "center"
  573: 
  574: 
  575: =back 
  576: 
  577: =cut
  578: 
  579: sub configure_row {
  580:     my ($self, $config) = @_;
  581:     if($tracing) {&Apache::lonnet::logthis("configure_row");}
  582:     if (!$self->{'row_open'}) {
  583: 	$self->start_row();
  584:     }
  585:     
  586:     my $row = $self->{'rows'}->{$self->{'part'}}->[-1];
  587:     foreach my $config_item (keys %$config) {
  588: 	$row->{$config_item} = $config->{$config_item};
  589:     }
  590: }
  591: 
  592: 
  593: =pod
  594: 
  595: =head2 add_cell
  596: 
  597: Add a new cell to a row.  If there is a row above us, we need to 
  598: watch out for row spans that may force additional blank cell entries
  599: to fill in the span. 
  600: 
  601: =head3 Parameters:
  602: 
  603: =over 2
  604: 
  605: =item text
  606: 
  607: Text to put in the cell.
  608: 
  609: =item cell_config
  610: 
  611: Hash of configuration options that override the defaults.   The recognized options,
  612: and their defaults are:
  613: 
  614: =over 2
  615: 
  616: =item halign 
  617: 
  618: If nonblank overrides the row's default for the cell's horizontal alignment.
  619: 
  620: =item valign
  621: 
  622: If nonblank, overrides the row's default for the cdell's vertical alignment.
  623: 
  624: =item rowspan
  625: 
  626: Number of rows the cell spans.
  627: 
  628: =item colspan
  629: 
  630: Number of columns the cell spans.
  631: 
  632: =item width
  633: 
  634: LaTeX specification of the width of the cell.
  635: Note that if there is a colspan this width is going to be equally divided
  636: over the widths of the columnsn in the span.
  637: Note as well that if width specification conflict, the last one specified wins...silently.
  638: 
  639: =back
  640: 
  641: =back 
  642: 
  643: =cut
  644: 
  645: sub add_cell {
  646:     my ($self, $text, $config) = @_;
  647: 
  648:     if($tracing) {&Apache::lonnet::logthis("add_cell : $text"); }
  649: 
  650:     # If a row is not open, we must open it:
  651: 
  652:     if (!$self->{'row_open'}) {
  653: 	$self->start_row();
  654:     }
  655:     my $rows          = $self->{'rows'}->{$self->{'part'}};
  656:     my $current_row   = $rows->[-1];
  657:     my $current_cells = $current_row->{'cells'}; 
  658:     my $last_coord    = $current_row->{'cell_width'};
  659: 
  660:     #  We have to worry about row spans if there is a prior row:
  661: 
  662:     if (scalar(@$rows) > 1) {
  663: 
  664: 	my $last_row = $rows->[-2];
  665: 	if ($last_coord < $last_row->{'cell_width'}) {
  666: 	    my $prior_coord       = 0;
  667: 	    my $prior_cell_index  = 0;
  668: 	    while ($prior_coord <= $last_coord) {
  669: 		
  670: 		# Pull a cell down if it's coord matches our start coord
  671: 		# And there's a row span > 1.
  672: 		# Having done so, we adjust our $last_coord to match the
  673: 		# end point of the pulled down cell.
  674: 
  675: 		my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
  676: 		if (!defined($prior_cell)) {
  677: 		    last;
  678: 		}
  679: 		if (($prior_cell->{'start_col'} == $last_coord) &&
  680: 		    ($prior_cell->{'rowspan'}  > 1)) {
  681: 		    
  682: 		    #  Need to drop the cell down
  683: 
  684: 		    my %dropped_down_cell = %$prior_cell;
  685: 		    $dropped_down_cell{'rowspan'}--;
  686: 		    $dropped_down_cell{'contents'} = '';
  687: 
  688: 		    push(@$current_cells, \%dropped_down_cell);
  689: 		    $last_coord += $dropped_down_cell{'colspan'};
  690: 		    $current_row->{'cell_width'} = $last_coord;
  691: 		    
  692: 		}
  693: 		$prior_coord += $prior_cell->{'colspan'};
  694: 		$prior_cell_index++;
  695: 	    }
  696: 	}
  697: 
  698:     }
  699: 
  700:     #
  701:     # Now we're ready to build up our cell:
  702: 
  703:     my $cell = {
  704: 	rowspan    => 1,
  705: 	colspan    => 1,
  706: 	start_col  => $last_coord,
  707: 	contents   => $text
  708:     };
  709:     
  710:     if (defined($config)) {
  711: 	foreach my $key (keys(%$config)) {
  712:             if ($key eq 'colspan') {
  713:                 next if ($config->{$key} == 0);
  714:             }
  715: 	    $cell->{$key} = $config->{$key};
  716: 	}
  717:     }
  718: 
  719:     $current_row->{'cell_width'} += $cell->{'colspan'};
  720: 
  721: 
  722:     #
  723:     # Process the width if it exists.  If supplied it must be of the form:
  724:     #   float units
  725:     # Where units can be in, cm or mm.
  726:     # Regardless of the supplied units we will normalize to cm.
  727:     # This allows computation on units at final table generation time.
  728:     #
  729: 
  730:     if (exists($cell->{'width'})) {
  731: 	my $width;
  732: 	my $widthcm;
  733: 	$width   = $config->{'width'};
  734: 	$widthcm = $self->size_to_cm($width);
  735: 	
  736: 	# If there's a column span, the actual width is divided by the span
  737: 	# and applied to each of the columns in the span.
  738: 
  739: 	$widthcm = $widthcm / $cell->{'colspan'};
  740: 	for (my $i = $last_coord; $i < $last_coord + $cell->{'colspan'}; $i++) {
  741: 	    $self->{'col_widths'}->{$i} = $widthcm; 
  742: 	}
  743: 	
  744:     }
  745: 
  746:     push(@$current_cells, $cell);
  747: 
  748:     if ($tracing) { &Apache::lonnet::logthis("add_cell done"); }
  749: }
  750: 
  751: 
  752: =pod
  753: 
  754: =head2  append_cell_text
  755: 
  756: Sometimes it's necessary to create/configure the cell and then later add text to it.
  757: This sub allows text to be appended to the most recently created cell.
  758: 
  759: =head3 Parameters
  760: 
  761: The text to add to the cell.
  762: 
  763: =cut
  764: sub append_cell_text {
  765:     my ($this, $text) = @_;
  766: 
  767:     if($tracing) {&Apache::lonnet::logthis("append_cell_text: $text"); }
  768:     my $rows         = $this->{'rows'}->{$this->{'part'}};
  769:     my $current_row  = $rows->[-1];
  770:     my $cells        = $current_row->{'cells'};
  771:     my $current_cell = $cells->[-1];
  772:     $current_cell->{'contents'} .= $text;
  773:     
  774: }
  775: #-------------------------- Support for row/column groups.   ----
  776: 
  777: =pod 
  778: 
  779: =head2 start_head 
  780: 
  781: starts the table head.  This corresponds to the <thead> tag in 
  782: html/xml.  All rows defined in this group will be
  783: collected and placed at the front of the table come rendering time.
  784: Furthermore, if the table has group borders enabled, a rule will be
  785: rendered following and preceding this group of rows.
  786: 
  787: =cut
  788: 
  789: sub start_head {
  790:     my ($this) = @_;
  791:     if ($tracing) { &Apache::lonnet::logthis("start_head"); }
  792:     $this->{'part'}  = 'head';
  793: }
  794: 
  795: =pod     
  796: 
  797: =head2 end_head   
  798: 
  799: Ends a table head.  This corresponds to the
  800: </thead> closing tag in html/xml.
  801: 
  802: =cut
  803: 
  804: sub end_head {
  805:     my ($this) = @_;
  806:     if ($tracing) { &Apache::lonnet::logthis("end_head"); }
  807:     $this->{'part'} = 'body';
  808: }
  809: 
  810: =pod
  811: 
  812: =head2 start_foot
  813: 
  814: Starts the table footer.  All of the rows generated in the footer will
  815: be rendered at the bottom of the table.  This sub corresponds to the
  816: <tfoot> tag in html/xml.  If the table has group borders enabled, a rule
  817: will be rendered at the top and bottom of the set of columns in this
  818: group
  819: 
  820: =cut
  821: 
  822: sub start_foot {
  823:     my ($this) = @_;
  824:     if ($tracing) { &Apache::lonnet::logthis("start_foot"); }
  825:     $this->{'part'}   = 'foot';
  826: }
  827: 
  828: =pod
  829: 
  830: =head2 end_foot
  831: 
  832: Ends the set of rows in the table footer.  This corresponds to the
  833: </tfoot> end tag in xml/html.
  834: 
  835: =cut
  836: 
  837: sub end_foot {
  838:     my ($this) = @_;
  839:     if ($tracing) { &Apache::lonnet::logthis("end_foot") }
  840:     $this->{'part'}  = 'body';
  841: }
  842: 
  843: =pod
  844: 
  845: =head2 start_body
  846: 
  847: Starts the set of rows that will be in the table body.   Note that if
  848: we are not in the header or footer, body rows are implied.
  849: This correspondes to the presence of a <tbody> tag in html/xml.
  850: If group borders are on, a rule will be rendered at the top and bottom
  851: of the body rows.
  852: 
  853: =cut
  854: 
  855: sub start_body {
  856:     my ($this) = @_;
  857:     if ($tracing) { &Apache::lonnet::logthis("start_body"); }
  858:     $this->{'part'}  = 'body';
  859: }
  860: 
  861: =pod
  862:  
  863: =head2 end_body
  864: 
  865: Ends the set of rows in a table body.  Note that in the event we are not
  866: in  the header or footer groups this code assumes we are in the body
  867: group.  I believe this is a good match to how mot browsers render.
  868: 
  869: =cut
  870: 
  871: sub end_body {
  872:     my ($this) = @_;
  873:     if ($tracing) { &Apache::lonnet::logthis("end_body"); }
  874: 
  875: }
  876: 
  877: =pod
  878: 
  879: =head2 define_colgroup
  880: 
  881: Define a column group  a column group corresponds to the
  882: <cgroup> tag in Html/Xml. A column group as we implement it has
  883: the following properties tht will be shared amongst all cells in the
  884: columns in the group unless overidden in the specific oell definition:
  885: 
  886: =over 2
  887: 
  888: =item span 
  889: 
  890: The number of columns in the column group.  This defaults to 1.
  891: 
  892: =item halign
  893: 
  894: Horizontal alignment of the cells.  This defaults to left.
  895: Other values are left, center, right (justify and char are 
  896: accepted but treated as left).
  897:   
  898: =item valign
  899: 
  900: Vertical alignment of the cells.  This defaults to middle.
  901: Other values are top middle, bottom, (baseline is accepted and
  902: treated as top).
  903: 
  904: =back   
  905: 
  906: If group borders are turned on, a rule will be rendered
  907: at the left and right side of the column group.
  908: 
  909: =head3 parameters
  910: 
  911: =over 2
  912: 
  913: =item definition
  914: 
  915: This is a hash that contains any of the keys described above that
  916: define the column group.
  917: 
  918: =back
  919: 
  920: 
  921: =head3 Example
  922: 
  923:  $table->define_colgroup({
  924:     'span'    => 2,
  925:     'halign'  => 'center'
  926:                          })
  927: 
  928: 
  929: 
  930: =cut
  931: 
  932: sub define_colgroup {
  933:     my ($this, $attributes)  = @_;
  934:     if ($tracing) { &Apache::lonnet::logthis("col_group"); }
  935:     
  936: 
  937: }
  938: 
  939: #------------------------- Render the table ---------------------
  940: 
  941: =pod
  942: 
  943: =head2 generate
  944: 
  945: Call this when the structures for the table have been built.
  946: This will generate and return the table object that can be used
  947: to generate the table.  Returning the table object allows for
  948: a certain amount of testing to be done on the generated table.
  949: The caller can then ask the table object to generate LaTeX.
  950: 
  951: =cut
  952: 
  953: sub generate {
  954:     my ($this) = @_;
  955:     my $useP   = 0;
  956: 
  957:     my $colunits = 'cm';	# All widths get normalized to cm.
  958:     my $tablewidth;
  959: 
  960:     if($tracing) {&Apache::lonnet::logthis("generate"); }
  961:     my $table = Apache::lonlatextable->new();
  962: 
  963:     my $inner_border = $this->{'inner_border'};
  964:     my $outer_border = $this->{'outer_border'};
  965:     my $column_count = $this->{'column_count'};
  966: 
  967:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
  968:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
  969:     my $part_border   = ($inner_border == 4);
  970:  
  971:     # Add the caption if supplied.
  972: 
  973:     if ($this->{'caption'} ne "") {
  974: 	$table->set_caption($this->caption);
  975:     }
  976:     
  977:     # Set the width if defined:
  978: 
  979:     my $default_width;
  980:     my $colwidths        = $this->{'col_widths'};
  981:     if (defined ($this->{'width'})) {
  982: 	$tablewidth = $this->{'width'};
  983: 	$tablewidth = $this->size_to_cm($tablewidth);
  984: 
  985: 	$useP = 1;
  986: 
  987: 	# Figure out the default width for a column with unspecified
  988: 	# We take the initially specified widths and sum them up.
  989: 	# This is subtracted from total width  above.
  990: 	# If the result is negative we're going to allow a minimum of 2.54cm for
  991: 	# each column and make the table spill appropriately.  
  992: 	# This (like a riot) is an ugly thing but I'm open to suggestions about
  993: 	# how to handle it better (e.g. scaling down requested widths?).
  994: 
  995: 	my $specified_width = 0.0;
  996: 	my $specified_cols   = 0;
  997: 	foreach my $col (keys %$colwidths) {
  998: 	    $specified_width = $specified_width + $colwidths->{$col};
  999: 	    $specified_cols++;
 1000: 	}
 1001: 	my $unspecified_cols = $this->{'column_count'} - $specified_cols;
 1002: 
 1003: 	#  If zero unspecified cols, we are pretty much done... just have to
 1004: 	#  adjust the total width to be specified  width. Otherwise we
 1005: 	#  must figure out the default width and total width:
 1006: 	#
 1007: 	my $total_width;
 1008: 	if($unspecified_cols == 0) {
 1009: 	    $total_width = $specified_width;
 1010: 	} else {
 1011: 	    $default_width = ($tablewidth - $specified_width)/$unspecified_cols; #  Could be negative....
 1012: 	    $total_width   = $default_width * $unspecified_cols + $specified_width;
 1013: 	}
 1014: 	
 1015: 	# if the default_width is < 0.0 the user has oversubscribed the width of the table with the individual
 1016: 	# column.  In this case, we're going to maintain the desired proportions of the user's columns, but 
 1017: 	# ensure that the unspecified columns get a fair share of the width..where a fair share is defined as
 1018: 	# the total width of the table / unspecified column count.
 1019: 	# We figure out what this means in terms of reducing the specified widths by dividing by a constant proportionality.
 1020: 	# Note that this cannot happen if the user hasn't specified anywidths as the computation above would then
 1021: 	# just make all columns equal fractions of the total table width.
 1022: 
 1023: 	if ($default_width < 0) {
 1024: 	    $default_width = ($tablewidth/$unspecified_cols);                     # 'fair' default width.
 1025: 	    my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols.
 1026: 	    my $reduction       = $tablewidth/$width_remaining;                    # Reduction fraction for specified cols
 1027: 	    foreach my $col (keys %$colwidths) {
 1028: 		$colwidths->{$col} = $colwidths->{$col}/$reduction;
 1029: 	    }
 1030: 	    
 1031:         }
 1032:     }
 1033: 
 1034:     if ($tracing) { &Apache::lonnet::logthis("rendering head"); }
 1035:     $this->render_part('head', $table, $useP, $default_width);
 1036:     if ($tracing) { &Apache::lonnet::logthis("rendering body"); }
 1037:     $this->render_part('body', $table, $useP, $default_width);
 1038:     if ($tracing) { &Apache::lonnet::logthis("rendering footer"); }
 1039:     $this->render_part('foot', $table, $useP, $default_width);
 1040: 
 1041: 
 1042: 
 1043: 
 1044:     
 1045:     my $coldef = "";
 1046:     if ($outer_border || $cell_lr_border) {
 1047: 	$coldef .= '|';
 1048:     }
 1049:     for (my $i =0; $i < $column_count; $i++) {
 1050: 	if ($useP) {
 1051: 	    $coldef .= "p{$default_width $colunits}";
 1052: 	} else {
 1053: 	    $coldef .= 'l';
 1054: 	}
 1055: 	if ($cell_lr_border || 
 1056: 	    ($outer_border && ($i == $column_count-1))) {
 1057: 	    $coldef .= '|';
 1058: 	}
 1059:     }
 1060:     $table->{'coldef'} = $coldef;
 1061: 
 1062:     # Return the table:
 1063: 
 1064:     if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }
 1065: 
 1066: 
 1067:     return $table;
 1068: 
 1069: }
 1070: 
 1071: 
 1072: #---------------------------------------------------------------------------
 1073: #
 1074: #  Private methods:
 1075: #
 1076: 
 1077: # 
 1078: # Convert size with units -> size in cm.
 1079: # The resulting size is floating point with no  units so that it can be used in
 1080: # computation.  Note that an illegal or missing unit is treated silently as
 1081: #  cm for now.
 1082: #
 1083: sub size_to_cm {
 1084:     my ($this, $size_spec) = @_;
 1085:     my ($size, $units) = split(/ /, $size_spec);
 1086:     if (lc($units) eq 'mm') {
 1087: 	return $size / 10.0;
 1088:     }
 1089:     if (lc($units) eq 'in') {
 1090: 	return $size * 2.54;
 1091:     }
 1092:     
 1093:     return $size;		# Default is cm.
 1094: }
 1095: 
 1096: #
 1097: #  Render a part of the table.  The valid table parts are
 1098: #  head, body and foot.  These corresopnd to the set of rows
 1099: #  define within <thead></thead>, <tbody></tbody> and <tfoot></tfoot>
 1100: #  respectively.
 1101: #
 1102: sub render_part {
 1103:     my ($this, $part, $table, $useP, $default_width) = @_;
 1104: 
 1105:     if ($tracing) { &Apache::lonnet::logthis("render_part: $part") };
 1106: 
 1107:     # Do nothing if that part of the table is empty:
 1108: 
 1109:     &Apache::lonnet::logthis(Dumper($this->{'rows'}));
 1110:     if ($this->{'rows'}->{$part} == undef) {
 1111: 	if ($tracing) {&Apache::lonnet::logthis("$part is empty"); }
 1112: 	return;
 1113:     }
 1114: 
 1115: 
 1116:     # Build up the data:
 1117: 
 1118:     my @data;
 1119:     my $colwidths        = $this->{'col_widths'};
 1120:     my $rows      = $this->{'rows'}->{$part}; # TODO: Render header, body footer as groups.
 1121:     my $row_count = scalar(@$rows);
 1122:     my $inner_border = $this->{'inner_border'};
 1123:     my $outer_border = $this->{'outer_border'};
 1124:     my $column_count = $this->{'column_count'};
 1125: 
 1126:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
 1127:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
 1128:     my $part_border   = ($inner_border == 4);
 1129:     my $colunits    = 'cm';	# All units in cm.
 1130: 
 1131:     # Add a top line if the outer or inner border is enabled:
 1132:     # or if group rules are on.
 1133:     #
 1134: 
 1135:     if ($outer_border || $cell_ul_border || $part_border) {
 1136: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1137: 
 1138:     }
 1139: 
 1140:     for (my $row = 0; $row < $row_count; $row++) {
 1141: 	my @row;
 1142: 	my $cells      = $rows->[$row]->{'cells'};
 1143: 	my $def_halign = $rows->[$row]->{'default_halign'};
 1144: 	my $cell_count = scalar(@$cells);
 1145: 	my $startcol   = 1;
 1146: 	my @underlines;		# Array of \cline cells if cellborder on.
 1147: 
 1148: 
 1149: 
 1150: 	for (my $cell  = 0; $cell < $cell_count; $cell++) {
 1151: 	    my $contents = $cells->[$cell]->{'contents'};
 1152: 
 1153: 	    #
 1154: 	    #  Cell alignment is the default alignment unless
 1155: 	    #  explicitly specified in the cell.
 1156: 	    #  NOTE: at this point I don't know how to do vert alignment.
 1157: 	    #
 1158: 
 1159: 	    my $halign   = $def_halign;
 1160: 	    if (defined ($cells->[$cell]->{'halign'})) {
 1161: 		$halign = $cells->[$cell]->{'halign'};
 1162: 	    }
 1163: 
 1164: 	    # Create the horizontal alignment character:
 1165: 
 1166: 	    my $col_align = 'l';
 1167: 	    my $embeddedAlignStart = "";
 1168: 	    my $embeddedAlignEnd   = "";
 1169: 
 1170: 	    if ($halign eq 'right') {
 1171: 		$col_align = 'r';
 1172:                 $embeddedAlignStart = '\raggedleft';
 1173: 	    }
 1174: 	    if ($halign eq 'center') {
 1175: 		$col_align = 'c';
 1176: 		$embeddedAlignStart = '\begin{center}';
 1177: 		$embeddedAlignEnd   = '\end{center}';
 1178: 	    }
 1179: 
 1180: 	    # If the width has been specified, turn these into
 1181: 	    # para mode; and wrap the contents in the start/stop stuff:
 1182: 
 1183: 	    if ($useP) {
 1184: 		my $cw;
 1185: 		if (defined($colwidths->{$cell})) {
 1186: 		    $cw = $colwidths->{$cell};
 1187: 		} else {
 1188: 		    $cw = $default_width;
 1189: 		}
 1190: 		$cw = $cw * $cells->[$cell]->{'colspan'};
 1191: 		$col_align = "p{$cw $colunits}";
 1192: 		$contents = $embeddedAlignStart . $contents .  $embeddedAlignEnd;
 1193: 	    }
 1194: 
 1195: 	    if ($cell_lr_border || ($outer_border && ($cell == 0))) {
 1196: 		$col_align = '|'.$col_align;
 1197: 	    }
 1198: 	    if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) {
 1199: 		$col_align = $col_align.'|';
 1200: 	    }
 1201: 
 1202: 	    #factor in spans:
 1203: 
 1204: 	    my $cspan    = $cells->[$cell]->{'colspan'};
 1205: 	    my $nextcol  = $startcol + $cspan;
 1206: 
 1207: 	    # If we can avoid the \multicolumn directive that's best as
 1208: 	    # that makes some things like \parpic invalid in LaTeX which
 1209:             # screws everything up.
 1210: 
 1211: 	    if (($cspan > 1) || !($col_align =~ /l/)) {
 1212: 
 1213: 		$contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';
 1214: 
 1215: 		# A nasty edge case.  If there's only one cell, the software will assume
 1216: 		# we're in complete control of the row so we need to end the row ourselves.
 1217: 		
 1218: 		if ($cell_count == 1) {
 1219: 		    $contents .= '  \\\\';
 1220: 		}
 1221: 	    }
 1222: 	    if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) {
 1223: 		my $lastcol = $nextcol -1;
 1224: 		push(@underlines, "\\cline{$startcol-$lastcol}");
 1225: 	    }
 1226: 	    $startcol = $nextcol;
 1227: 	    # Rowspans should take care of themselves.
 1228: 	    
 1229: 	    push(@row, $contents);
 1230: 
 1231: 	}
 1232: 	push(@data, \@row);
 1233: 	if ($cell_ul_border) {
 1234: 	    for (my $i =0; $i < scalar(@underlines); $i++) {
 1235: 		push(@data, [$underlines[$i]]);
 1236: 	    }
 1237: 	}
 1238: 
 1239:     }
 1240:     #
 1241:     # Add bottom border if necessary: if the inner border was on, the loops above
 1242:     # will have done a bottom line under the last cell.
 1243:     #
 1244:     if (($outer_border || $part_border) && !$cell_ul_border) {
 1245: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1246: 
 1247:     }
 1248:     $table->set_data(\@data);    
 1249: }
 1250: 
 1251: #----------------------------------------------------------------------------
 1252: # The following methods allow for testability.
 1253: 
 1254: 
 1255: sub get_object_attribute {
 1256:     my ($self, $attribute) = @_;
 1257:     if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
 1258:     return $self->{$attribute};
 1259: }
 1260: 
 1261: sub get_row {
 1262:     my ($self, $row) = @_;
 1263:     if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }
 1264: 
 1265:     my $rows = $self->{'rows'}->{$self->{'part'}};	  # ref to an array....
 1266:     return $rows->[$row];         # ref to the row hash for the selected row.
 1267: }
 1268: 
 1269: #   Mandatory initialization.
 1270: BEGIN{
 1271: }
 1272: 
 1273: 1;
 1274: __END__
 1275: 

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