File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.22: download - view: text, annotated - select for diffs
Mon Dec 15 00:52:40 2014 UTC (9 years, 4 months ago) by raeburn
Branches: MAIN
CVS tags: version_2_12_X, version_2_11_X, version_2_11_4_uiuc, version_2_11_4_msu, version_2_11_4, version_2_11_3_uiuc, version_2_11_3_msu, version_2_11_3, version_2_11_2_uiuc, version_2_11_2_msu, version_2_11_2_educog, version_2_11_2, version_2_11_1, HEAD
- Coding style: keys()

    1: # The LearningOnline Network with CAPA
    2: #  Generating TeX tables.
    3: #
    4: # $Id: lontable.pm,v 1.22 2014/12/15 00:52:40 raeburn 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: my $tracing = 0;		# Set to 1 to enable log tracing. 2 for local sub tracing.
   62: 
   63: =pod
   64: 
   65: =head1  lontable Table generation assistant for the LaTeX target
   66: 
   67: This module contains support software for generating tables in LaTeX output mode 
   68: In this implementation, we use the Apache::lonlatextable package to do the actual final formatting.
   69: Each table creates a new object.  Table objects can have global properties configured.
   70: The main operations on a table object are:
   71: 
   72: =over 3
   73: 
   74: =item start_row  
   75: 
   76: Opens a new table row.
   77: 
   78: =item end_row
   79: 
   80: Closes a table row.
   81: 
   82: =item configure_row
   83: 
   84: Modifies a configuration item in the currently open row.
   85: 
   86: =item generate
   87: 
   88: Returns the generated table string.
   89: 
   90: =item configure
   91: 
   92: Configures a table's global configuration.
   93: 
   94: =item add_cell
   95: 
   96: Add and configure a cell to the current row.6
   97: 
   98: =back
   99: 
  100: =cut
  101: 
  102: =pod
  103: 
  104: =head2 new - create a new object.
  105: 
  106: Create a new table object.  Any of the raw table configuration items can be
  107: modified by this.  These configuration items include:
  108: 
  109:   my $table = lontable::new(\%config_hash)
  110: 
  111: =over 3
  112: 
  113: 
  114: =item alignment
  115: 
  116: Table alignment.  Some table styles support this but not all.
  117: 
  118: =item tableborder
  119: 
  120: If true, a border is drawn around the table.
  121: 
  122: =item cellborder
  123: 
  124: If true, borders are drawn around the cells inside a table.
  125: 
  126: =item caption
  127: 
  128: The table caption text.
  129: 
  130: =item theme
  131: 
  132: The theme of the table to use.  Defaults to Zurich.  Themes we know about are:
  133: NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris.  Other themes can be added
  134: to the Apache::lonlatextable package, and they will become supported automatically, as theme names are
  135: not error checked.  Any use of a non-existent theme is reported by the Apache::lonlatextable package
  136: when the table text is generated.
  137: 
  138: =item width
  139: 
  140: The width of the table.   in any
  141: TeX unit measure e.g.  10.8cm  This forces the table to the
  142: tabularx environment.  It also forces the declarations for
  143: cells to be paragraph mode which supports more internal formatting.
  144: 
  145: =back
  146: 
  147: =head3 Member data
  148: 
  149: The object hash has the following members:
  150: 
  151: =over 3
  152: 
  153: =item column_count 
  154: 
  155: Maintained internally, the number of colums in the widest row.
  156: 
  157: =item alignment
  158: 
  159: Table alignment (configurable) "left", "center", or "right".
  160: 
  161: =item outer_border
  162: 
  163: True if a border should be drawn around the entire table (configurable)
  164: 
  165: =item inner_borders
  166: 
  167: True if a border should be drawn around all cells (configurable).
  168: 
  169: =item caption
  170: 
  171: Table caption (configurable).
  172: 
  173: =item theme
  174: 
  175: Theme desired (configurable).
  176: 
  177: =item width
  178: 
  179: If defined, the width of the table (should be supplied
  180: in fraction of column width e.g. .75 for 75%.
  181: 
  182: =item row_open 
  183: 
  184: True if a row is open and not yet closed.
  185: 
  186: =item rows
  187: 
  188: Array of row data. This is an array of hashes described below.
  189: 
  190: =back
  191: 
  192: =head3 Row data.
  193: 
  194: Each row of table data is an element of the rows hash array.  Hash elements are
  195: 
  196: =over 3
  197: 
  198: 
  199: =item default_halign 
  200: 0
  201: Default horizontal alignment for cells in this row.
  202: 
  203: =item default_valign
  204: 
  205: Default vertical alignment for cells in this row (may be ignored).
  206: 
  207: =item cell_width
  208:  
  209: The width of the row in cells.  This is the sum of the column spans 
  210: of the cells in the row.
  211: 
  212: =item cells
  213: 
  214: 
  215: Array of hashes where each element represents the data for a cell.
  216: The contents of each element of this hash are described below:
  217: 
  218: =item header
  219: 
  220: If present, the row is a 'header' that is it was made via the
  221: <th> tag.
  222: 
  223: =item halign
  224: 
  225: If present, overrides the row default horizontal alignment.
  226: 
  227: =item valign
  228: 
  229: if present, override the row default vertical alignment.
  230: 
  231: =item rowspan
  232: 
  233: If present, indicates the number of rows this cell spans.
  234: 
  235: =item colspan
  236: 
  237: If present indicates the number of columns this cell spans.
  238: Note that a cell can span both rows and columns.
  239: 
  240: =item start_col
  241: 
  242: The starting column of the cell in the table grid.
  243: 
  244: =item contents
  245: 
  246: The contents of the cell.
  247: 
  248: =back
  249: 
  250: 
  251: =cut
  252: 
  253: sub new {
  254:     my ($class, $configuration) = @_;
  255: 
  256:     if ($tracing) {&Apache::lonnet::logthis("new table"); }
  257:     #  Initialize the object member data with the default values
  258:     #  then override with any stuff in $configuration.
  259: 
  260:     my $self = {
  261: 	alignment      => "left",
  262: 	outer_border   => 0,
  263: 	inner_border  => 0,
  264: 	caption        => "",
  265: 	theme          => "plain",
  266: 	column_count   => 0,
  267: 	row_open       => 0,
  268: 	rows           => {
  269: 	    'body'     => [],
  270:             'head'     => [],
  271: 	    'foot'     => []
  272: 	},
  273: 	col_widths      => {},
  274: 	part           => 'body',     # one of 'body', 'head', 'foot'.
  275: 	colgroups      => []	      # Stores information about column groups.
  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:     my $colgroups = $this->{'colgroups'};
  935:     push(@$colgroups, $attributes); # Colgroups always add at end.
  936: 
  937: 
  938: }
  939: 
  940: #------------------------- Render the table ---------------------
  941: 
  942: =pod
  943: 
  944: =head2 generate
  945: 
  946: Call this when the structures for the table have been built.
  947: This will generate and return the table object that can be used
  948: to generate the table.  Returning the table object allows for
  949: a certain amount of testing to be done on the generated table.
  950: The caller can then ask the table object to generate LaTeX.
  951: 
  952: =cut
  953: 
  954: sub generate {
  955:     my ($this) = @_;
  956:     my $useP   = 0;
  957: 
  958:     my $colunits = 'cm';	# All widths get normalized to cm.
  959:     my $tablewidth;
  960: 
  961:     if($tracing) {&Apache::lonnet::logthis("generate"); }
  962:     my $table = Apache::lonlatextable->new();
  963: 
  964:     my $inner_border = $this->{'inner_border'};
  965:     my $outer_border = $this->{'outer_border'};
  966:     my $column_count = $this->{'column_count'};
  967: 
  968:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
  969:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
  970:     my $part_border   = ($inner_border == 4);
  971:  
  972:  
  973:       # Add the caption if supplied.
  974: 
  975:     if ($this->{'caption'} ne "") {
  976: 	$table->set_caption($this->caption);
  977:     }
  978:     
  979:     # Set the width if defined:
  980: 
  981:     my $default_width;
  982:     my $colwidths        = $this->{'col_widths'};
  983:     if (defined ($this->{'width'})) {
  984: 	$tablewidth = $this->{'width'};
  985: 	$tablewidth = $this->size_to_cm($tablewidth);
  986: 
  987: 	$useP = 1;
  988: 
  989: 	# Figure out the default width for a column with unspecified
  990: 	# We take the initially specified widths and sum them up.
  991: 	# This is subtracted from total width  above.
  992: 	# If the result is negative we're going to allow a minimum of 2.54cm for
  993: 	# each column and make the table spill appropriately.  
  994: 	# This (like a riot) is an ugly thing but I'm open to suggestions about
  995: 	# how to handle it better (e.g. scaling down requested widths?).
  996: 
  997: 	my $specified_width = 0.0;
  998: 	my $specified_cols   = 0;
  999: 	foreach my $col (keys(%$colwidths)) {
 1000: 	    $specified_width = $specified_width + $colwidths->{$col};
 1001: 	    $specified_cols++;
 1002: 	}
 1003: 	my $unspecified_cols = $this->{'column_count'} - $specified_cols;
 1004: 
 1005: 	#  If zero unspecified cols, we are pretty much done... just have to
 1006: 	#  adjust the total width to be specified  width. Otherwise we
 1007: 	#  must figure out the default width and total width:
 1008: 	#
 1009: 	my $total_width;
 1010: 	if($unspecified_cols == 0) {
 1011: 	    $total_width = $specified_width;
 1012: 	} else {
 1013: 	    $default_width = ($tablewidth - $specified_width)/$unspecified_cols; #  Could be negative....
 1014: 	    $total_width   = $default_width * $unspecified_cols + $specified_width;
 1015: 	}
 1016: 	
 1017: 	# if the default_width is < 0.0 the user has oversubscribed the width of the table with the individual
 1018: 	# column.  In this case, we're going to maintain the desired proportions of the user's columns, but 
 1019: 	# ensure that the unspecified columns get a fair share of the width..where a fair share is defined as
 1020: 	# the total width of the table / unspecified column count.
 1021: 	# We figure out what this means in terms of reducing the specified widths by dividing by a constant proportionality.
 1022: 	# Note that this cannot happen if the user hasn't specified anywidths as the computation above would then
 1023: 	# just make all columns equal fractions of the total table width.
 1024: 
 1025: 	if ($default_width < 0) {
 1026: 	    $default_width = ($tablewidth/$unspecified_cols);                     # 'fair' default width.
 1027: 	    my $width_remaining = $tablewidth - $default_width*$unspecified_cols; # What's left for the specified cols.
 1028: 	    my $reduction       = $tablewidth/$width_remaining;                    # Reduction fraction for specified cols
 1029: 	    foreach my $col (keys(%$colwidths)) {
 1030: 		$colwidths->{$col} = $colwidths->{$col}/$reduction;
 1031: 	    }
 1032: 	    
 1033:         }
 1034:     }
 1035:     # If rule is groups. we need to have a 
 1036:     # list of the column numbers at which a column ends...
 1037:     # and the coldef needs to start with a |
 1038:     #
 1039:     my @colgroup_ends;
 1040:     my $colgroup_col = 0;
 1041:     my $group = 0;
 1042:     my $coldef = "";
 1043:     if ($outer_border || $cell_lr_border) {
 1044: 	$coldef .= '|';
 1045:     }
 1046:     if ($part_border) {
 1047: 	$coldef .= '|';
 1048: 	my $colgroup_col = 0;
 1049: 	my $colgroups = $this->{'colgroups'};
 1050: 	foreach my $group (@$colgroups) {
 1051: 	    if (defined $group->{'span'}) {
 1052: 		$colgroup_col += $group->{'span'};
 1053: 	    } else {
 1054: 		$colgroup_col++;
 1055: 	    }
 1056: 	    push(@colgroup_ends, $colgroup_col);
 1057: 	}
 1058: 				 
 1059:     }
 1060:     $this->render_part('head', $table, $useP, $default_width, 
 1061: 		       \@colgroup_ends);
 1062:     $this->render_part('body', $table, $useP, $default_width,
 1063: 	\@colgroup_ends);
 1064:     $this->render_part('foot', $table, $useP, $default_width,
 1065: 	\@colgroup_ends);
 1066: 
 1067: 
 1068: 
 1069: 
 1070:     
 1071:     for (my $i =0; $i < $column_count; $i++) {
 1072: 	if ($useP) {
 1073: 	    $coldef .= "p{$default_width $colunits}";
 1074: 	} else {
 1075: 	    $coldef .= 'l';
 1076: 	}
 1077: 	if ($cell_lr_border || 
 1078: 	    ($outer_border && ($i == $column_count-1))) {
 1079: 	    $coldef .= '|';
 1080: 	}
 1081: 	if ($part_border && ($i == ($colgroup_ends[$group]-1)))  {
 1082: 	    $coldef .= '|';
 1083: 	    $group++;
 1084: 	}
 1085:     }
 1086:     $table->{'coldef'} = $coldef;
 1087: 
 1088:     # Return the table:
 1089: 
 1090:     if ($tracing) { &Apache::lonnet::logthis("Leaving generate"); }
 1091: 
 1092: 
 1093:     return $table;
 1094: 
 1095: }
 1096: 
 1097: 
 1098: #---------------------------------------------------------------------------
 1099: #
 1100: #  Private methods:
 1101: #
 1102: 
 1103: # 
 1104: # Convert size with units -> size in cm.
 1105: # The resulting size is floating point with no  units so that it can be used in
 1106: # computation.  Note that an illegal or missing unit is treated silently as
 1107: #  cm for now.
 1108: #
 1109: sub size_to_cm {
 1110:     my ($this, $size_spec) = @_;
 1111:     my ($size, $units) = split(/ /, $size_spec);
 1112:     if (lc($units) eq 'mm') {
 1113: 	return $size / 10.0;
 1114:     }
 1115:     if (lc($units) eq 'in') {
 1116: 	return $size * 2.54;
 1117:     }
 1118:     
 1119:     return $size;		# Default is cm.
 1120: }
 1121: 
 1122: #
 1123: #  Render a part of the table.  The valid table parts are
 1124: #  head, body and foot.  These corresopnd to the set of rows
 1125: #  define within <thead></thead>, <tbody></tbody> and <tfoot></tfoot>
 1126: #  respectively.
 1127: #
 1128: sub render_part {
 1129:     my ($this, $part, $table, $useP,
 1130: 	$default_width, $colgroup_ends) = @_;
 1131: 
 1132:     if ($tracing) { &Apache::lonnet::logthis("render_part: $part") };
 1133: 
 1134:     # Do nothing if that part of the table is empty:
 1135: 
 1136:     if ($this->{'rows'}->{$part} == undef) {
 1137: 	if ($tracing) {&Apache::lonnet::logthis("$part is empty"); }
 1138: 	return;
 1139:     }
 1140: 
 1141:     my @cgends = @$colgroup_ends;
 1142:     # Build up the data:
 1143: 
 1144:     my @data;
 1145:     my $colwidths        = $this->{'col_widths'};
 1146:     my $rows      = $this->{'rows'}->{$part}; 
 1147:     my $row_count = scalar(@$rows);
 1148:     my $inner_border = $this->{'inner_border'};
 1149:     my $outer_border = $this->{'outer_border'};
 1150:     my $column_count = $this->{'column_count'};
 1151: 
 1152:     my $cell_ul_border = (($inner_border == 1) || ($inner_border == 2)) ? 1 : 0;
 1153:     my $cell_lr_border = (($inner_border == 1) || ($inner_border == 3)) ? 1 : 0;
 1154:     my $part_border   = ($inner_border == 4);
 1155:     my $colunits    = 'cm';	# All units in cm.
 1156: 
 1157:     # Add a top line if the outer or inner border is enabled:
 1158:     # or if group rules are on.
 1159:     #
 1160: 
 1161:     if ($outer_border || $cell_ul_border || $part_border) {
 1162: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1163: 
 1164:     }
 1165: 
 1166:     for (my $row = 0; $row < $row_count; $row++) {
 1167: 	my @row;
 1168: 	my $cells      = $rows->[$row]->{'cells'};
 1169: 	my $def_halign = $rows->[$row]->{'default_halign'};
 1170: 	my $cell_count = scalar(@$cells);
 1171: 	my $startcol   = 1;
 1172: 	my @underlines;		# Array of \cline cells if cellborder on.
 1173: 
 1174: 	my $colgroup_count = @cgends; # Number of column groups.
 1175: 	my $cgroup         = 0;	     # Group we are on.
 1176: 	my $cgstart        = 0;	     # Where the next cgroup starts.
 1177: 
 1178: 	for (my $cell  = 0; $cell < $cell_count; $cell++) {
 1179: 	    my $contents = $cells->[$cell]->{'contents'};
 1180: 	    
 1181: 	    #
 1182: 	    #  Cell alignment is the default alignment unless
 1183: 	    #  explicitly specified in the cell.
 1184: 	    #  NOTE: at this point I don't know how to do vert alignment.
 1185: 	    #
 1186: 
 1187: 	    my $halign   = $def_halign;
 1188: 	    if (defined ($cells->[$cell]->{'halign'})) {
 1189: 		$halign = $cells->[$cell]->{'halign'};
 1190: 	    }
 1191: 
 1192: 	    # Create the horizontal alignment character:
 1193: 
 1194: 	    my $col_align = 'l';
 1195: 	    my $embeddedAlignStart = "";
 1196: 	    my $embeddedAlignEnd   = "";
 1197: 
 1198: 	    if ($halign eq 'right') {
 1199: 		$col_align = 'r';
 1200:                 $embeddedAlignStart = '\raggedleft';
 1201: 	    }
 1202: 	    if ($halign eq 'center') {
 1203: 		$col_align = 'c';
 1204: 		$embeddedAlignStart = '\begin{center}';
 1205: 		$embeddedAlignEnd   = '\end{center}';
 1206: 	    }
 1207: 
 1208: 	    # If the width has been specified, turn these into
 1209: 	    # para mode; and wrap the contents in the start/stop stuff:
 1210: 
 1211: 	    if ($useP) {
 1212: 		my $cw;
 1213: 		if (defined($colwidths->{$cell})) {
 1214: 		    $cw = $colwidths->{$cell};
 1215: 		} else {
 1216: 		    $cw = $default_width;
 1217: 		}
 1218: 		$cw = $cw * $cells->[$cell]->{'colspan'};
 1219: 		$col_align = "p{$cw $colunits}";
 1220: 		$contents = $embeddedAlignStart . $contents .  $embeddedAlignEnd;
 1221: 	    }
 1222: 
 1223: 	    if ($cell_lr_border || ($outer_border && ($cell == 0))) {
 1224: 		$col_align = '|'.$col_align;
 1225: 	    }
 1226: 	    if ($cell_lr_border || ($outer_border && ($cell == ($cell_count -1)))) {
 1227: 		$col_align = $col_align.'|';
 1228: 	    }
 1229: 	    if ($part_border)  {
 1230: 		if ($cell == $cgstart) {
 1231: 		    $col_align = '|' . $col_align;
 1232: 		    if ($cgroup < $colgroup_count) {
 1233: 			$cgstart = $cgends[$cgroup];
 1234: 			$cgroup++;
 1235: 		    } else {
 1236: 			$cgstart = 1000000; # TODO: Get this logic right
 1237: 		    }
 1238: 		    if ($cell == ($cell_count - 1) &&
 1239: 			($cell == ($cgstart-1))) {
 1240: 			$col_align = $col_align . '|'; # last col ends colgrp.
 1241: 		    }
 1242: 		}
 1243: 	    }
 1244: 
 1245: 	    #factor in spans:
 1246: 
 1247: 	    my $cspan    = $cells->[$cell]->{'colspan'};
 1248: 	    my $nextcol  = $startcol + $cspan;
 1249: 	    
 1250: 	    # At this point this col is the start of the span.
 1251: 	    # nextcol is the end of the span.
 1252: 
 1253: 	    # If we can avoid the \multicolumn directive that's best as
 1254: 	    # that makes some things like \parpic invalid in LaTeX which
 1255:             # screws everything up.
 1256: 
 1257: 	    if (($cspan > 1) || !($col_align =~ /l/)) {
 1258: 
 1259: 		$contents = '\multicolumn{'.$cspan.'}{'.$col_align.'}{'.$contents.'}';
 1260: 
 1261: 		# A nasty edge case.  If there's only one cell, the software will assume
 1262: 		# we're in complete control of the row so we need to end the row ourselves.
 1263: 		
 1264: 		if ($cell_count == 1) {
 1265: 		    $contents .= '  \\\\';
 1266: 		}
 1267: 	    }
 1268: 	    if ($cell_ul_border && ($cells->[$cell]->{'rowspan'} == 1)) {
 1269: 		my $lastcol = $nextcol -1;
 1270: 		push(@underlines, "\\cline{$startcol-$lastcol}");
 1271: 	    }
 1272: 	    $startcol = $nextcol;
 1273: 
 1274: 	    # Rowspans should take care of themselves.
 1275: 	    
 1276: 	    push(@row, $contents);
 1277: 
 1278: 	}
 1279: 	push(@data, \@row);
 1280: 	if ($cell_ul_border) {
 1281: 	    for (my $i =0; $i < scalar(@underlines); $i++) {
 1282: 		push(@data, [$underlines[$i]]);
 1283: 	    }
 1284: 	}
 1285: 
 1286:     }
 1287:     #
 1288:     # Add bottom border if necessary: if the inner border was on, the loops above
 1289:     # will have done a bottom line under the last cell.
 1290:     #
 1291:     if (($outer_border || $part_border) && !$cell_ul_border) {
 1292: 	push(@data, ["\\cline{1-$column_count}"]);	     
 1293: 
 1294:     }
 1295:     $table->set_data(\@data);    
 1296: }
 1297: 
 1298: #----------------------------------------------------------------------------
 1299: # The following methods allow for testability.
 1300: 
 1301: 
 1302: sub get_object_attribute {
 1303:     my ($self, $attribute) = @_;
 1304:     if ($tracing > 1) { &Apache::lonnet::logthis("get_object_attribute: $attribute"); }
 1305:     return $self->{$attribute};
 1306: }
 1307: 
 1308: sub get_row {
 1309:     my ($self, $row) = @_;
 1310:     if ($tracing > 1) { &Apache::lonnet::logthis("get_row"); }
 1311: 
 1312:     my $rows = $self->{'rows'}->{$self->{'part'}};	  # ref to an array....
 1313:     return $rows->[$row];         # ref to the row hash for the selected row.
 1314: }
 1315: 
 1316: #   Mandatory initialization.
 1317: BEGIN{
 1318: }
 1319: 
 1320: 1;
 1321: __END__
 1322: 

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