/*
* 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.layout.model;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.ReportAttributeMap;
import org.pentaho.reporting.engine.classic.core.layout.model.context.NodeLayoutProperties;
import org.pentaho.reporting.engine.classic.core.metadata.ElementType;
import org.pentaho.reporting.engine.classic.core.states.ReportStateKey;
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.style.VerticalTextAlign;
import org.pentaho.reporting.engine.classic.core.util.InstanceID;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds;
import org.pentaho.reporting.libraries.base.config.Configuration;
public abstract class RenderNode implements Cloneable {
public enum CacheState {
CLEAN, DIRTY, DEEP_DIRTY
}
private static final boolean paranoidModelChecks;
static {
final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig();
if ( "true".equals( configuration
.getConfigProperty( "org.pentaho.reporting.engine.classic.core.layout.ParanoidChecks" ) ) ) {
paranoidModelChecks = true;
} else {
paranoidModelChecks = false;
}
}
public static boolean isParanoidModelChecks() {
return paranoidModelChecks;
}
public static final int HORIZONTAL_AXIS = 0;
public static final int VERTICAL_AXIS = 1;
public static final CacheState CACHE_CLEAN = CacheState.CLEAN;
public static final CacheState CACHE_DIRTY = CacheState.DIRTY;
public static final CacheState CACHE_DEEP_DIRTY = CacheState.DEEP_DIRTY;
private static final int FLAG_FROZEN = 0x01;
private static final int FLAG_FINISHED_PAGINATE = 0x02;
private static final int FLAG_FINISHED_TABLE = 0x04;
private static final int FLAG_VIRTUAL_NODE = 0x04;
private static final int FLAG_WIDOW_BOX = 0x08;
private static final int FLAG_RESERVED = 0xFFF0;
private int flags;
private CacheState cacheState;
private CacheState applyState;
private RenderBox parentNode;
private RenderNode nextNode;
private RenderNode prevNode;
private NodeLayoutProperties nodeLayoutProperties;
private long changeTracker;
private long minimumChunkWidth;
private long maximumBoxWidth;
private long validateModelAge;
private ValidationResult validateModelResult;
private long linebreakAge;
private long cachedX;
private long cachedY;
private long cachedWidth;
private long cachedHeight;
private long x;
private long y;
private long width;
private long height;
private long cachedAge;
protected RenderNode( final int majorAxis, final int minorAxis, final StyleSheet styleSheet,
final InstanceID instanceID, final ElementType elementType, final ReportAttributeMap<Object> attributes ) {
this( new NodeLayoutProperties( majorAxis, minorAxis, styleSheet, attributes, instanceID, elementType ) );
}
protected RenderNode( final NodeLayoutProperties nodeLayoutProperties ) {
if ( nodeLayoutProperties == null ) {
throw new NullPointerException();
}
this.nodeLayoutProperties = nodeLayoutProperties;
this.cacheState = RenderNode.CACHE_DIRTY;
}
protected void reinit( final StyleSheet styleSheet, final ElementType elementType,
final ReportAttributeMap<Object> attributes, final InstanceID instanceId ) {
if ( attributes == null ) {
throw new NullPointerException();
}
if ( instanceId == null ) {
throw new NullPointerException();
}
if ( elementType == null ) {
throw new NullPointerException();
}
this.flags = 0;
this.changeTracker = -1;
this.validateModelAge = 0;
this.cachedX = 0;
this.cachedY = 0;
this.cachedWidth = 0;
this.cachedHeight = 0;
this.x = 0;
this.y = 0;
this.width = 0;
this.height = 0;
this.minimumChunkWidth = 0;
this.maximumBoxWidth = 0;
this.cacheState = RenderNode.CACHE_DIRTY;
this.nodeLayoutProperties =
new NodeLayoutProperties( this.nodeLayoutProperties.getMajorAxis(), this.nodeLayoutProperties.getMinorAxis(),
styleSheet, attributes, instanceId, elementType );
}
public ElementType getElementType() {
return nodeLayoutProperties.getElementType();
}
public ReportAttributeMap<Object> getAttributes() {
return nodeLayoutProperties.getAttributes();
}
/**
* The content-ref-count counts inline-subreports.
*/
public int getContentRefCount() {
return 0;
}
public int getTableRefCount() {
return 0;
}
public int getDescendantCount() {
return 1;
}
public boolean isSizeSpecifiesBorderBox() {
return true;
}
public abstract int getNodeType();
public int getLayoutNodeType() {
return getNodeType();
}
public int getMinorAxis() {
return this.nodeLayoutProperties.getMinorAxis();
}
public int getMajorAxis() {
return this.nodeLayoutProperties.getMajorAxis();
}
public final NodeLayoutProperties getNodeLayoutProperties() {
return nodeLayoutProperties;
}
public final long getX() {
return x;
}
public final void setX( final long x ) {
this.x = x;
// this.updateChangeTracker();
}
public final long getY() {
return y;
}
public void shift( final long amount ) {
this.y += amount;
}
public void setY( final long y ) {
this.y = y;
}
protected final void updateCacheState( final CacheState state ) {
switch ( state ) {
case CLEAN:
break;
case DIRTY:
if ( cacheState == RenderNode.CACHE_CLEAN ) {
this.cacheState = RenderNode.CACHE_DIRTY;
final RenderBox parent = getParent();
if ( parent != null ) {
parent.updateCacheState( RenderNode.CACHE_DIRTY );
}
}
// if cache-state either dirty or deep-dirty, no need to update.
break;
case DEEP_DIRTY:
if ( cacheState == RenderNode.CACHE_CLEAN ) {
final RenderBox parent = getParent();
if ( parent != null ) {
parent.updateCacheState( RenderNode.CACHE_DEEP_DIRTY );
}
}
this.cacheState = RenderNode.CACHE_DEEP_DIRTY;
break;
default:
throw new IllegalArgumentException();
}
}
public final long getWidth() {
return width;
}
public final void setWidth( final long width ) {
if ( width < 0 ) {
throw new IndexOutOfBoundsException( "Width cannot be negative" );
}
this.width = width;
this.updateCacheState( RenderNode.CACHE_DIRTY );
// this.updateChangeTracker();
}
public final long getHeight() {
return height;
}
public void setHeight( final long height ) {
if ( height < 0 ) {
throw new IndexOutOfBoundsException( "Height cannot be negative" );
}
this.height = height;
// this.updateCacheState(RenderNode.CACHE_DIRTY);
}
public final StyleSheet getStyleSheet() {
return nodeLayoutProperties.getStyleSheet();
}
public InstanceID getInstanceId() {
return nodeLayoutProperties.getInstanceId();
}
protected void updateChangeTracker() {
changeTracker += 1;
if ( cacheState == RenderNode.CACHE_CLEAN ) {
cacheState = RenderNode.CACHE_DIRTY;
}
final RenderBox parent = getParent();
if ( parent != null ) {
parent.updateChangeTracker();
}
}
public final long getChangeTracker() {
return changeTracker;
}
public final RenderBox getParent() {
return parentNode;
}
public RenderBox getLayoutParent() {
if ( parentNode != null ) {
if ( parentNode.getNodeType() == LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT ) {
return parentNode.getLayoutParent();
}
}
return parentNode;
}
protected final void setParent( final RenderBox parent ) {
if ( isParanoidModelChecks() ) {
final RenderNode prev = getPrev();
if ( parent != null && prev == parent ) {
throw new IllegalStateException( "Assertation failed: Cannot have a parent that is the same as a silbling." );
}
if ( parent == null ) {
final RenderNode next = getNext();
if ( next != null ) {
throw new NullPointerException();
}
if ( prev != null ) {
throw new NullPointerException();
}
}
}
this.parentNode = parent;
}
public final RenderNode getPrev() {
return prevNode;
}
protected final void setPrevUnchecked( final RenderNode prev ) {
this.prevNode = prev;
}
protected final void setPrev( final RenderNode prev ) {
this.prevNode = prev;
if ( isParanoidModelChecks() && prev != null ) {
final RenderBox parent = getParent();
if ( prev == parent ) {
throw new IllegalStateException();
}
if ( parent != null ) {
if ( parent.getFirstChild() == this ) {
throw new NullPointerException( "Cannot have a prev node if the parent has me as first child." );
}
}
}
}
public final RenderNode getNext() {
return nextNode;
}
protected final void setNextUnchecked( final RenderNode next ) {
this.nextNode = next;
}
protected final void setNext( final RenderNode next ) {
this.nextNode = next;
if ( isParanoidModelChecks() && next != null ) {
final RenderBox parent = getParent();
if ( next == parent ) {
throw new IllegalStateException();
}
if ( parent != null ) {
if ( parent.getLastChild() == this ) {
throw new NullPointerException( "Cannot have a next-node, if the parent has me as last child." );
}
}
}
}
public LogicalPageBox getLogicalPage() {
RenderNode parent = this;
while ( parent != null ) {
if ( parent.getNodeType() == LayoutNodeTypes.TYPE_BOX_LOGICALPAGE ) {
return (LogicalPageBox) parent;
}
parent = parent.getParent();
}
return null;
}
/**
* Clones this node. Be aware that cloning can get you into deep trouble, as the relations this node has may no longer
* be valid.
*
* @return
* @noinspection CloneDoesntDeclareCloneNotSupportedException
*/
public Object clone() {
try {
return super.clone();
} catch ( final CloneNotSupportedException e ) {
// ignored ..
throw new IllegalStateException( "Clone failed for some reason." );
}
}
/**
* Derive creates a disconnected node that shares all the properties of the original node. The derived node will no
* longer have any parent, silbling, child or any other relationships with other nodes.
*
* @param deep
* @return
*/
public RenderNode derive( final boolean deep ) {
final RenderNode node = (RenderNode) clone();
node.parentNode = null;
node.nextNode = null;
node.prevNode = null;
if ( deep ) {
node.cachedAge = this.changeTracker;
node.validateModelAge = -1;
// todo PRD-4606
node.cacheState = CACHE_DIRTY;
}
return node;
}
public RenderNode deriveFrozen( final boolean deep ) {
final RenderNode node = (RenderNode) clone();
node.parentNode = null;
node.nextNode = null;
node.prevNode = null;
node.freeze();
return node;
}
public boolean isFrozen() {
return isFlag( FLAG_FROZEN );
}
public RenderNode findNodeById( final InstanceID instanceId ) {
if ( instanceId == getInstanceId() ) {
return this;
}
return null;
}
public boolean isOpen() {
return false;
}
public boolean isEmpty() {
return false;
}
public boolean isDiscardable() {
return false;
}
/**
* If that method returns true, the element will not be used for rendering. For the purpose of computing sizes or
* performing the layouting (in the validate() step), this element will treated as if it is not there.
* <p/>
* If the element reports itself as non-empty, however, it will affect the margin computation.
*
* @return
*/
public boolean isIgnorableForRendering() {
return isEmpty();
}
public void freeze() {
setFlag( FLAG_FROZEN, true );
}
public long getMaximumBoxWidth() {
return maximumBoxWidth;
}
public void setMaximumBoxWidth( final long maximumBoxWidth ) {
this.maximumBoxWidth = maximumBoxWidth;
}
public long getMinimumChunkWidth() {
return minimumChunkWidth;
}
protected void setMinimumChunkWidth( final long minimumChunkWidth ) {
if ( minimumChunkWidth < 0 ) {
throw new IllegalArgumentException();
}
this.minimumChunkWidth = minimumChunkWidth;
}
public long getEffectiveMarginTop() {
return 0;
}
public long getEffectiveMarginBottom() {
return 0;
}
public VerticalTextAlign getVerticalTextAlignment() {
return nodeLayoutProperties.getVerticalTextAlign();
}
//
// /**
// * The sticky-Marker contains the original Y of this node.
// * @return
// */
// public long getStickyMarker()
// {
// return stickyMarker;
// }
//
// public void setStickyMarker(final long stickyMarker)
// {
// this.stickyMarker = stickyMarker;
// }
public String getName() {
return null;
}
public boolean isBreakAfter() {
return false;
}
public long getValidateModelAge() {
return validateModelAge;
}
protected void resetValidateModelResult() {
this.validateModelAge = -1;
}
public void setValidateModelResult( final ValidationResult result ) {
this.validateModelAge = changeTracker;
this.validateModelResult = result;
}
public ValidationResult isValidateModelResult() {
return validateModelResult;
}
public long getLinebreakAge() {
return linebreakAge;
}
public void setLinebreakAge( final long linebreakAge ) {
this.linebreakAge = linebreakAge;
}
/**
* Returns the cached y position. This position is known after all layouting steps have been finished. In most cases
* the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration.
* <p/>
* The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter
* converts them into border-box sizes before filling the cache.
*
* @return the cached x position
*/
public final long getCachedX() {
return cachedX;
}
/**
* Defines the cached x position. This position is known after all layouting steps have been finished. In most cases
* the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration.
* <p/>
* The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter
* converts them into border-box sizes before filling the cache.
*
* @param cachedX
* the cached x position
*/
public void setCachedX( final long cachedX ) {
this.cachedX = cachedX;
}
/**
* Returns the cached y position. This position is known after all layouting steps have been finished. In most cases
* the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration.
* <p/>
* The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter
* converts them into border-box sizes before filling the cache.
*
* @return the cached y position
*/
public final long getCachedY() {
return cachedY;
}
public final long getCachedY2() {
return cachedY + cachedHeight;
}
/**
* Defines the cached y position. This position is known after all layouting steps have been finished. In most cases
* the layouter tries to reuse the cached values instead of recomputing everything from scratch on each iteration.
* <p/>
* The cached positions always specify the border-box. If the user specified sizes as content-box sizes, the layouter
* converts them into border-box sizes before filling the cache.
*
* @param cachedY
* the cached y position
*/
public void setCachedY( final long cachedY ) {
this.cachedY = cachedY;
}
public void shiftCached( final long amount ) {
this.cachedY += amount;
}
public final long getCachedWidth() {
return cachedWidth;
}
public final long getCachedX2() {
return cachedX + cachedWidth;
}
public void setCachedWidth( final long cachedWidth ) {
if ( cachedWidth < 0 ) {
throw new IndexOutOfBoundsException( "'cached width' cannot be negative." );
}
this.cachedWidth = cachedWidth;
}
public final long getCachedHeight() {
return cachedHeight;
}
public void setCachedHeight( final long cachedHeight ) {
if ( cachedHeight < 0 ) {
throw new IndexOutOfBoundsException( "'cached height' cannot be negative, was " + cachedHeight );
}
this.cachedHeight = cachedHeight;
}
public void apply() {
this.x = this.cachedX;
this.y = this.cachedY;
this.width = this.cachedWidth;
this.height = this.cachedHeight;
this.cachedAge = this.changeTracker;
this.cacheState = CacheState.CLEAN;
this.applyState = CacheState.CLEAN;
final RenderBox parent = getParent();
if ( parent != null ) {
parent.addOverflowArea( x + getOverflowAreaWidth() - parent.getX(), y + getOverflowAreaHeight() - parent.getY() );
}
}
public final boolean isLinebreakCacheValid() {
if ( linebreakAge != changeTracker ) {
return false;
}
return true;
}
public final boolean isValidateModelCacheValid() {
if ( validateModelAge != changeTracker ) {
return false;
}
if ( validateModelResult == ValidationResult.UNKNOWN ) {
return false;
}
return true;
}
/**
* Checks whether this node can be removed. This flag is used by iterative streaming output targets to mark nodes that
* have been fully processed.
*
* @return
*/
public boolean isFinishedPaginate() {
return isFlag( FLAG_FINISHED_PAGINATE );
}
public void setFinishedPaginate( final boolean finished ) {
if ( isFinishedPaginate() == true && finished == false ) {
throw new IllegalStateException( "Cannot undo a finished-marker" );
}
setFlag( FLAG_FINISHED_PAGINATE, finished );
}
public boolean isFinishedTable() {
return isFlag( FLAG_FINISHED_TABLE );
}
public void setFinishedTable( final boolean finished ) {
if ( isFinishedTable() == true && finished == false ) {
throw new IllegalStateException( "Cannot undo a finished-marker" );
}
setFlag( FLAG_FINISHED_TABLE, finished );
}
public boolean isDeepFinishedTable() {
return isFinishedTable();
}
public CacheState getCacheState() {
return cacheState;
}
public ReportStateKey getStateKey() {
return null;
}
public boolean isBoxOverflowX() {
return false;
}
public boolean isBoxOverflowY() {
return false;
}
public final boolean isNodeVisible( final StrictBounds drawArea, final boolean overflowX, final boolean overflowY ) {
final long drawAreaX0 = drawArea.getX();
final long drawAreaY0 = drawArea.getY();
return isNodeVisible( drawAreaX0, drawAreaY0, drawArea.getWidth(), drawArea.getHeight(), overflowX, overflowY );
}
public final boolean isNodeVisible( final StrictBounds drawArea ) {
final long drawAreaX0 = drawArea.getX();
final long drawAreaY0 = drawArea.getY();
return isNodeVisible( drawAreaX0, drawAreaY0, drawArea.getWidth(), drawArea.getHeight() );
}
public final boolean isNodeVisible( final long drawAreaX0, final long drawAreaY0, final long drawAreaWidth,
final long drawAreaHeight ) {
return isNodeVisible( drawAreaX0, drawAreaY0, drawAreaWidth, drawAreaHeight, isBoxOverflowX(), isBoxOverflowY() );
}
public final boolean isNodeVisible( final long drawAreaX0, final long drawAreaY0, final long drawAreaWidth,
final long drawAreaHeight, final boolean overflowX, final boolean overflowY ) {
if ( getStyleSheet().getBooleanStyleProperty( ElementStyleKeys.VISIBLE ) == false ) {
return false;
}
final long drawAreaX1 = drawAreaX0 + drawAreaWidth;
final long drawAreaY1 = drawAreaY0 + drawAreaHeight;
final long x2 = x + width;
final long y2 = y + height;
if ( width == 0 ) {
if ( x2 < drawAreaX0 ) {
return false;
}
if ( x > drawAreaX1 ) {
return false;
}
} else if ( overflowX == false ) {
if ( x2 <= drawAreaX0 ) {
return false;
}
if ( x >= drawAreaX1 ) {
return false;
}
}
if ( height == 0 ) {
if ( y2 < drawAreaY0 ) {
return false;
}
if ( y > drawAreaY1 ) {
return false;
}
} else if ( overflowY == false ) {
if ( y2 <= drawAreaY0 ) {
return false;
}
if ( y >= drawAreaY1 ) {
return false;
}
}
return true;
}
public boolean isVirtualNode() {
return isFlag( FLAG_VIRTUAL_NODE );
}
public void setVirtualNode( final boolean virtualNode ) {
setFlag( FLAG_VIRTUAL_NODE, virtualNode );
}
protected void setFlag( final int flag, final boolean value ) {
if ( value ) {
flags = flags | flag;
} else {
flags = flags & ( ~flag );
}
}
protected boolean isFlag( final int flag ) {
return ( flags & flag ) != 0;
}
public final boolean isBoxVisible( final StrictBounds drawArea ) {
return isBoxVisible( drawArea.getX(), drawArea.getY(), drawArea.getWidth(), drawArea.getHeight() );
}
public final boolean isBoxVisible( final long x, final long y, final long width, final long height ) {
if ( isNodeVisible( x, y, width, height ) == false ) {
return false;
}
final RenderBox parent = getParent();
if ( parent == null ) {
return true;
}
final StyleSheet styleSheet = getStyleSheet();
if ( styleSheet.getStyleProperty( ElementStyleKeys.ANCHOR_NAME ) != null ) {
return true;
}
if ( parent.getNodeType() != LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT
&& parent.getStaticBoxLayoutProperties().isOverflowX() == false ) {
final long parentX1 = parent.getX();
final long parentX2 = parentX1 + parent.getWidth();
if ( getWidth() == 0 ) {
// could be a line ..
return true;
}
final long boxX1 = getX();
final long boxX2 = boxX1 + getWidth();
if ( boxX2 <= parentX1 ) {
return false;
}
if ( boxX1 >= parentX2 ) {
return false;
}
}
final int layoutNodeType = getLayoutNodeType();
if ( layoutNodeType == LayoutNodeTypes.TYPE_BOX_TABLE_CELL || layoutNodeType == LayoutNodeTypes.TYPE_BOX_TABLE_ROW ) {
// we cannot perform an overflow test for table-rows or table-cells based on the parent node.
// With col and row-spanning, this can be non-deterministic.
return true;
}
if ( parent.getNodeType() != LayoutNodeTypes.TYPE_BOX_AUTOLAYOUT
&& parent.getStaticBoxLayoutProperties().isOverflowY() == false ) {
// Compute whether the box is at least partially contained in the parent's bounding box.
final long parentY1 = parent.getY();
final long parentY2 = parentY1 + parent.getHeight();
if ( getHeight() == 0 ) {
// could be a line ..
return true;
}
final long boxY1 = getY();
final long boxY2 = boxY1 + getHeight();
if ( boxY2 <= parentY1 ) {
return false;
}
if ( boxY1 >= parentY2 ) {
return false;
}
}
return true;
}
public long getOverflowAreaHeight() {
return getHeight();
}
public long getOverflowAreaWidth() {
return getWidth();
}
public long getEffectiveMinimumChunkSize() {
return minimumChunkWidth;
}
public int getChildCount() {
return 0;
}
public boolean isWidowBox() {
return isFlag( FLAG_WIDOW_BOX );
}
public void setWidowBox( final boolean widowBox ) {
setFlag( FLAG_WIDOW_BOX, widowBox );
}
public boolean isOrphanLeaf() {
return false;
}
public RenderBox.RestrictFinishClearOut getRestrictFinishedClearOut() {
return RenderBox.RestrictFinishClearOut.UNRESTRICTED;
}
public long getCachedAge() {
return cachedAge;
}
public boolean isCacheValid() {
if ( cachedAge != changeTracker ) {
return false;
}
if ( this.cacheState != CACHE_CLEAN ) {
return false;
}
return true;
}
protected final void setCachedAge( final long cachedAge ) {
this.cachedAge = cachedAge;
}
public final long getY2() {
return y + height;
}
public boolean isVisible() {
return nodeLayoutProperties.isVisible();
}
public boolean isContainsReservedContent() {
return false;
}
public void markApplyStateDirty() {
if ( applyState != CacheState.CLEAN ) {
return;
}
applyState = CACHE_DIRTY;
RenderBox parent = getParent();
if ( parent != null ) {
parent.markApplyStateDirty();
}
}
public CacheState getApplyState() {
return applyState;
}
public int getRowIndex() {
return 0;
}
public boolean isRenderBox() {
return false;
}
public int getWidowLeafCount() {
return 0;
}
public int getOrphanLeafCount() {
return 0;
}
}