/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/* $Id: TableStepper.java 1820665 2018-01-09 14:16:04Z ssteiner $ */

package org.apache.fop.layoutmgr.table;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.fo.Constants;
import org.apache.fop.fo.flow.table.EffRow;
import org.apache.fop.fo.flow.table.GridUnit;
import org.apache.fop.fo.flow.table.PrimaryGridUnit;
import org.apache.fop.layoutmgr.BreakElement;
import org.apache.fop.layoutmgr.Keep;
import org.apache.fop.layoutmgr.KnuthBlockBox;
import org.apache.fop.layoutmgr.KnuthBox;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.KnuthPenalty;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.util.BreakUtil;

This class processes row groups to create combined element lists for tables.
/** * This class processes row groups to create combined element lists for tables. */
public class TableStepper {
Logger
/** Logger **/
private static Log log = LogFactory.getLog(TableStepper.class); private TableContentLayoutManager tclm; private EffRow[] rowGroup;
Number of columns in the row group.
/** Number of columns in the row group. */
private int columnCount; private int totalHeight; private int previousRowsLength; private int activeRowIndex; private boolean rowFinished;
Cells spanning the current row.
/** Cells spanning the current row. */
private List activeCells = new LinkedList();
Cells that will start the next row.
/** Cells that will start the next row. */
private List nextActiveCells = new LinkedList();
True if the next row is being delayed, that is, if cells spanning the current and the next row have steps smaller than the next row's first step. In this case the next row may be extended to offer additional break possibilities.
/** * True if the next row is being delayed, that is, if cells spanning the current and * the next row have steps smaller than the next row's first step. In this case the * next row may be extended to offer additional break possibilities. */
private boolean delayingNextRow;
The first step for a row. This is the minimal step necessary to include some content from all the cells starting the row.
/** * The first step for a row. This is the minimal step necessary to include some * content from all the cells starting the row. */
private int rowFirstStep;
Flag used to produce an infinite penalty if the height of the current row is smaller than the first step for that row (may happen with row-spanning cells).
See Also:
  • considerRowLastStep(int)
/** * Flag used to produce an infinite penalty if the height of the current row is * smaller than the first step for that row (may happen with row-spanning cells). * * @see #considerRowLastStep(int) */
private boolean rowHeightSmallerThanFirstStep;
The class of the next break. One of Constants.EN_AUTO, Constants.EN_COLUMN, Constants.EN_PAGE, Constants.EN_EVEN_PAGE, Constants.EN_ODD_PAGE. Defaults to EN_AUTO.
/** * The class of the next break. One of {@link Constants#EN_AUTO}, * {@link Constants#EN_COLUMN}, {@link Constants#EN_PAGE}, * {@link Constants#EN_EVEN_PAGE}, {@link Constants#EN_ODD_PAGE}. Defaults to * EN_AUTO. */
private int nextBreakClass;
Main constructor
Params:
  • tclm – The parent TableContentLayoutManager
/** * Main constructor * @param tclm The parent TableContentLayoutManager */
public TableStepper(TableContentLayoutManager tclm) { this.tclm = tclm; this.columnCount = tclm.getTableLM().getTable().getNumberOfColumns(); }
Initializes the fields of this instance to handle a new row group.
Params:
  • rows – the new row group to handle
/** * Initializes the fields of this instance to handle a new row group. * * @param rows the new row group to handle */
private void setup(EffRow[] rows) { rowGroup = rows; previousRowsLength = 0; activeRowIndex = 0; activeCells.clear(); nextActiveCells.clear(); delayingNextRow = false; rowFirstStep = 0; rowHeightSmallerThanFirstStep = false; } private void calcTotalHeight() { totalHeight = 0; for (EffRow aRowGroup : rowGroup) { totalHeight += aRowGroup.getHeight().getOpt(); } if (log.isDebugEnabled()) { log.debug("totalHeight=" + totalHeight); } } private int getMaxRemainingHeight() { int maxW = 0; for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; int remain = activeCell.getRemainingLength(); PrimaryGridUnit pgu = activeCell.getPrimaryGridUnit(); for (int i = activeRowIndex + 1; i < pgu.getRowIndex() - rowGroup[0].getIndex() + pgu.getCell().getNumberRowsSpanned(); i++) { remain -= rowGroup[i].getHeight().getOpt(); } maxW = Math.max(maxW, remain); } for (int i = activeRowIndex + 1; i < rowGroup.length; i++) { maxW += rowGroup[i].getHeight().getOpt(); } return maxW; }
Creates ActiveCell instances for cells starting on the row at the given index.
Params:
  • activeCellList – the list that will hold the active cells
  • rowIndex – the index of the row from which cells must be activated
