File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.19: download - view: text, annotated - select for diffs
Tue Apr 19 22:30:42 2011 UTC (13 years ago) by foxr
Branches: MAIN
CVS tags: HEAD
BZ6317 - Get rules to work for <cgroup /> case.  This should complete the work
on this bug.

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

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