1<?php
2
3require_once DOKU_PLUGIN.'odt/ODT/elements/ODTStateElement.php';
4require_once DOKU_PLUGIN.'odt/ODT/elements/ODTContainerElement.php';
5
6/**
7 * ODTElementTable:
8 * Class for handling the table element.
9 *
10 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 * @author  LarsDW223
12 */
13class ODTElementTable extends ODTStateElement implements iContainerAccess
14{
15    // Table specific state data
16    protected $container = NULL;
17    protected $containerPos = NULL;
18    protected $table_column_styles = array ();
19    protected $table_style = NULL;
20    protected $table_autocols = false;
21    protected $table_maxcols = 0;
22    protected $table_curr_column = 0;
23    protected $table_row_count = 0;
24    protected $own_max_width = NULL;
25
26    // Flag indicating that a table was created inside of a list
27    protected $list_interrupted = false;
28
29    /**
30     * Constructor.
31     * ($numrows is currently unused)
32     */
33    public function __construct($style_name=NULL, $maxcols = 0, $numrows = 0) {
34        parent::__construct();
35        $this->setClass ('table');
36        if ($style_name != NULL) {
37            $this->setStyleName ($style_name);
38        }
39        $this->setTableMaxColumns($maxcols);
40        if ($maxcols == 0) {
41            $this->setTableAutoColumns(true);
42        }
43        $this->container = new ODTContainerElement($this);
44    }
45
46    /**
47     * Return the elements name.
48     *
49     * @return string The ODT XML element name.
50     */
51    public function getElementName () {
52        return ('table:table');
53    }
54
55    /**
56     * Return string with encoded opening tag.
57     *
58     * @return string The ODT XML code to open this element.
59     */
60    public function getOpeningTag () {
61        $style_name = $this->getStyleName();
62        if ($style_name == NULL) {
63            $encoded = '<table:table>';
64        } else {
65            $encoded .= '<table:table table:style-name="'.$style_name.'">';
66        }
67        $maxcols = $this->getTableMaxColumns();
68        $count = $this->getCount();
69        if ($maxcols == 0) {
70            // Try to automatically detect the number of columns.
71            $this->setTableAutoColumns(true);
72        } else {
73            $this->setTableAutoColumns(false);
74        }
75
76        // Add column definitions placeholder.
77        // This will be replaced on tabelClose()/getClosingTag()
78        $encoded .= '<ColumnsPlaceholder'.$count.'>';
79
80        // We start with the first column
81        $this->setTableCurrentColumn(0);
82
83        return $encoded;
84    }
85
86    /**
87     * Return string with encoded closing tag.
88     *
89     * @return string The ODT XML code to close this element.
90     */
91    public function getClosingTag (&$content = NULL) {
92        // Generate table column definitions and replace the placeholder with it
93        $count = $this->getCount();
94        $max = $this->getTableMaxColumns();
95        if ($max > 0 && $content != NULL) {
96            $column_defs = '';
97            for ($index = 0 ; $index < $max ; $index++) {
98                $styleName = $this->getTableColumnStyleName($index);
99                if (!empty($styleName)) {
100                    $column_defs .= '<table:table-column table:style-name="'.$styleName.'"/>';
101                } else {
102                    $column_defs .= '<table:table-column/>';
103                }
104            }
105            $content =
106                str_replace ('<ColumnsPlaceholder'.$count.'>', $column_defs, $content);
107            $content =
108                str_replace ('<MaxColsPlaceholder'.$count.'>', $max, $content);
109        }
110
111        return '</table:table>';
112    }
113
114    /**
115     * Are we in a paragraph or not?
116     * As a table we are not.
117     *
118     * @return boolean
119     */
120    public function getInParagraph() {
121        return false;
122    }
123
124    /**
125     * Determine and set the parent for this element.
126     * As a table the previous element is our parent.
127     *
128     * If the table is nested in another table, then the surrounding
129     * table is the parent!
130     *
131     * @param ODTStateElement $previous
132     */
133    public function determineParent(ODTStateElement $previous) {
134        $this->container->determineParent($previous);
135        if ($this->isNested ()) {
136            $this->containerPos = array();
137            $this->getParent()->determinePositionInContainer($this->containerPos, $previous);
138        }
139    }
140
141    /**
142     * Set table column styles
143     *
144     * @param array $value
145     */
146    public function setTableColumnStyles($value) {
147        $this->table_column_styles = $value;
148    }
149
150    /**
151     * Set table column style for $column
152     *
153     * @param array $value
154     */
155    public function setTableColumnStyleName($column, $style_name) {
156        $this->table_column_styles [$column] = $style_name;
157    }
158
159    /**
160     * Get table column styles
161     *
162     * @return array
163     */
164    public function getTableColumnStyles() {
165        return $this->table_column_styles;
166    }
167
168    /**
169     * Set table column style for $column
170     *
171     * @param array $value
172     */
173    public function getTableColumnStyleName($column) {
174        return $this->table_column_styles [$column];
175    }
176
177    /**
178     * Set flag if table columns shall be generated automatically.
179     * (automatically detect the number of columns)
180     *
181     * @param boolean $value
182     */
183    public function setTableAutoColumns($value) {
184        $this->table_autocols = $value;
185    }
186
187    /**
188     * Get flag if table columns shall be generated automatically.
189     * (automatically detect the number of columns)
190     *
191     * @return boolean
192     */
193    public function getTableAutoColumns() {
194        return $this->table_autocols;
195    }
196
197    /**
198     * Set maximal number of columns.
199     *
200     * @param integer $value
201     */
202    public function setTableMaxColumns($value) {
203        $this->table_maxcols = $value;
204    }
205
206    /**
207     * Get maximal number of columns.
208     *
209     * @return integer
210     */
211    public function getTableMaxColumns() {
212        return $this->table_maxcols;
213    }
214
215    /**
216     * Set current column.
217     *
218     * @param integer $value
219     */
220    public function setTableCurrentColumn($value) {
221        $this->table_curr_column = $value;
222    }
223
224    /**
225     * Get current column.
226     *
227     * @return integer
228     */
229    public function getTableCurrentColumn() {
230        return $this->table_curr_column;
231    }
232
233    /**
234     * Get the predefined style name for the current
235     * table column.
236     *
237     * @return string
238     */
239    public function getCurrentTableColumnStyleName() {
240        $table_column_styles = $this->getTableColumnStyles();
241        $curr_column = $this->getTableCurrentColumn();
242        return $table_column_styles [$curr_column];
243    }
244
245    /**
246     * Set flag if current list is interrupted (by a table) or not.
247     *
248     * @param boolean $value
249     */
250    public function setListInterrupted($value) {
251        $this->list_interrupted = $value;
252    }
253
254    /**
255     * Get flag if current list is interrupted (by a table) or not.
256     *
257     * @return boolean
258     */
259    public function getListInterrupted() {
260        return $this->list_interrupted;
261    }
262
263    /**
264     * Increae the number of rows
265     *
266     * @param boolean $value
267     */
268    public function increaseRowCount() {
269        $this->table_row_count++;
270    }
271
272    public function getRowCount() {
273        return $this->table_row_count;
274    }
275
276    /**
277     * Is this table a nested table (inserted into another table)?
278     *
279     * @return boolean
280     */
281    public function isNested () {
282        return $this->container->isNested();
283    }
284
285    public function addNestedContainer (iContainerAccess $nested) {
286        $this->container->addNestedContainer ($nested);
287    }
288
289    public function getNestedContainers () {
290        return $this->container->getNestedContainers ();
291    }
292
293    public function determinePositionInContainer (array &$data, ODTStateElement $current) {
294        $data ['column'] = $this->getTableCurrentColumn();
295        $cell = NULL;
296        while ($current != NULL) {
297            if ($current->getClass() == 'table-cell') {
298                $cell = $current;
299                break;
300            }
301            if ($current->getClass() == 'table') {
302                break;
303            }
304            $current = $current->getParent();
305        }
306        if ($cell !== NULL) {
307            $data ['cell'] = $cell;
308        }
309    }
310
311    public function getMaxWidthOfNestedContainer (ODTInternalParams $params, array $data) {
312        if ($this->own_max_width === NULL) {
313            // We do not know our own width yet. Calculate it first.
314            $this->own_max_width = $this->getMaxWidth($params);
315        }
316
317        $column = $data ['column'];
318        $cell = $data ['cell'];
319
320        $cell_style = $cell->getStyle();
321        $padding = 0;
322        if ($cell_style->getProperty('padding-left') != NULL
323            ||
324            $cell_style->getProperty('padding-right') != NULL) {
325            $value = $cell_style->getProperty('padding-left');
326            $value = $params->document->toPoints($value, 'y');
327            $padding += $value;
328            $value = $cell_style->getProperty('padding-right');
329            $value = $params->document->toPoints($value, 'y');
330            $padding += $value;
331        } else if ($cell_style->getProperty('padding') != NULL) {
332            $value = $cell_style->getProperty('padding');
333            $value = $params->document->toPoints($value, 'y');
334            $padding += 2 * $value;
335        }
336
337        $table_column_styles = $this->getTableColumnStyles();
338        $style_name = $table_column_styles [$column-1];
339        $style_obj = $params->document->getStyle($style_name);
340        if ($style_obj !== NULL) {
341            $width = $style_obj->getProperty('column-width');
342            $width = trim ($width, 'pt');
343            $width -= $padding;
344        }
345
346        // Compare with total table width
347        if ($this->own_max_width !== NULL) {
348            $table_width = $params->units->getDigits ($params->units->toPoints($this->own_max_width));
349
350            if ($table_width < $width) {
351                $width = $table_width;
352            }
353        }
354
355        return $width.'pt';
356    }
357
358    public function getMaxWidth (ODTInternalParams $params) {
359        $tableStyle = $this->getStyle();
360        if (!$this->isNested ()) {
361            // Get max page width in points.
362            $maxPageWidth = $params->document->getAbsWidthMindMargins ();
363            $maxPageWidthPt = $params->units->getDigits ($params->units->toPoints($maxPageWidth.'cm'));
364
365            // Get table left margin
366            $leftMargin = $tableStyle->getProperty('margin-left');
367            if ($leftMargin === NULL) {
368                $leftMarginPt = 0;
369            } else {
370                $leftMarginPt = $params->units->getDigits ($params->units->toPoints($leftMargin));
371            }
372
373            // Get table right margin
374            $rightMargin = $tableStyle->getProperty('margin-right');
375            if ($rightMargin === NULL) {
376                $rightMarginPt = 0;
377            } else {
378                $rightMarginPt = $params->units->getDigits ($params->units->toPoints($rightMargin));
379            }
380
381            // Get table width
382            $width = $tableStyle->getProperty('width');
383            if ($width !== NULL) {
384                $widthPt = $params->units->getDigits ($params->units->toPoints($width));
385            }
386
387            if ($width === NULL) {
388                $width = $maxPageWidthPt - $leftMarginPt - $rightMarginPt;
389            } else {
390                $width = $widthPt;
391            }
392            $width = $width.'pt';
393        } else {
394            // If this table is nested in another container we have to ask it's parent
395            // for the allowed max width
396            $width = $this->getParent()->getMaxWidthOfNestedContainer($params, $this->containerPos);
397        }
398
399        return $width;
400    }
401
402    /**
403     * This function replaces the width of $table with the
404     * value of all column width added together. If a column has
405     * no width set then the function will abort and change nothing.
406     *
407     * @param ODTDocument $doc The current document
408     * @param ODTElementTable $table The table to be adjusted
409     */
410    public function adjustWidth (ODTInternalParams $params, $allowNested=false) {
411        if ($this->isNested () && !$allowNested) {
412            // Do not do anything if this is a nested table.
413            // Only if the function is called for the parent/root table
414            // then the width of the nested tables will be calculated.
415            return;
416        }
417        $matches = array ();
418
419        $table_style_name = $this->getStyleName();
420        if (empty($table_style_name)) {
421            return;
422        }
423
424        $max_width = $this->getMaxWidth($params);
425        $width = $this->adjustWidthInternal ($params, $max_width);
426
427        $style_obj = $params->document->getStyle($table_style_name);
428        if ($style_obj != NULL) {
429            $style_obj->setProperty('width', $width.'pt');
430            if (!$this->isNested ()) {
431                // Calculate rel width in relation to maximum page width
432                $maxPageWidth = $params->document->getAbsWidthMindMargins ();
433                $maxPageWidth = $params->units->getDigits ($params->units->toPoints($maxPageWidth.'cm'));
434                if ($maxPageWidth != 0) {
435                    $rel_width = round(($width * 100)/$maxPageWidth);
436                }
437            } else {
438                // Calculate rel width in relation to maximum table width
439                if ($max_width != 0) {
440                    $rel_width = round(($width * 100)/$max_width);
441                }
442            }
443            $style_obj->setProperty('rel-width', $rel_width.'%');
444        }
445
446        // Now adjust all nested containers too
447        $nested = $this->getNestedContainers ();
448        foreach ($nested as $container) {
449            $container->adjustWidth ($params, true);
450        }
451    }
452
453    public function adjustWidthInternal (ODTInternalParams $params, $maxWidth) {
454        $empty = array();
455        $relative = array();
456        $anyWidthFound = false;
457        $onlyAbsWidthFound = true;
458
459        $tableStyle = $this->getStyle();
460
461        // First step:
462        // - convert all absolute widths to points
463        // - build the sum of all absolute column width values (if any)
464        // - build the sum of all relative column width values (if any)
465        $abs_sum = 0;
466        $table_column_styles = $this->getTableColumnStyles();
467        $replace = true;
468        for ($index = 0 ; $index < $this->getTableMaxColumns() ; $index++ ) {
469            $style_name = $table_column_styles [$index];
470            $style_obj = $params->document->getStyle($style_name);
471            if ($style_obj != NULL) {
472                if ($style_obj->getProperty('rel-column-width') != NULL) {
473                    $width = $style_obj->getProperty('rel-column-width');
474                    $length = strlen ($width);
475                    $width = trim ($width, '*');
476
477                    // Add column style object to relative array
478                    // We need convert it to an absolute width
479                    $entry = array();
480                    $entry ['width'] = $width;
481                    $entry ['obj'] = $style_obj;
482                    $relative [] = $entry;
483
484                    $abs_sum += (($width/10)/100) * $maxWidth;
485                    $onlyAbsWidthFound = false;
486                    $anyWidthFound = true;
487                } else if ($style_obj->getProperty('column-width') != NULL) {
488                    $width = $style_obj->getProperty('column-width');
489                    $length = strlen ($width);
490                    $width = $params->document->toPoints($width, 'x');
491                    $abs_sum += (float) trim ($width, 'pt');
492                    $anyWidthFound = true;
493                } else {
494                    // Add column style object to empty array
495                    // We need to assign a width to this column
496                    $empty [] = $style_obj;
497                    $onlyAbsWidthFound = false;
498                }
499            }
500        }
501
502        // Convert max width to points
503        $maxWidth = $params->units->toPoints($maxWidth);
504        $maxWidth = $params->units->getDigits($maxWidth);
505
506        // The remaining absolute width is the max width minus the sum of
507        // all absolute width values
508        $absWidthLeft = $maxWidth - $abs_sum;
509
510        // Calculate the relative width left
511        // (e.g. if the absolute width is the half of the max width
512        //  then the relative width left if 50%)
513        if ($maxWidth != 0) {
514            $relWidthLeft = 100-(($absWidthLeft/$maxWidth)*100);
515        }
516
517        // Give all empty columns a width
518        $maxEmpty = count($empty);
519        foreach ($empty as $column) {
520            //$width = ($relWidthLeft/$maxEmpty) * $absWidthLeft;
521            $width = $absWidthLeft/$maxEmpty;
522            $column->setProperty('column-width', $width.'pt');
523            $column->setProperty('rel-column-width', NULL);
524        }
525
526        // Convert all relative width to absolute
527        foreach ($relative as $column) {
528            $width = (($column ['width']/10)/100) * $maxWidth;
529            $column ['obj']->setProperty('column-width', $width.'pt');
530            $column ['obj']->setProperty('rel-column-width', NULL);
531        }
532
533        // If all columns have a fixed absolute width set then that means
534        // the table shall have the width of all comuns added together
535        // and not the maximum available width. Return $abs_sum.
536        if ($onlyAbsWidthFound && $anyWidthFound) {
537            return $abs_sum;
538        }
539        return $maxWidth;
540    }
541}
542