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>