/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.layout.process; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.ElementAlignment; import org.pentaho.reporting.engine.classic.core.InvalidReportStateException; import org.pentaho.reporting.engine.classic.core.layout.ModelPrinter; import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes; import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox; import org.pentaho.reporting.engine.classic.core.layout.model.PageBreakPositionList; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode; import org.pentaho.reporting.engine.classic.core.layout.model.context.StaticBoxLayoutProperties; import org.pentaho.reporting.engine.classic.core.layout.process.util.BasePaginationTableState; import org.pentaho.reporting.engine.classic.core.layout.process.util.PaginationShiftState; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; /** * A helper class that contains generic methods that would distract me from the actual pagination logic. */ public final class PaginationStepLib { private static final Log logger = LogFactory.getLog( PaginationStepLib.class ); private PaginationStepLib() { } public static void configureBreakUtility( final PageBreakPositionList breakUtility, final LogicalPageBox pageBox, final long[] allCurrentBreaks, final long reservedHeight, final long lastBreakLocal ) { final PageBreakPositionList allPreviousBreak = pageBox.getAllVerticalBreaks(); breakUtility.copyFrom( allPreviousBreak ); final long pageOffset = pageBox.getPageOffset(); final long headerHeight = pageBox.getHeaderArea().getHeight(); // Then add all new breaks (but take the header and footer-size into account) .. if ( allCurrentBreaks.length == 1 ) { breakUtility.addMajorBreak( pageOffset, headerHeight ); breakUtility.addMajorBreak( ( lastBreakLocal - reservedHeight ) + pageOffset, headerHeight ); } else { // more than one physical page; therefore header and footer are each on a separate canvas .. breakUtility.addMajorBreak( pageOffset, headerHeight ); final int breakCount = allCurrentBreaks.length - 1; for ( int i = 1; i < breakCount; i++ ) { final long aBreak = allCurrentBreaks[i]; breakUtility.addMinorBreak( pageOffset + ( aBreak - headerHeight ) ); } breakUtility.addMajorBreak( pageOffset + ( lastBreakLocal - reservedHeight ), headerHeight ); } } public static void assertProgress( final LogicalPageBox pageBox ) { final RenderNode lastChild = pageBox.getLastChild(); if ( lastChild != null ) { final long lastChildY2 = lastChild.getY() + lastChild.getHeight(); if ( lastChildY2 < pageBox.getHeight() ) { // ModelPrinter.print(pageBox); throw new IllegalStateException( "Assertation failed: Pagination did not proceed: " + lastChildY2 + " < " + pageBox.getHeight() ); } } } public static long restrictPageAreaHeights( final LogicalPageBox pageBox, final long[] allCurrentBreaks ) { final BlockRenderBox headerArea = pageBox.getHeaderArea(); final long headerHeight = Math.min( headerArea.getHeight(), allCurrentBreaks[0] ); headerArea.setHeight( headerHeight ); final BlockRenderBox footerArea = pageBox.getFooterArea(); final BlockRenderBox repeatFooterArea = pageBox.getRepeatFooterArea(); if ( allCurrentBreaks.length > 1 ) { final long lastBreakLocal = allCurrentBreaks[allCurrentBreaks.length - 1]; final long lastPageHeight = lastBreakLocal - allCurrentBreaks[allCurrentBreaks.length - 2]; final long footerHeight = Math.min( footerArea.getHeight(), lastPageHeight ); footerArea.setHeight( footerHeight ); final long repeatFooterHeight = Math.min( repeatFooterArea.getHeight(), lastPageHeight ); repeatFooterArea.setHeight( repeatFooterHeight ); } final long footerHeight = footerArea.getHeight(); final long repeatFooterHeight = repeatFooterArea.getHeight(); // Assertion: Make sure that we do not run into a infinite loop.. return headerHeight + repeatFooterHeight + footerHeight; } public static void assertBlockPosition( final RenderBox box, final long shift ) { if ( box.getLayoutNodeType() == LayoutNodeTypes.TYPE_BOX_TABLE_SECTION ) { // no point in testing table-sections, as the header will be an out-of-order band. return; } final boolean error; final long expectedYPos; if ( box.getPrev() != null ) { error = true; expectedYPos = box.getPrev().getY() + box.getPrev().getHeight(); } else { if ( box.getParent() != null ) { error = false; expectedYPos = box.getParent().getY(); final Object parentVAlignment = box.getParent().getStyleSheet().getStyleProperty( ElementStyleKeys.VALIGNMENT ); if ( parentVAlignment != null && ElementAlignment.TOP.equals( parentVAlignment ) == false ) { return; } } else { error = true; expectedYPos = 0; } } final long realY = box.getY() + shift; if ( realY != expectedYPos ) { final long additionalShift = expectedYPos - realY; final long realShift = shift + additionalShift; if ( error ) { ModelPrinter.INSTANCE.print( box ); ModelPrinter.INSTANCE.print( ModelPrinter.getRoot( box ) ); throw new InvalidReportStateException( String.format( "Assert: Shift is not as expected: " + "realY=%d != expectation=%d; Shift=%d; AdditionalShift=%d; RealShift=%d", realY, expectedYPos, shift, additionalShift, realShift ) ); } else { if ( logger.isDebugEnabled() ) { logger.debug( String.format( "Assert: Shift is not as expected: realY=%d != expectation=%d; Shift=%d; " + "AdditionalShift=%d; RealShift=%d (False positive if block box has valign != TOP", realY, expectedYPos, shift, additionalShift, realShift ) ); } } } } /** * Computes the height that will be required on this page to display at least some parts of the box. * * @param box * the box for which the height is computed * @return the height in micro-points. */ public static long computeNonBreakableBoxHeight( final RenderBox box, final PaginationShiftState shiftState, final BasePaginationTableState tableState ) { // must return the reserved space starting from box's y position. final long widowSize = getWidowConstraint( box, shiftState, tableState ); final StaticBoxLayoutProperties sblp = box.getStaticBoxLayoutProperties(); if ( sblp.isAvoidPagebreakInside() && box.getRestrictFinishedClearOut() != RenderBox.RestrictFinishClearOut.RESTRICTED ) { return Math.max( widowSize, box.getHeight() ); } final int nodeType = box.getLayoutNodeType(); if ( ( nodeType & LayoutNodeTypes.MASK_BOX_BLOCK ) != LayoutNodeTypes.MASK_BOX_BLOCK ) { // Canvas, row or inline boxes have no notion of lines, and therefore they cannot have orphans and widows. return widowSize; } if ( isOrphanConstraintNeeded( box, shiftState, tableState ) == false ) { return widowSize; } final long orphanHeight = box.getOrphanConstraintSize(); if ( widowSize + orphanHeight > box.getHeight() ) { // if the widows and orphan areas overlap, then the box becomes non-breakable. return Math.max( widowSize, box.getHeight() ); } return Math.max( orphanHeight, widowSize ); } /** * Widow constraint evaluation is skipped if * <p/> * (a) the box has a widow/orphan-parent defining that sits on the current page's page-offset and (b) this parent's * orphan-restricted pagebreak-exclusion zone includes this box. * * @param box * @param shiftState * @param tableState * @return */ private static boolean isOrphanConstraintNeeded( final RenderBox box, final PaginationShiftState shiftState, final BasePaginationTableState tableState ) { final long boxY = box.getY() + shiftState.getShiftForNextChild(); final long pageOffset = tableState.getPageOffset( boxY ); if ( pageOffset == boxY ) { // no need to set a widow constraint, we are not shifting anyway .. return false; } if ( box.getRestrictFinishedClearOut() != RenderBox.RestrictFinishClearOut.RESTRICTED ) { return false; } if ( isBoxInsideParentOrphanZoneOnThisPage( box, pageOffset, boxY ) ) { return false; } return true; } private static boolean isBoxInsideParentOrphanZoneOnThisPage( final RenderBox box, final long pageOffset, final long boxYShifted ) { RenderBox parent = box.getParent(); while ( parent != null ) { if ( parent.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { break; } if ( parent.getY() < pageOffset ) { // once the parent is sitting on the previous page, we no longer need to ask it .. break; } if ( parent.getOrphanConstraintSize() == 0 ) { parent = parent.getParent(); continue; } final long constraintBoundary = parent.getY() + parent.getOrphanConstraintSize(); if ( constraintBoundary > boxYShifted ) { // this parent is not relevant. return true; } parent = parent.getParent(); } return false; } public static long getWidowConstraint( final RenderBox box, final PaginationShiftState shiftState, final BasePaginationTableState tableState ) { if ( box.isWidowBox() == false ) { return 0; } final long boxY = box.getY() + shiftState.getShiftForNextChild(); long retval = 0; RenderBox parent = box.getParent(); final long pageOffset = tableState.getPageOffset(); if ( pageOffset == boxY ) { // no need to set a widow constraint, we are not shifting anyway .. return 0; } while ( parent != null ) { if ( parent.getRestrictFinishedClearOut() == RenderBox.RestrictFinishClearOut.UNRESTRICTED ) { break; } if ( parent.getWidowConstraintSize() == 0 ) { parent = parent.getParent(); continue; } final long y2 = parent.getY2(); final long constraintBoundary; if ( parent.getY() < pageOffset ) { constraintBoundary = y2 - Math.max( 0, parent.getWidowConstraintSize() ); } else { constraintBoundary = y2 - Math.max( 0, parent.getWidowConstraintSizeWithKeepTogether() ); } if ( constraintBoundary == boxY ) { retval = Math.max( retval, y2 - boxY ); } parent = parent.getParent(); } return retval; } }