All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.pdflayout.element.table.PLTable Maven / Gradle / Ivy

There is a newer version: 7.3.5
Show newest version
/**
 * Copyright (C) 2014-2016 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.pdflayout.element.table;

import java.util.Collection;
import java.util.List;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.OverridingMethodsMustInvokeSuper;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.string.ToStringGenerator;
import com.helger.commons.typeconvert.TypeConverter;
import com.helger.pdflayout.PLDebug;
import com.helger.pdflayout.base.AbstractPLElement;
import com.helger.pdflayout.base.IPLRenderableObject;
import com.helger.pdflayout.base.IPLSplittableObject;
import com.helger.pdflayout.base.PLElementWithSize;
import com.helger.pdflayout.base.PLSplitResult;
import com.helger.pdflayout.element.hbox.AbstractPLHBox;
import com.helger.pdflayout.element.hbox.PLHBoxColumn;
import com.helger.pdflayout.element.hbox.PLHBoxSplittable;
import com.helger.pdflayout.element.special.PLSpacerX;
import com.helger.pdflayout.element.vbox.AbstractPLVBox;
import com.helger.pdflayout.element.vbox.PLVBoxRow;
import com.helger.pdflayout.spec.SizeSpec;
import com.helger.pdflayout.spec.WidthSpec;
import com.helger.pdflayout.spec.WidthSpec.EWidthType;

/**
 * A special table with a repeating header
 *
 * @author Philip Helger
 */
public class PLTable extends AbstractPLVBox  implements IPLSplittableObject 
{
  private final ICommonsList  m_aWidths;
  private int m_nHeaderRowCount = 0;

  /**
   * @param aWidths
   *        Must all be of the same type!
   */
  public PLTable (@Nonnull @Nonempty final Iterable  aWidths)
  {
    ValueEnforcer.notEmptyNoNullValue (aWidths, "Widths");

    // Check that all width are of the same type
    EWidthType eWidthType = null;
    for (final WidthSpec aWidth : aWidths)
      if (eWidthType == null)
        eWidthType = aWidth.getType ();
      else
        if (aWidth.getType () != eWidthType)
          throw new IllegalArgumentException ("All widths must be of the same type! Found " +
                                              eWidthType +
                                              " and " +
                                              aWidth.getType ());
    m_aWidths = new CommonsArrayList<> (aWidths);
  }

  @Nonnull
  @OverridingMethodsMustInvokeSuper
  public PLTable setBasicDataFrom (@Nonnull final PLTable aSource)
  {
    super.setBasicDataFrom (aSource);
    setHeaderRowCount (aSource.m_nHeaderRowCount);
    return this;
  }

  /**
   * @return A copy of the list with all widths as specified in the constructor.
   *         Neither null nor empty.
   */
  @Nonnull
  @Nonempty
  @ReturnsMutableCopy
  public ICommonsList  getWidths ()
  {
    return m_aWidths.getClone ();
  }

  /**
   * @return The number of columns in the table. Always ≥ 0.
   */
  @Nonnegative
  public int getColumnCount ()
  {
    return m_aWidths.size ();
  }

  /**
   * Add a new table row. All contained elements are added with the specified
   * width in the constructor. null elements are represented as
   * empty cells.
   *
   * @param aElements
   *        The elements to add. May be null.
   * @return The added row and never null.
   */
  @Nonnull
  public PLHBoxSplittable addTableRow (@Nullable final AbstractPLElement ... aElements)
  {
    return addTableRow (new CommonsArrayList<> (aElements));
  }