/** * Creates ActiveCell instances for cells starting on the row at the given index. * * @param activeCellList the list that will hold the active cells * @param rowIndex the index of the row from which cells must be activated */
private void activateCells(List activeCellList, int rowIndex) { EffRow row = rowGroup[rowIndex]; for (int i = 0; i < columnCount; i++) { GridUnit gu = row.getGridUnit(i); if (!gu.isEmpty() && gu.isPrimary()) { assert (gu instanceof PrimaryGridUnit); activeCellList.add(new ActiveCell((PrimaryGridUnit) gu, row, rowIndex, previousRowsLength, getTableLM())); } } }
Creates the combined element list for a row group.
Params:
  • context – Active LayoutContext
  • rows – the row group
  • bodyType – Indicates what type of body is processed (body, header or footer)
Returns:the combined element list
/** * Creates the combined element list for a row group. * @param context Active LayoutContext * @param rows the row group * @param bodyType Indicates what type of body is processed (body, header or footer) * @return the combined element list */
public LinkedList getCombinedKnuthElementsForRowGroup(LayoutContext context, EffRow[] rows, int bodyType) { setup(rows); activateCells(activeCells, 0); calcTotalHeight(); int cumulateLength = 0; // Length of the content accumulated before the break TableContentPosition lastTCPos = null; LinkedList returnList = new LinkedList(); int laststep = 0; int step = getFirstStep(); do { int maxRemainingHeight = getMaxRemainingHeight(); int penaltyOrGlueLen = step + maxRemainingHeight - totalHeight; int boxLen = step - cumulateLength - Math.max(0, penaltyOrGlueLen)/* penalty, if any */; cumulateLength += boxLen + Math.max(0, -penaltyOrGlueLen)/* the glue, if any */; if (log.isDebugEnabled()) { log.debug("Next step: " + step + " (+" + (step - laststep) + ")"); log.debug(" max remaining height: " + maxRemainingHeight); if (penaltyOrGlueLen >= 0) { log.debug(" box = " + boxLen + " penalty = " + penaltyOrGlueLen); } else { log.debug(" box = " + boxLen + " glue = " + (-penaltyOrGlueLen)); } } LinkedList footnoteList = new LinkedList(); //Put all involved grid units into a list List cellParts = new java.util.ArrayList(activeCells.size()); for (Object activeCell2 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell2; CellPart part = activeCell.createCellPart(); cellParts.add(part); activeCell.addFootnotes(footnoteList); } //Create elements for step TableContentPosition tcpos = new TableContentPosition(getTableLM(), cellParts, rowGroup[activeRowIndex]); if (delayingNextRow) { tcpos.setNewPageRow(rowGroup[activeRowIndex + 1]); } if (returnList.size() == 0) { tcpos.setFlag(TableContentPosition.FIRST_IN_ROWGROUP, true); } lastTCPos = tcpos; // TODO TableStepper should remain as footnote-agnostic as possible if (footnoteList.isEmpty()) { returnList.add(new KnuthBox(boxLen, tcpos, false)); } else { returnList.add(new KnuthBlockBox(boxLen, footnoteList, tcpos, false)); } int effPenaltyLen = Math.max(0, penaltyOrGlueLen); TableHFPenaltyPosition penaltyPos = new TableHFPenaltyPosition(getTableLM()); if (bodyType == TableRowIterator.BODY) { if (!getTableLM().getTable().omitHeaderAtBreak()) { effPenaltyLen += tclm.getHeaderNetHeight(); penaltyPos.headerElements = tclm.getHeaderElements(); } if (!getTableLM().getTable().omitFooterAtBreak()) { effPenaltyLen += tclm.getFooterNetHeight(); penaltyPos.footerElements = tclm.getFooterElements(); } } Keep keep = getTableLM().getKeepTogether(); int stepPenalty = 0; for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; keep = keep.compare(activeCell.getKeepWithNext()); stepPenalty = Math.max(stepPenalty, activeCell.getPenaltyValue()); } if (!rowFinished) { keep = keep.compare(rowGroup[activeRowIndex].getKeepTogether()); } else if (activeRowIndex < rowGroup.length - 1) { keep = keep.compare(rowGroup[activeRowIndex].getKeepWithNext()); keep = keep.compare(rowGroup[activeRowIndex + 1].getKeepWithPrevious()); nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass, rowGroup[activeRowIndex].getBreakAfter()); nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass, rowGroup[activeRowIndex + 1].getBreakBefore()); } int p = keep.getPenalty(); if (rowHeightSmallerThanFirstStep) { rowHeightSmallerThanFirstStep = false; p = KnuthPenalty.INFINITE; } p = Math.max(p, stepPenalty); int breakClass = keep.getContext(); if (nextBreakClass != Constants.EN_AUTO) { log.trace("Forced break encountered"); p = -KnuthPenalty.INFINITE; //Overrides any keeps (see 4.8 in XSL 1.0) breakClass = nextBreakClass; } returnList.add(new BreakElement(penaltyPos, effPenaltyLen, p, breakClass, context)); laststep = step; step = getNextStep(); if (penaltyOrGlueLen < 0) { if (step < 0) { returnList.add(new KnuthGlue(0, -penaltyOrGlueLen, 0, new Position(null), true)); } else { returnList.add(new KnuthGlue(-penaltyOrGlueLen, 0, 0, new Position(null), true)); } } } while (step >= 0); assert !returnList.isEmpty(); lastTCPos.setFlag(TableContentPosition.LAST_IN_ROWGROUP, true); return returnList; }
Returns the first step for the current row group.
Returns:the first step for the current row group
/** * Returns the first step for the current row group. * * @return the first step for the current row group */
private int getFirstStep() { computeRowFirstStep(activeCells); signalRowFirstStep(); int minStep = considerRowLastStep(rowFirstStep); signalNextStep(minStep); return minStep; }
Returns the next break possibility.
Returns:the next step
/** * Returns the next break possibility. * * @return the next step */
private int getNextStep() { if (rowFinished) { if (activeRowIndex == rowGroup.length - 1) { // The row group is finished, no next step return -1; } rowFinished = false; removeCellsEndingOnCurrentRow(); log.trace("Delaying next row"); delayingNextRow = true; } if (delayingNextRow) { int minStep = computeMinStep(); if (minStep < 0 || minStep >= rowFirstStep || minStep > rowGroup[activeRowIndex].getExplicitHeight().getMax()) { if (log.isTraceEnabled()) { log.trace("Step = " + minStep); } delayingNextRow = false; minStep = rowFirstStep; switchToNextRow(); signalRowFirstStep(); minStep = considerRowLastStep(minStep); } signalNextStep(minStep); return minStep; } else { int minStep = computeMinStep(); minStep = considerRowLastStep(minStep); signalNextStep(minStep); return minStep; } }
Computes the minimal necessary step to make the next row fit. That is, so such as cell on the next row can contribute some content.
Params:
  • cells – the cells occupying the next row (may include cells starting on previous rows and spanning over this one)
/** * Computes the minimal necessary step to make the next row fit. That is, so such as * cell on the next row can contribute some content. * * @param cells the cells occupying the next row (may include cells starting on * previous rows and spanning over this one) */
private void computeRowFirstStep(List cells) { for (Object cell : cells) { ActiveCell activeCell = (ActiveCell) cell; rowFirstStep = Math.max(rowFirstStep, activeCell.getFirstStep()); } }
Computes the next minimal step.
Returns:the minimal step from the active cells, < 0 if there is no such step
/** * Computes the next minimal step. * * @return the minimal step from the active cells, &lt; 0 if there is no such step */
private int computeMinStep() { int minStep = Integer.MAX_VALUE; boolean stepFound = false; for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; int nextStep = activeCell.getNextStep(); if (nextStep >= 0) { stepFound = true; minStep = Math.min(minStep, nextStep); } } if (stepFound) { return minStep; } else { return -1; } }
Signals the first step to the active cells, to allow them to add more content to the step if possible.
See Also:
  • signalRowFirstStep.signalRowFirstStep(int)
/** * Signals the first step to the active cells, to allow them to add more content to * the step if possible. * * @see ActiveCell#signalRowFirstStep(int) */
private void signalRowFirstStep() { for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; activeCell.signalRowFirstStep(rowFirstStep); } }
Signals the next selected step to the active cells.
Params:
  • step – the next step
/** * Signals the next selected step to the active cells. * * @param step the next step */
private void signalNextStep(int step) { nextBreakClass = Constants.EN_AUTO; for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass, activeCell.signalNextStep(step)); } }
Determines if the given step will finish the current row, and if so switch to the last step for this row.

