File:  [LON-CAPA] / loncom / xml / lontable.pm
Revision 1.6: download - view: text, annotated - select for diffs
Tue Dec 23 11:49:32 2008 UTC (15 years, 4 months ago) by foxr
Branches: MAIN
CVS tags: HEAD
Starting to get an idea of how to control what LaTeX::Table generates
so we can format the table the way the XML/Html will request it.l

    1: # The LearningOnline Network with CAPA
    2: #  Generating TeX tables.
    3: #
    4: # $Id: lontable.pm,v 1.6 2008/12/23 11:49:32 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 LaTeX::Table 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 LaTeX::Table;
   59: 
   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 LaTeX::Table 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: =over3
  110: 
  111: =item alignment
  112: 
  113: Table alignment.  Some table styles support this but not all.
  114: 
  115: =item tableborder
  116: 
  117: If true, a border is drawn around the table.
  118: 
  119: =item cellborder
  120: 
  121: If true, borders are drawn around the cells inside a table.
  122: 
  123: =item caption
  124: 
  125: The table caption text.
  126: 
  127: =item theme
  128: 
  129: The theme of the table to use.  Defaults to Zurich.  Themes we know about are:
  130: NYC, NYC2, Zurich, Berlin, Dresden, Houston, Miami, plain, Paris.  Other themes can be added
  131: to the LaTeX::Table package, and they will become supported automatically, as theme names are
  132: not error checked.  Any use of a non-existent theme is reported by the LaTeX::Table package
  133: when the table text is generated.
  134: 
  135: =back
  136: 
  137: =head3 Member data
  138: 
  139: The object hash has the following members:
  140: 
  141: =over 3
  142: 
  143: =item column_count 
  144: 
  145: Maintained internally, the number of colums in the widest row.
  146: 
  147: =item alignment
  148: 
  149: Table alignment (configurable) "left", "center", or "right".
  150: 
  151: =item outer_border
  152: 
  153: True if a border should be drawn around the entire table (configurable)
  154: 
  155: =item inner_borders
  156: 
  157: True if a border should be drawn around all cells (configurable).
  158: 
  159: =item caption
  160: 
  161: Table caption (configurable).
  162: 
  163: =item theme
  164: 
  165: Theme desired (configurable).
  166: 
  167: =item width
  168: 
  169: If defined, the width of the table (should be supplied
  170: in fraction of column width e.g. .75 for 75%.
  171: 
  172: =item row_open 
  173: 
  174: True if a row is open and not yet closed.
  175: 
  176: =item rows
  177: 
  178: Array of row data. This is an array of hashes described below.
  179: 
  180: =back
  181: 
  182: =head3 Row data.
  183: 
  184: Each row of table data is an element of the rows hash array.  Hash elements are
  185: 
  186: =over 3
  187: 
  188: 
  189: =item default_halign 
  190: 0
  191: Default horizontal alignment for cells in this row.
  192: 
  193: =item default_valign
  194: 
  195: Default vertical alignment for cells in this row (may be ignored).
  196: 
  197: =item cell_width
  198:  
  199: The width of the row in cells.  This is the sum of the column spans 
  200: of the cells in the row.
  201: 
  202: =item cells
  203: 
  204: Array of hashes where each element represents the data for a cell.
  205: The contents of each element of this hash are described below:
  206: 
  207: =over 3
  208: 
  209: =item header
  210: 
  211: If present, the row is a 'header' that is it was made via the
  212: <th> tag.
  213: 
  214: =item halign
  215: 
  216: If present, overrides the row default horizontal alignment.
  217: 
  218: =item valign
  219: 
  220: if present, override the row default vertical alignment.
  221: 
  222: =item rowspan
  223: 
  224: If present, indicates the number of rows this cell spans.
  225: 
  226: =item colspan
  227: 
  228: If present indicates the number of columns this cell spans.
  229: Note that a cell can span both rows and columns.
  230: 
  231: =item start_col
  232: 
  233: The starting column of the cell in the table grid.
  234: 
  235: =item contents
  236: 
  237: The contents of the cell.
  238: 
  239: =back
  240: 
  241: =back
  242: 
  243: =cut
  244: 
  245: sub new {
  246:     my ($class, $configuration) = @_;
  247: 
  248:     #  Initialize the object member data with the default values
  249:     #  then override with any stuff in $configuration.
  250: 
  251:     my $self = {
  252: 	alignment      => "left",
  253: 	outer_border   => 0,
  254: 	inner_border  => 0,
  255: 	caption        => "",
  256: 	theme          => "Zurich",
  257: 	column_count   => 0,
  258: 	row_open       => 0,
  259: 	rows           => [],
  260:     };
  261: 
  262:     foreach my $key (keys %$configuration) {
  263: 	$self->{$key} = $$configuration{$key};
  264:     }
  265: 
  266:     bless($self, $class);
  267: 
  268:     return $self;
  269: }
  270: 
  271: 
  272: #-------------------------------------------------------------------------
  273: #
  274: #  Methods that get/set table global configuration.
  275: #
  276: 
  277: =pod
  278: 
  279: =head2 Gets/set alignment.  
  280: 
  281: If the method is passed a new alignment value, that replaces the current one.
  282: Regardless, the current alignment is used:
  283: 
  284: =head3 Examples:
  285: 
  286:  my $align = $table->alignment(); # Return current alignment
  287:  $table->alignment("center");     # Attempt centered alignment.
  288: 
  289: =cut
  290: 
  291: sub alignment {
  292:     my ($self, $new_value) = @_;
  293: 
  294:     if (defined($new_value)) {
  295: 	$self->{'alignment'} = $new_value;
  296:     }
  297:     return $self->{'alignment'};
  298: }
  299: 
  300: =pod
  301: 
  302: =head2 table_border
  303: 
  304: Set or get the presence of an outer border in the table.
  305: If passed a parameter, that parameter replaces the current request
  306: for or not for an outer border. Regardless, the function returns
  307: the final value of the outer_border request.
  308: 
  309: =head3 Examples:
  310: 
  311:   $table->table_border(1);      # Request an outer border.
  312:   my $outer_requested = $table->table_border();
  313: 
  314: =cut
  315: 
  316: sub table_border {
  317:     my ($self, $new_value) = @_;
  318: 
  319:     if (defined($new_value)) {
  320: 	$self->{'outer_border'} = $new_value;
  321:     }
  322:     return $self->{'outer_border'};
  323: }
  324: 
  325: 
  326: =pod
  327: 
  328: =head2 cell_border
  329: 
  330: Set or get the presence of a request for cells to have borders
  331: drawn around them.  If a paramter is passed, it will be treated as
  332: a new value for the cell border configuration.  Regardless,the final
  333: value of that configuration parameter is returned.
  334: 
  335: =head3 Examples:
  336: 
  337:  my $cell_border = $table->cell_border(); # ask if cell borders are requested.
  338:  $table->cell_border(1);	# Request cell borders.
  339: 
  340: =cut
  341: 
  342: sub cell_border {
  343:     my ($self, $new_value) = @_;
  344: 
  345:     if (defined($new_value)) {
  346: 	$self->{'inner_border'} = $new_value;
  347:     }
  348:     return $self->{'inner_border'};
  349: }
  350: 
  351: =pod
  352: 
  353: =head2 caption
  354: 
  355: Gets and/or sets the caption string for the table.  The caption string appears to label
  356: the table.  If a parameter is supplied it will become the new caption string.k
  357: 
  358: =head3 Examples:
  359: 
  360: 
  361:   $my caption = $table->caption();
  362:   $table->caption("This is the new table caption");
  363: 
  364: =cut
  365: 
  366: sub caption {
  367:     my ($self, $new_value) = @_;
  368: 
  369:     if (defined($new_value)) {
  370: 	$self->{'caption'} = $new_value;
  371:     }
  372: 
  373:     return $self->{'caption'};
  374: }
  375: 
  376: =pod
  377: 
  378: =head2 theme
  379: 
  380: Gets and optionally sets the table theme.  The table theme describes how the
  381: table will be typset by the table package.  If a parameter is supplied it
  382: will be the new theme selection.
  383: 
  384: =head3 Examples:
  385: 
  386:   my $theme = $table->theme();
  387:   $table->theme("Dresden");
  388: 
  389: =cut
  390: 
  391: sub theme {
  392:     my ($self, $new_value) = @_;
  393: 
  394:     if (defined($new_value)) {
  395: 	$self->{'theme'} = $new_value;
  396:     }
  397:     return $self->{'theme'};
  398: }
  399: 
  400: =pod
  401: 
  402: =head2 start_row
  403: 
  404: Begins a new row in the table.  If a row is already open, that row is
  405: closed off prior to starting the new row.  Rows can have the following attributes
  406: which are specified by an optional hash passed in to this function.
  407: 
  408: =over 3
  409: 
  410: =item default_halign
  411: 
  412: The default horizontal alignment of the row. This can be "left", "center", or "right"
  413: 
  414: =item default_valign
  415: 
  416: The default vertical alignment of the row.  This can be "top", "center", or "bottom"
  417: 
  418: =back
  419: 
  420: =head3 Examples:
  421: 
  422:   $table_start_row();                  # no attributes.
  423:   $table_start({default_halign => "center",
  424:                 default_valign => "bottom"}); # Create setting the attrbutes.
  425: 
  426: =cut
  427: 
  428: sub start_row {
  429:     my ($self, $config) = @_;
  430: 
  431:     if ($self->{'row_open'}) { 
  432: 	$self->end_row();
  433:     }
  434:     my $row_hash = {
  435: 	default_halign => "left",
  436: 	default_valign => "top",
  437: 	cell_width     =>  0,
  438: 	cells          => []
  439:     };
  440: 
  441:     # Override the defaults if the config hash is present:
  442: 
  443:     if (defined($config)) {
  444: 	foreach my $key  (keys %$config) {
  445: 	    $row_hash->{$key} = $config->{$key};
  446: 	}
  447:     }
  448: 
  449:     
  450:     my $rows = $self->{'rows'};
  451:     push(@$rows, $row_hash);
  452: 
  453:     $self->{"row_open"} = 1;	# Row is now open and ready for business.
  454: }
  455: 
  456: =pod
  457: 
  458: =head2  end_row 
  459: 
  460: Closes off a row.  Once closed, cells cannot be added to this row again.
  461: 
  462: =head3 Examples:
  463: 
  464:    $table->end_row();
  465: 
  466: 
  467: =cut
  468: 
  469: sub end_row {
  470:     my ($self) = @_;
  471: 
  472:     if ($self->{'row_open'}) {
  473: 	
  474: 	# Mostly we need to determine if this row has the maximum
  475: 	# cell count of any row in existence in the table:
  476: 
  477: 	my $row        = $self->{'rows'}->[-1];
  478: 	my $cells      = $row->{'cells'};
  479: 
  480: 	if ($row->{'cell_width'} > $self->{'column_count'}) {
  481: 	    $self->{'column_count'} = $row->{'cell_width'};
  482: 	}
  483: 
  484: 	$self->{'row_open'} = 0;;
  485:     }
  486: }
  487: 
  488: =pod
  489: 
  490: =head2 configure_row
  491: 
  492: Modify the configuration of a row.   If a row is not open, a new one will be opened.
  493: 
  494: =head3 Parameters:
  495: 
  496: config_hash - A hash that contains new values for the set of row confiuguration 
  497: items to be modified.  There is currently no check/penalty for items that are not in
  498: the set of defined configuration properties which are:
  499: 
  500: =over 2
  501: 
  502: =item default_halign
  503: 
  504: The default horizontal alignment for text in  cells in the row.  This can be any of:
  505: "left", "right" or "center".
  506: 
  507: =item default_valign
  508: 
  509: The default vertical alignment for text in cells in the row.  This can be any of:
  510: 
  511: "top", "bottom" or "center"
  512: 
  513: =back 
  514: 
  515: =cut
  516: 
  517: sub configure_row {
  518:     my ($self, $config) = @_;
  519: 
  520:     if (!$self->{'row_open'}) {
  521: 	$self->start_row();
  522:     }
  523:     
  524:     my $row = $self->{'rows'}[-1];
  525:     foreach my $config_item (keys %$config) {
  526: 	$row->{$config_item} = $config->{$config_item};
  527:     }
  528: }
  529: 
  530: 
  531: =pod
  532: 
  533: =head2 add_cell
  534: 
  535: Add a new cell to a row.  If there is a row above us, we need to 
  536: watch out for row spans that may force additional blank cell entries
  537: to fill in the span. 
  538: 
  539: =head3 Parameters:
  540: 
  541: =over 2
  542: 
  543: =item text
  544: 
  545: Text to put in the cell.
  546: 
  547: =item cell_config
  548: 
  549: Hash of configuration options that override the defaults.   The recognized options,
  550: and their defaults are:
  551: 
  552: =over 2
  553: 
  554: =item halign 
  555: 
  556: If nonblank overrides the row's default for the cell's horizontal alignment.
  557: 
  558: =item valign
  559: 
  560: If nonblank, overrides the row's default for the cdell's vertical alignment.
  561: 
  562: =item rowspan
  563: 
  564: Number of rows the cell spans.
  565: 
  566: =item colspan
  567: 
  568: Number of columns the cell spans.
  569: 
  570: =back
  571: 
  572: =cut
  573: 
  574: sub add_cell {
  575:     my ($self, $text, $config) = @_;
  576: 
  577:     # If a row is not open, we must open it:
  578: 
  579:     if (!$self->{'row_open'}) {
  580: 	$self->start_row();
  581:     }
  582:     my $rows          = $self->{'rows'};
  583:     my $current_row   = $rows->[-1];
  584:     my $current_cells = $current_row->{'cells'}; 
  585:     my $last_coord    = $current_row->{'cell_width'};
  586: 
  587:     #  We have to worry about row spans if there is a prior row:
  588: 
  589:     if (scalar(@$rows) > 1) {
  590: 
  591: 	my $last_row = $rows->[-2];
  592: 	if ($last_coord < $last_row->{'cell_width'}) {
  593: 	    my $prior_coord       = 0;
  594: 	    my $prior_cell_index  = 0;
  595: 	    while ($prior_coord <= $last_coord) {
  596: 		
  597: 		# Pull a cell down if it's coord matches our start coord
  598: 		# And there's a row span > 1.
  599: 		# Having done so, we adjust our $last_coord to match the
  600: 		# end point of the pulled down cell.
  601: 
  602: 		my $prior_cell = $last_row->{'cells'}->[$prior_cell_index];
  603: 		if (($prior_cell->{'start_col'} == $last_coord) &&
  604: 		    ($prior_cell->{'rowspan'}  > 1)) {
  605: 		    
  606: 		    #  Need to drop the cell down
  607: 
  608: 		    my %dropped_down_cell = %$prior_cell;
  609: 		    $dropped_down_cell{'rowspan'}--;
  610: 		    $dropped_down_cell{'contents'} = '';
  611: 
  612: 		    push(@$current_cells, \%dropped_down_cell);
  613: 		    $last_coord += $dropped_down_cell{'colspan'};
  614: 		    $current_row->{'cell_width'} = $last_coord;
  615: 		    
  616: 		}
  617: 		$prior_coord += $prior_cell->{'colspan'};
  618: 		$prior_cell_index++;
  619: 	    }
  620: 	}
  621: 
  622:     }
  623: 
  624:     #
  625:     # Now we're ready to build up our cell:
  626: 
  627:     my $cell = {
  628: 	rowspan    => 1,
  629: 	colspan    => 1,
  630: 	start_col  => $last_coord,
  631: 	contents   => $text
  632:     };
  633:     
  634:     if (defined($config)) {
  635: 	foreach my $key (keys(%$config)) {
  636: 	    $cell->{$key} = $config->{$key};
  637: 	}
  638:     }
  639:     $current_row->{'cell_width'} += $cell->{'colspan'};
  640: 
  641:     push(@$current_cells, $cell);
  642: }
  643: 
  644: =pod
  645: 
  646: =head2 generate
  647: 
  648: Call this when the structures for the table have been built.
  649: This will generate and return the table object that can be used
  650: to generate the table.  Returning the table object allows for
  651: a certain amount of testing to be done on the generated table.
  652: The caller can then ask the table object to generate LaTeX.
  653: 
  654: =cut
  655: sub generate {
  656:     my ($this) = @_;
  657: 
  658:     my $table = LaTeX::Table->new();
  659: 
  660:     # Build up the data:
  661: 
  662:     my @data;
  663:     my $rows      = $this->{'rows'};
  664:     my $row_count = scalar(@$rows);
  665:     my $inner_border = $this->{'inner_border'};
  666:     my $outer_border = $this->{'outer_border'};
  667:     my $column_count = $this->{'column_count'};
  668: 
  669:     for (my $row = 0; $row < $row_count; $row++) {
  670: 	my @row;
  671: 	my $cells      = $rows->[$row]->{'cells'};
  672: 	my $cell_count = scalar(@$cells);
  673: 	my $startcol   = 1;
  674: 	my @underlines;		# Array of \cline cells if cellborder on.
  675: 
  676: 	for (my $cell  = 0; $cell < $cell_count; $cell++) {
  677: 	    my $contents = $cells->[$cell]->{'contents'};
  678: 	    my $cspan    = $cells->[$cell]->{'colspan'};
  679: 	    my $nextcol  = $startcol + $cspan;
  680: 	    if ($cspan > 1) {
  681: 		$contents = '\multicolumn{'.$cspan.'}{|l|}{'.$contents.'}';
  682: 	    }
  683: 	    if ($inner_border && ($cells->[$cell]->{'rowspan'} == 1)) {
  684: 		my $lastcol = $nextcol -1;
  685: 		push(@underlines, "\\cline{$startcol-$lastcol}");
  686: 	    }
  687: 	    $startcol = $nextcol;
  688: 	    # Rowspans should take care of themselves.
  689: 	    
  690: 
  691: 	    push(@row, $contents);
  692: 
  693: 	}
  694: 	push(@data, \@row);
  695: 	if ($inner_border) {
  696: 	    for (my $i =0; $i < scalar(@underlines); $i++) {
  697: 		push(@data, [$underlines[$i]]);
  698: 	    }
  699: 	}
  700: 
  701:     }
  702:     $table->set_data(\@data);
  703:     
  704:     my $coldef = "";
  705:     if ($outer_border || $inner_border) {
  706: 	$coldef .= '|';
  707:     }
  708:     for (my $i =0; $i < $column_count; $i++) {
  709: 	$coldef .= 'l';
  710: 	if ($inner_border || 
  711: 	    ($outer_border && ($i == $column_count-1))) {
  712: 	    $coldef .= '|';
  713: 	}
  714:     }
  715:     $table->{'coldef'} = $coldef;
  716: 
  717:     # Return the table:
  718: 
  719:     return $table;
  720: 
  721: }
  722: #----------------------------------------------------------------------------
  723: # The following methods allow for testability.
  724: 
  725: 
  726: sub get_object_attribute {
  727:     my ($self, $attribute) = @_;
  728:     return $self->{$attribute};
  729: }
  730: 
  731: sub get_row {
  732:     my ($self, $row) = @_;
  733:     my $rows = $self->{'rows'};	  # ref to an array....
  734:     return $rows->[$row];         # ref to the row hash for the selected row.
  735: }
  736: #   Mandatory initialization.
  737: BEGIN{
  738: }
  739: 
  740: 1;
  741: __END__

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