  /**
   * Add a new table row. All contained elements are added with the specified
   * width in the constructor. null elements are represented as
   * empty cells.
   *
   * @param aElements
   *        The elements to add. May not be null.
   * @return The added row and never null.
   */
  @Nonnull
  public PLHBoxSplittable addTableRow (@Nonnull final Collection > aElements)
  {
    ValueEnforcer.notNull (aElements, "Elements");
    if (aElements.size () > m_aWidths.size ())
      throw new IllegalArgumentException ("More elements in row (" +
                                          aElements.size () +
                                          ") than defined in the table (" +
                                          m_aWidths.size () +
                                          ")!");

    final PLHBoxSplittable aRowHBox = new PLHBoxSplittable ();
    int nWidthIndex = 0;
    for (AbstractPLElement  aElement : aElements)
    {
      if (aElement == null)
      {
        // null elements end as a spacer
        aElement = new PLSpacerX ();
      }
      final WidthSpec aWidth = m_aWidths.get (nWidthIndex);
      aRowHBox.addColumn (aElement, aWidth);
      ++nWidthIndex;
    }
    super.addRow (aRowHBox);
    return aRowHBox;
  }

  @Nonnull
  public PLHBoxSplittable addTableRowExt (@Nonnull final PLTableCell... aCells)
  {
    return addTableRowExt (new CommonsArrayList<> (aCells));
  }

  /**
   * Add a new table row. All contained elements are added with the specified
   * width in the constructor. null elements are represented as
   * empty cells.
   *
   * @param aCells
   *        The cells to add. May not be null.
   * @return The added row and never null.
   */
  @Nonnull
  public PLHBoxSplittable addTableRowExt (@Nonnull final Iterable  aCells)
  {
    ValueEnforcer.notNull (aCells, "Cells");

    // Small consistency check
    int nUsedCols = 0;
    for (final PLTableCell aCell : aCells)
      nUsedCols += aCell.getColSpan ();
    if (nUsedCols > m_aWidths.size ())
      throw new IllegalArgumentException ("More cells in row (" +
                                          nUsedCols +
                                          ") than defined in the table (" +
                                          m_aWidths.size () +
                                          ")!");

    final PLHBoxSplittable aHBox = new PLHBoxSplittable ();
    int nWidthIndex = 0;
    for (final PLTableCell aCell : aCells)
    {
      final int nCols = aCell.getColSpan ();
      if (nCols == 1)
      {
        aHBox.addAndReturnColumn (aCell.getElement (), m_aWidths.get (nWidthIndex))
             .setFillColor (aCell.getFillColor ());
      }
      else
      {
        final List  aWidths = m_aWidths.subList (nWidthIndex, nWidthIndex + nCols);
        final EWidthType eWidthType = aWidths.get (0).getType ();
        WidthSpec aRealWidth;
        if (eWidthType == EWidthType.STAR)
        {
          // aggregate
          aRealWidth = WidthSpec.perc (nCols * 100f / m_aWidths.size ());
        }
        else
        {
          // aggregate values
          float fWidth = 0;
          for (final WidthSpec aWidth : aWidths)
            fWidth += aWidth.getValue ();
          aRealWidth = new WidthSpec (eWidthType, fWidth);
        }
        aHBox.addAndReturnColumn (aCell.getElement (), aRealWidth).setFillColor (aCell.getFillColor ());
      }
      nWidthIndex += nCols;
    }
    super.addRow (aHBox);
    return aHBox;
  }

  /**
   * @return The number of header rows. By default 0. Always ≥ 0.
   */
  @Nonnegative
  public int getHeaderRowCount ()
  {
    return m_nHeaderRowCount;
  }

  /**
   * Set the number of header rows in this table. Header rows get repeated on
   * every page upon rendering.
   *
   * @param nHeaderRowCount
   *        The number of header rows, to be repeated by page. Must be ≥ 0.
   * @return this
   */
  @Nonnull
  public PLTable setHeaderRowCount (@Nonnegative final int nHeaderRowCount)
  {
    ValueEnforcer.isGE0 (nHeaderRowCount, "HeaderRowCount");

    m_nHeaderRowCount = nHeaderRowCount;
    return this;
  }