If the row is finished then the after borders for the cell may change (their conditionalities no longer apply for the cells ending on the current row). Thus the final step may grow with respect to the given one.

In more rare occasions, the given step may correspond to the first step of a row-spanning cell, and may be greater than the height of the current row (consider, for example, an unbreakable cell spanning three rows). In such a case the returned step will correspond to the row height and a flag will be set to produce an infinite penalty for this step. This will prevent the breaking algorithm from choosing this break, but still allow to create the appropriate TableContentPosition for the cells ending on the current row.

Params:
  • step – the next step
Returns:the updated step if any
/** * Determines if the given step will finish the current row, and if so switch to the * last step for this row. * <p>If the row is finished then the after borders for the cell may change (their * conditionalities no longer apply for the cells ending on the current row). Thus the * final step may grow with respect to the given one.</p> * <p>In more rare occasions, the given step may correspond to the first step of a * row-spanning cell, and may be greater than the height of the current row (consider, * for example, an unbreakable cell spanning three rows). In such a case the returned * step will correspond to the row height and a flag will be set to produce an * infinite penalty for this step. This will prevent the breaking algorithm from * choosing this break, but still allow to create the appropriate TableContentPosition * for the cells ending on the current row.</p> * * @param step the next step * @return the updated step if any */
private int considerRowLastStep(int step) { rowFinished = true; for (Object activeCell3 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell3; if (activeCell.endsOnRow(activeRowIndex)) { if (!activeCell.finishes(step)) { rowFinished = false; } } } if (rowFinished) { if (log.isTraceEnabled()) { log.trace("Step = " + step); log.trace("Row finished, computing last step"); } int maxStep = 0; for (Object activeCell2 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell2; if (activeCell.endsOnRow(activeRowIndex)) { maxStep = Math.max(maxStep, activeCell.getLastStep()); } } if (log.isTraceEnabled()) { log.trace("Max step: " + maxStep); } for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; activeCell.endRow(activeRowIndex); if (!activeCell.endsOnRow(activeRowIndex)) { activeCell.signalRowLastStep(maxStep); } } if (maxStep < step) { log.trace("Row height smaller than first step, produced penalty will be infinite"); rowHeightSmallerThanFirstStep = true; } step = maxStep; prepareNextRow(); } return step; }
Pre-activates the cells that will start the next row, and computes the first step for that row.
/** * Pre-activates the cells that will start the next row, and computes the first step * for that row. */
private void prepareNextRow() { if (activeRowIndex < rowGroup.length - 1) { previousRowsLength += rowGroup[activeRowIndex].getHeight().getOpt(); activateCells(nextActiveCells, activeRowIndex + 1); if (log.isTraceEnabled()) { log.trace("Computing first step for row " + (activeRowIndex + 2)); } computeRowFirstStep(nextActiveCells); if (log.isTraceEnabled()) { log.trace("Next first step = " + rowFirstStep); } } } private void removeCellsEndingOnCurrentRow() { for (Iterator iter = activeCells.iterator(); iter.hasNext();) { ActiveCell activeCell = (ActiveCell) iter.next(); if (activeCell.endsOnRow(activeRowIndex)) { iter.remove(); } } }
Actually switches to the next row, increasing activeRowIndex and transferring to activeCells the cells starting on the next row.
/** * Actually switches to the next row, increasing activeRowIndex and transferring to * activeCells the cells starting on the next row. */
private void switchToNextRow() { activeRowIndex++; if (log.isTraceEnabled()) { log.trace("Switching to row " + (activeRowIndex + 1)); } for (Object activeCell1 : activeCells) { ActiveCell activeCell = (ActiveCell) activeCell1; activeCell.nextRowStarts(); } activeCells.addAll(nextActiveCells); nextActiveCells.clear(); }
Returns:the table layout manager
/** @return the table layout manager */
private TableLayoutManager getTableLM() { return this.tclm.getTableLM(); } }