/* * 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) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.modules.output.table.base; import java.awt.Shape; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import org.pentaho.reporting.engine.classic.core.layout.model.BorderEdge; import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes; import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox; import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContent; import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContentBox; import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData; import org.pentaho.reporting.engine.classic.core.layout.process.util.ProcessUtility; import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys; import org.pentaho.reporting.engine.classic.core.style.StyleSheet; import org.pentaho.reporting.engine.classic.core.util.ShapeDrawable; import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds; import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility; import org.pentaho.reporting.libraries.resourceloader.factory.drawable.DrawableWrapper; /** * The sheet layout is used to build the background map and to collect the x- and y-cell-borders. */ public class SheetLayout implements SlimSheetLayout, Cloneable { private static final SheetLayoutTableCellDefinition MARKER_DEFINITION = new SheetLayoutTableCellDefinition(); /** * An internal flag indicating that the upper or left bounds should be used. */ private static final boolean UPPER_BOUNDS = true; private static final boolean LOWER_BOUNDS = false; /** * A flag, defining whether to use strict layout mode. */ private final boolean strict; /** * The XBounds, all vertical cell boundaries (as CoordinateMappings). */ private TableCutList xBounds; /** * The YBounds, all vertical cell boundaries (as CoordinateMappings). */ private TableCutList yBounds; /** * The right border of the grid. This is needed when not being in the strict mode. */ private long xMaxBounds; private long yMaxBounds; private boolean ellipseAsRectangle; /** * Creates a new TableGrid-object. If strict mode is enabled, all cell bounds are used to create the table grid, * resulting in a more complex layout. * * @param strict * the strict mode for the layout. * @param ellipseAsRectangle * a flag that defines whether ellipse-objects are translated into rectangles and therefore create * backgrounds. */ public SheetLayout( final boolean strict, final boolean ellipseAsRectangle ) { this.ellipseAsRectangle = ellipseAsRectangle; this.xBounds = new TableCutList( 50, true ); this.yBounds = new TableCutList( 2500, true ); this.strict = strict; this.xMaxBounds = 0; this.yMaxBounds = 0; this.ensureXMapping( 0, Boolean.FALSE ); this.ensureYMapping( 0, Boolean.FALSE ); } public SheetLayout( OutputProcessorMetaData metaData ) { this( metaData.isFeatureSupported( AbstractTableOutputProcessor.STRICT_LAYOUT ), metaData .isFeatureSupported( AbstractTableOutputProcessor.TREAT_ELLIPSE_AS_RECTANGLE ) ); } public SheetLayout derive() { SheetLayout clone = clone(); clone.clearVerticalInfo(); return clone; } public SheetLayout clone() { try { SheetLayout clone = (SheetLayout) super.clone(); clone.xBounds = xBounds.clone(); clone.yBounds = yBounds.clone(); return clone; } catch ( CloneNotSupportedException e ) { throw new IllegalStateException(); } } private SheetLayoutTableCellDefinition createBackground( final RenderBox box ) { if ( box.getBoxDefinition().getBorder().isEmpty() == false ) { return MARKER_DEFINITION; } final StyleSheet styleSheet = box.getStyleSheet(); if ( styleSheet.getStyleProperty( ElementStyleKeys.BACKGROUND_COLOR ) != null ) { return MARKER_DEFINITION; } if ( styleSheet.getStyleProperty( ElementStyleKeys.ANCHOR_NAME ) != null ) { return MARKER_DEFINITION; } return null; } private SheetLayoutTableCellDefinition computeLegacyBackground( final RenderBox box, final long shift ) { // For legacy reasons: A single ReplacedContent in a canvas means, we may have a old-style border and // background definition. if ( box.getNodeType() != LayoutNodeTypes.TYPE_BOX_CONTENT ) { return null; } final RenderableReplacedContentBox contentBox = (RenderableReplacedContentBox) box; final StyleSheet styleSheet = box.getStyleSheet(); final RenderableReplacedContent rpc = contentBox.getContent(); final Object rawContentObject = rpc.getRawObject(); if ( rawContentObject instanceof DrawableWrapper == false ) { return null; } final DrawableWrapper wrapper = (DrawableWrapper) rawContentObject; final Object rawbackend = wrapper.getBackend(); if ( rawbackend instanceof ShapeDrawable == false ) { return null; } final ShapeDrawable drawable = (ShapeDrawable) rawbackend; final Shape rawObject = drawable.getShape(); final boolean draw = styleSheet.getBooleanStyleProperty( ElementStyleKeys.DRAW_SHAPE ); if ( draw && rawObject instanceof Line2D ) { final int lineType; final long coordinate; final Line2D line = (Line2D) rawObject; final boolean vertical = line.getX1() == line.getX2(); final boolean horizontal = line.getY1() == line.getY2(); if ( vertical && horizontal ) { return null; } if ( vertical ) { lineType = SheetLayoutTableCellDefinition.LINE_HINT_VERTICAL; coordinate = StrictGeomUtility.toInternalValue( line.getX1() ) + box.getX(); } else if ( horizontal ) { lineType = SheetLayoutTableCellDefinition.LINE_HINT_HORIZONTAL; coordinate = StrictGeomUtility.toInternalValue( line.getY1() ) + box.getY() + shift; } else { return null; } return new SheetLayoutTableCellDefinition( lineType, coordinate ); } if ( rawObject instanceof Rectangle2D || ( ellipseAsRectangle && rawObject instanceof Ellipse2D ) ) { if ( draw ) { final BorderEdge edge = ProcessUtility.produceBorderEdge( box.getStyleSheet() ); if ( edge != null ) { return MARKER_DEFINITION; } } if ( styleSheet.getBooleanStyleProperty( ElementStyleKeys.FILL_SHAPE ) ) { return MARKER_DEFINITION; } return null; } if ( rawObject instanceof RoundRectangle2D ) { if ( draw ) { // the beast has a border .. final BorderEdge edge = ProcessUtility.produceBorderEdge( box.getStyleSheet() ); if ( edge != null ) { return MARKER_DEFINITION; } } if ( styleSheet.getBooleanStyleProperty( ElementStyleKeys.FILL_SHAPE ) ) { return MARKER_DEFINITION; } return null; } return null; } /** * Adds the bounds of the given TableCellData to the grid. The bounds given must be the same as the bounds of the * element, or the layouting might produce surprising results. * <p/> * This method will do nothing, if the element has a width and height of zero and does not define any anchors. * * @param element * the position that should be added to the grid (might be null). * @param offset * the vertical shift which adjusts the visual position of the content. * @param headerSize * the vertical shift which adjusts the visual position of the content. * @return true, if the box has not changed and can be safely skipped. * @throws NullPointerException * if the bounds are null */ public boolean add( final RenderBox element, final long offset, final long headerSize, final long maxHeight ) { final long shift = headerSize - offset; final long shiftedY = element.getY() + shift; final long elementY; if ( shiftedY < 0 ) { if ( ( shiftedY + element.getHeight() ) < 0 ) { // The box will not be visible at all. (Should not happen in a sane environment ..) return true; } elementY = 0; } else { elementY = shiftedY; } final Object state = element.getTableExportState(); final TableExportRenderBoxState tablestate; final SheetLayoutTableCellDefinition background; final boolean unmodified; if ( state != null ) { tablestate = (TableExportRenderBoxState) state; if ( tablestate.getBackgroundDefinitionAge() == element.getChangeTracker() ) { background = tablestate.getBackgroundDefinition(); unmodified = true; } else { background = createBackground( element ); tablestate.setBackgroundDefinition( background, element.getChangeTracker() ); unmodified = false; } } else { tablestate = new TableExportRenderBoxState(); element.setTableExportState( tablestate ); background = createBackground( element ); tablestate.setBackgroundDefinition( background, element.getChangeTracker() ); unmodified = false; } if ( addLine( element, background, elementY, shiftedY, unmodified, headerSize ) ) { return unmodified; } final long elementX = element.getX(); final long elementRightX = ( element.getWidth() + elementX ); final long elementBottomY = element.getHeight() + shiftedY; // collect the bounds and add them to the xBounds and yBounds collection // if necessary... if ( unmodified == false ) { ensureXMapping( elementX, Boolean.FALSE ); } if ( elementY >= headerSize ) { ensureYMapping( elementY, Boolean.FALSE ); } // an end cut is auxilary, if it is not a background and the layout is not strict final Boolean aux; if ( ( background == null ) && ( isStrict() == false ) ) { aux = Boolean.TRUE; } else { aux = Boolean.FALSE; } ensureXMapping( elementRightX, aux ); if ( elementBottomY >= headerSize && elementBottomY <= maxHeight ) { ensureYMapping( elementBottomY, aux ); } // update the collected maximums return unmodified; } public void addRenderableContent( final RenderableReplacedContentBox element, final long offset, final long headerSize, final long maxHeight ) { final long shift = headerSize - offset; final long shiftedY = element.getY() + shift; final long elementY; if ( shiftedY < 0 ) { if ( ( shiftedY + element.getHeight() ) < 0 ) { // The box will not be visible at all. (Should not happen in a sane environment ..) return; } elementY = 0; } else { elementY = shiftedY; } final SheetLayoutTableCellDefinition background = computeLegacyBackground( element, shift ); if ( addLine( element, background, elementY, shiftedY, false, headerSize ) ) { return; } final long elementX = element.getX(); final long elementRightX = ( element.getWidth() + elementX ); final long elementBottomY = element.getHeight() + shiftedY; ensureXMapping( elementX, Boolean.FALSE ); if ( elementY >= headerSize ) { ensureYMapping( elementY, Boolean.FALSE ); } ensureXMapping( elementRightX, Boolean.FALSE ); if ( elementBottomY >= headerSize && elementBottomY <= maxHeight ) { ensureYMapping( elementBottomY, Boolean.FALSE ); if ( yMaxBounds < elementBottomY ) { yMaxBounds = elementBottomY; } } } private boolean addLine( final RenderBox element, final SheetLayoutTableCellDefinition background, final long elementY, final long shiftedY, final boolean unmodified, final long headerSize ) { // This method handles several special cases. If the element is a non-area box with borderss, // it mapps the borders into a equivalent line-definition. final long width = element.getWidth(); final long height = element.getHeight(); if ( width == 0 && height == 0 ) { if ( background != null ) { // Elements that define anchors are an exception. We add it .. return false; } // this element will be invisible. We do not add it to the layout; signal that the element has been handled .. return true; } if ( width != 0 && height != 0 ) { return false; } if ( background == null ) { return false; } if ( background.getLineType() == SheetLayoutTableCellDefinition.LINE_HINT_NONE ) { return false; } if ( elementY < headerSize ) { return false; } final long elementX = element.getX(); final long elementRightX = ( element.getWidth() + elementX ); final long elementBottomY = element.getHeight() + shiftedY; // Begin the mapping .. if ( background.getLineType() == SheetLayoutTableCellDefinition.LINE_HINT_HORIZONTAL ) { if ( unmodified == false ) { ensureXMapping( elementX, Boolean.FALSE ); } ensureXMapping( elementRightX, Boolean.FALSE ); if ( background.getCoordinate() == 0 ) { ensureYMapping( elementY, Boolean.FALSE ); } else { ensureYMapping( elementBottomY, Boolean.FALSE ); } } else { ensureYMapping( elementY, Boolean.FALSE ); ensureYMapping( elementBottomY, Boolean.FALSE ); if ( background.getCoordinate() == 0 ) { if ( unmodified == false ) { ensureXMapping( elementX, Boolean.FALSE ); } } else { ensureXMapping( elementRightX, Boolean.FALSE ); } } // update the collected maximums return true; } public void ensureXMapping( final long coordinate, final Boolean aux ) { xBounds.put( coordinate, aux ); if ( xMaxBounds < coordinate ) { xMaxBounds = coordinate; } } public void ensureYMapping( final long coordinate, final Boolean aux ) { yBounds.put( coordinate, aux ); if ( yMaxBounds < coordinate ) { yMaxBounds = coordinate; } } /** * Gets the strict mode flag. * * @return true, if strict mode is enabled, false otherwise. */ public boolean isStrict() { return strict; } public boolean isEmpty() { return yMaxBounds == 0 && xMaxBounds == 0; } /** * Returns the position of the given element within the table. The TableRectangle contains row and cell indices, no * layout coordinates. * * @param x * the element bounds for which the table bounds should be found. * @param y * the element bounds for which the table bounds should be found. * @param width * the element bounds for which the table bounds should be found. * @param height * the element bounds for which the table bounds should be found. * @param rect * the returned rectangle or null, if a new instance should be created * @return the filled table rectangle. */ public TableRectangle getTableBounds( final long x, final long y, final long width, final long height, TableRectangle rect ) { if ( rect == null ) { rect = new TableRectangle(); } final int x1 = xBounds.findKeyPosition( x, SheetLayout.LOWER_BOUNDS ); final int y1 = yBounds.findKeyPosition( y, SheetLayout.LOWER_BOUNDS ); final int x2 = xBounds.findKeyPosition( x + width, SheetLayout.UPPER_BOUNDS ); final int y2 = yBounds.findKeyPosition( y + height, SheetLayout.UPPER_BOUNDS ); rect.setRect( x1, y1, x2, y2 ); return rect; } public TableRectangle getTableBoundsWithCache( final long x, final long y, final long width, final long height, final TableRectangle rect ) { if ( rect == null ) { throw new NullPointerException(); } final int x1 = xBounds.findKeyPosition( x, SheetLayout.LOWER_BOUNDS, rect.getX1() ); final int y1 = yBounds.findKeyPosition( y, SheetLayout.LOWER_BOUNDS, rect.getY1() ); final int x2 = xBounds.findKeyPosition( x + width, SheetLayout.UPPER_BOUNDS, rect.getX2() ); final int y2 = yBounds.findKeyPosition( y + height, SheetLayout.UPPER_BOUNDS, rect.getY2() ); rect.setRect( x1, y1, x2, y2 ); return rect; } public int getColSpan( final int x1, final long endPosition ) { final int x2 = xBounds.findKeyPosition( endPosition, SheetLayout.UPPER_BOUNDS ); return x2 - x1; } public int getRowSpan( final int y1, final long endPosition ) { final int y2 = yBounds.findKeyPosition( endPosition, SheetLayout.UPPER_BOUNDS ); return y2 - y1; } /** * Returns the position of the given element within the table. The TableRectangle contains row and cell indices, no * layout coordinates. * * @param bounds * the element bounds for which the table bounds should be found. * @param rect * the returned rectangle or null, if a new instance should be created * @return the filled table rectangle. */ public TableRectangle getTableBounds( final StrictBounds bounds, TableRectangle rect ) { if ( bounds == null ) { throw new NullPointerException(); } if ( rect == null ) { rect = new TableRectangle(); } final long xCoord = bounds.getX(); final long yCoord = bounds.getY(); final int x1 = xBounds.findKeyPosition( xCoord, SheetLayout.LOWER_BOUNDS ); final int y1 = yBounds.findKeyPosition( yCoord, SheetLayout.LOWER_BOUNDS ); final int x2 = xBounds.findKeyPosition( xCoord + bounds.getWidth(), SheetLayout.UPPER_BOUNDS ); final int y2 = yBounds.findKeyPosition( yCoord + bounds.getHeight(), SheetLayout.UPPER_BOUNDS ); rect.setRect( x1, y1, x2, y2 ); return rect; } /** * A Callback method to inform the sheet layout, that the current page is complete, and no more content will be added. */ public void pageCompleted() { removeAuxilaryBounds(); } protected void removeAuxilaryBounds() { ensureXMapping( this.xMaxBounds, Boolean.FALSE ); ensureYMapping( this.yMaxBounds, Boolean.FALSE ); // Log.debug("Size: " + getRowCount() + ", " + getColumnCount()); final long[] removedXCuts = new long[xBounds.size()]; final Boolean[] xEntries = xBounds.getRawEntries(); final int xEntrySize = xBounds.size(); int arrayIdx = 0; for ( int i = 0; i < xEntrySize; i++ ) { final Boolean cut = xEntries[i]; if ( Boolean.TRUE.equals( cut ) ) { removedXCuts[arrayIdx] = xBounds.getKeyAt( i ); arrayIdx += 1; } } xBounds.removeAll( removedXCuts, arrayIdx ); arrayIdx = 0; final long[] removedYCuts = new long[yBounds.size()]; final Boolean[] yEntries = yBounds.getRawEntries(); final int ySize = yBounds.size(); for ( int i = 0; i < ySize; i++ ) { final Boolean cut = yEntries[i]; if ( Boolean.TRUE.equals( cut ) ) { removedYCuts[arrayIdx] = yBounds.getKeyAt( i ); arrayIdx += 1; } } yBounds.removeAll( removedYCuts, arrayIdx ); } /** * Computes the height of the given row. * * @param row * the row, for which the height should be computed. * @return the height of the row. * @throws IndexOutOfBoundsException * if the row is invalid. */ public long getRowHeight( final int row ) { final int rowCount = yBounds.size(); if ( row >= rowCount ) { throw new IndexOutOfBoundsException( "Row " + row + " is invalid. Max valid row is " + ( rowCount - 1 ) ); } final long bottomBorder; if ( ( row + 1 ) < rowCount ) { bottomBorder = yBounds.getKeyAt( row + 1 ); } else { bottomBorder = yMaxBounds; } return bottomBorder - yBounds.getKeyAt( row ); } public long getMaxHeight() { return yMaxBounds; } public long getMaxWidth() { return xMaxBounds; } public long getCellWidth( final int startCell ) { return getCellWidth( startCell, startCell + 1 ); } /** * Computes the height of the given row. * * @param startCell * the first cell in the range * @param endCell * the last cell included in the cell range * @return the height of the row. * @throws IndexOutOfBoundsException * if the row is invalid. */ public long getCellWidth( final int startCell, final int endCell ) { if ( startCell < 0 ) { throw new IndexOutOfBoundsException( "Start-Cell must not be negative" ); } if ( endCell < 0 ) { throw new IndexOutOfBoundsException( "End-Cell must not be negative" ); } if ( endCell < startCell ) { throw new IndexOutOfBoundsException( "End-Cell must not smaller than end-cell" ); } final long rightBorder; if ( endCell >= xBounds.size() ) { rightBorder = xMaxBounds; } else { rightBorder = xBounds.getKeyAt( endCell ); } return rightBorder - xBounds.getKeyAt( startCell ); } public long getRowHeight( final int startRow, final int endRow ) { if ( startRow < 0 ) { throw new IndexOutOfBoundsException( "Start-Cell must not be negative" ); } if ( endRow < 0 ) { throw new IndexOutOfBoundsException( "End-Cell must not be negative" ); } if ( endRow < startRow ) { throw new IndexOutOfBoundsException( "End-Cell must not smaller than end-cell" ); } final long bottomBorder; if ( endRow >= yBounds.size() ) { bottomBorder = yMaxBounds; } else { bottomBorder = yBounds.getKeyAt( endRow ); } return bottomBorder - yBounds.getKeyAt( startRow ); } /** * The current number of columns. Of course, this value begins to be reliable, once the number of columns is known * (that is at the end of the layouting process). * * @return the number columns. */ public int getColumnCount() { return Math.max( xBounds.size() - 1, 0 ); } /** * The current number of rows. Of course, this value begins to be reliable, once the number of rows is known (that is * at the end of the layouting process). * * @return the number columns. */ public int getRowCount() { return Math.max( yBounds.size() - 1, 0 ); } public long getXPosition( final int col ) { return xBounds.getKeyAt( col ); } public long getYPosition( final int row ) { return yBounds.getKeyAt( row ); } public void clear() { xBounds.clear(); xMaxBounds = 0; yBounds.clear(); yMaxBounds = 0; } public void clearVerticalInfo() { yBounds.clear(); yMaxBounds = 0; } }