  /**
   * Get the cell at the specified row and column index
   *
   * @param nRowIndex
   *        row index
   * @param nColumnIndex
   *        column index
   * @return null if row and/or column index are out of bounds.
   * @since 3.0.4
   */
  @Nullable
  public IPLRenderableObject  getCellElement (@Nonnegative final int nRowIndex, @Nonnegative final int nColumnIndex)
  {
    final PLVBoxRow aRow = getRowAtIndex (nRowIndex);
    if (aRow != null)
    {
      final PLHBoxColumn aColumn = ((AbstractPLHBox ) aRow.getElement ()).getColumnAtIndex (nColumnIndex);
      if (aColumn != null)
        return aColumn.getElement ();
    }
    return null;
  }

  @Nonnull
  @ReturnsMutableCopy
  private static float [] _getAsArray (@Nonnull final List  aList)
  {
    return TypeConverter.convertIfNecessary (aList, float [].class);
  }

  @SuppressWarnings ("unused")
  @Override
  @Nullable
  public PLSplitResult splitElements (final float fElementWidth, final float fAvailableHeight)
  {
    if (fAvailableHeight <= 0)
      return null;

    final PLTable aTable1 = new PLTable (m_aWidths).setBasicDataFrom (this);
    final PLTable aTable2 = new PLTable (m_aWidths).setBasicDataFrom (this);

    final int nTotalRows = getRowCount ();
    final ICommonsList  aTable1RowWidth = new CommonsArrayList<> (nTotalRows);
    final ICommonsList  aTable1RowHeight = new CommonsArrayList<> (nTotalRows);

    // Copy all header rows
    float fUsedTable1Width = 0;
    float fUsedTable1WidthFull = 0;
    float fUsedTable1Height = 0;
    float fUsedTable1HeightFull = 0;
    for (int nRow = 0; nRow < m_nHeaderRowCount; ++nRow)
    {
      final IPLRenderableObject  aHeaderRowElement = getRowElementAtIndex (nRow);
      aTable1.addRow (aHeaderRowElement);
      aTable2.addRow (aHeaderRowElement);

      final float fRowWidth = m_aPreparedRowElementWidth[nRow];
      final float fRowWidthFull = fRowWidth + aHeaderRowElement.getFullXSum ();
      final float fRowHeight = m_aPreparedRowElementHeight[nRow];
      final float fRowHeightFull = fRowHeight + aHeaderRowElement.getFullYSum ();

      fUsedTable1Width = Math.max (fUsedTable1Width, fRowWidth);
      fUsedTable1WidthFull = Math.max (fUsedTable1WidthFull, fRowWidthFull);
      fUsedTable1Height += fRowHeight;
      fUsedTable1HeightFull += fRowHeightFull;
      aTable1RowWidth.add (Float.valueOf (fRowWidth));
      aTable1RowHeight.add (Float.valueOf (fRowHeight));
    }

    // The height and width after header are identical
    float fUsedTable2Width = fUsedTable1Width;
    float fUsedTable2Height = fUsedTable1Height;
    float fUsedTable2HeightFull = fUsedTable1HeightFull;
    final ICommonsList  aTable2RowWidth = new CommonsArrayList<> (aTable1RowWidth);
    final ICommonsList  aTable2RowHeight = new CommonsArrayList<> (aTable1RowHeight);

    // Copy all content rows
    boolean bOnTable1 = true;

    for (int nRow = m_nHeaderRowCount; nRow < nTotalRows; ++nRow)
    {
      final IPLRenderableObject  aRowElement = getRowElementAtIndex (nRow);
      final float fRowWidth = m_aPreparedRowElementWidth[nRow];
      final float fRowWidthFull = fRowWidth + aRowElement.getFullXSum ();
      final float fRowHeight = m_aPreparedRowElementHeight[nRow];
      final float fRowHeightFull = fRowHeight + aRowElement.getFullYSum ();

      if (bOnTable1)
      {
        if (fUsedTable1HeightFull + fRowHeightFull <= fAvailableHeight)
        {
          // Row fits in first table without a change
          aTable1.addRow (aRowElement);
          fUsedTable1Width = Math.max (fUsedTable1Width, fRowWidth);
          fUsedTable1WidthFull = Math.max (fUsedTable1WidthFull, fRowWidthFull);
          fUsedTable1Height += fRowHeight;
          fUsedTable1HeightFull += fRowHeightFull;
          aTable1RowWidth.add (Float.valueOf (fRowWidth));
          aTable1RowHeight.add (Float.valueOf (fRowHeight));
        }
        else
        {
          // Row does not fit - check if it can be splitted
          bOnTable1 = false;
          // try to split the row
          boolean bSplittedRow = false;
          if (aRowElement.isSplittable ())
          {
            // don't override fTable1Width
            final float fWidth = Math.max (fUsedTable1Width, fRowWidth);
            final float fWidthFull = Math.max (fUsedTable1WidthFull, fRowWidthFull);

            final float fAvailableSplitWidth = fWidth;
            final float fAvailableSplitHeight = fAvailableHeight - fUsedTable1HeightFull - aRowElement.getFullYSum ();

            if (PLDebug.isDebugSplit ())
              PLDebug.debugSplit (this,
                                  "Trying to split " +
                                        aRowElement.getDebugID () +
                                        " into pieces for split width " +
                                        fAvailableSplitWidth +
                                        " and height " +
                                        fAvailableSplitHeight);

            // Try to split the element contained in the row (without padding
            // and margin of the element)
            final PLSplitResult aSplitResult = aRowElement.getAsSplittable ().splitElements (fAvailableSplitWidth,
                                                                                             fAvailableSplitHeight);

            if (aSplitResult != null)
            {
              final IPLRenderableObject  aTable1RowElement = aSplitResult.getFirstElement ().getElement ();
              aTable1.addRow (aTable1RowElement);
              fUsedTable1Width = fWidth;
              fUsedTable1WidthFull = fWidthFull;
              final float fTable1RowHeight = aSplitResult.getFirstElement ().getHeight ();
              fUsedTable1Height += fTable1RowHeight;
              fUsedTable1HeightFull += fTable1RowHeight + aTable1RowElement.getFullYSum ();
              aTable1RowWidth.add (Float.valueOf (fWidth));
              aTable1RowHeight.add (Float.valueOf (fTable1RowHeight));

              final IPLRenderableObject  aTable2RowElement = aSplitResult.getSecondElement ().getElement ();
              aTable2.addRow (aTable2RowElement);
              fUsedTable2Width = fWidth;
              final float fTable2RowHeight = aSplitResult.getSecondElement ().getHeight ();
              final float fTable2RowHeightFull = fTable2RowHeight + aTable2RowElement.getFullYSum ();
              fUsedTable2Height += fTable2RowHeight;
              fUsedTable2HeightFull += fTable2RowHeightFull;
              aTable2RowWidth.add (Float.valueOf (fWidth));
              aTable2RowHeight.add (Float.valueOf (fTable2RowHeight));

              if (PLDebug.isDebugSplit ())
                PLDebug.debugSplit (this,
                                    "Split " +
                                          aRowElement.getDebugID () +
                                          " into pieces: " +
                                          aTable1RowElement.getDebugID () +
                                          " (" +
                                          aSplitResult.getFirstElement ().getHeight () +
                                          ") and " +
                                          aTable2RowElement.getDebugID () +
                                          " (" +
                                          aSplitResult.getSecondElement ().getHeight () +
                                          ")");
              bSplittedRow = true;
            }
            else
            {
              if (PLDebug.isDebugSplit ())
                PLDebug.debugSplit (this, "Failed to split " + aRowElement.getDebugID () + " into pieces");
            }
          }

          if (!bSplittedRow)
          {
            // just add the full row to the second table
            aTable2.addRow (aRowElement);
            fUsedTable2Width = Math.max (fUsedTable2Width, fRowWidth);
            fUsedTable2Height += fRowHeight;
            fUsedTable2HeightFull += fRowHeightFull;
            aTable2RowWidth.add (Float.valueOf (fRowWidth));
            aTable2RowHeight.add (Float.valueOf (fRowHeight));
          }
        }
      }
      else
      {
        // We're already on table 2
        aTable2.addRow (aRowElement);
        fUsedTable2Width = Math.max (fUsedTable2Width, fRowWidth);
        fUsedTable2Height += fRowHeight;
        fUsedTable2HeightFull += fRowHeightFull;
        aTable2RowWidth.add (Float.valueOf (fRowWidth));
        aTable2RowHeight.add (Float.valueOf (fRowHeight));
      }
    }

    if (aTable1.getRowCount () == m_nHeaderRowCount)
    {
      // Splitting makes no sense!
      if (PLDebug.isDebugSplit ())
        PLDebug.debugSplit (this, "Splitting makes no sense, because only the header row would be in table 1");
      return null;
    }

    if (aTable2.getRowCount () == m_nHeaderRowCount)
    {
      // Splitting makes no sense!
      if (PLDebug.isDebugSplit ())
        PLDebug.debugSplit (this,
                            "Splitting makes no sense, because only the header row would be in table 2 and this means the whole table 1 would match");
      return null;
    }

    // Excluding padding/margin
    aTable1.internalMarkAsPrepared (new SizeSpec (fElementWidth, fUsedTable1HeightFull));
    aTable1.m_aPreparedRowElementWidth = _getAsArray (aTable1RowWidth);
    aTable1.m_aPreparedRowElementHeight = _getAsArray (aTable1RowHeight);

    aTable2.internalMarkAsPrepared (new SizeSpec (fElementWidth, fUsedTable2HeightFull));
    aTable2.m_aPreparedRowElementWidth = _getAsArray (aTable2RowWidth);
    aTable2.m_aPreparedRowElementHeight = _getAsArray (aTable2RowHeight);

    return new PLSplitResult (new PLElementWithSize (aTable1, new SizeSpec (fElementWidth, fUsedTable1HeightFull)),
                              new PLElementWithSize (aTable2, new SizeSpec (fElementWidth, fUsedTable2HeightFull)));
  }

  @Override
  public String toString ()
  {
    return ToStringGenerator.getDerived (super.toString ())
                            .append ("width", m_aWidths)
                            .append ("headerRowCount", m_nHeaderRowCount)
                            .toString ();
  }

  /**
   * Create a new table with the specified percentages.
   *
   * @param aPercentages
   *        The array to use. The sum of all percentages should be ≤ 100. May
   *        neither be null nor empty.
   * @return The created {@link PLTable} and never null.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static PLTable createWithPercentage (@Nonnull @Nonempty final float... aPercentages)
  {
    ValueEnforcer.notEmpty (aPercentages, "Percentages");

    final ICommonsList  aWidths = new CommonsArrayList<> (aPercentages.length);
    for (final float fPercentage : aPercentages)
      aWidths.add (WidthSpec.perc (fPercentage));
    return new PLTable (aWidths);
  }

  /**
   * Create a new table with evenly sized columns.
   *
   * @param nColumnCount
   *        The number of columns to use. Must be > 0.
   * @return The created {@link PLTable} and never null.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static PLTable createWithEvenlySizedColumns (@Nonnegative final int nColumnCount)
  {
    ValueEnforcer.isGT0 (nColumnCount, "ColumnCount");

    final ICommonsList  aWidths = new CommonsArrayList<> (nColumnCount);
    for (int i = 0; i < nColumnCount; ++i)
      aWidths.add (WidthSpec.star ());
    return new PLTable (aWidths);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy