/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.gvt;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.event.EventListenerList;
import org.apache.batik.ext.awt.RenderingHintsKeyExt;
import org.apache.batik.ext.awt.image.renderable.ClipRable;
import org.apache.batik.ext.awt.image.renderable.Filter;
import org.apache.batik.gvt.event.GraphicsNodeChangeEvent;
import org.apache.batik.gvt.event.GraphicsNodeChangeListener;
import org.apache.batik.gvt.filter.GraphicsNodeRable;
import org.apache.batik.gvt.filter.GraphicsNodeRable8Bit;
import org.apache.batik.gvt.filter.Mask;
/**
* A partial implementation of the <tt>GraphicsNode</tt> interface.
*
* @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a>
* @author <a href="mailto:etissandier@ilog.fr">Emmanuel Tissandier</a>
* @author <a href="mailto:Thomas.DeWeeese@Kodak.com">Thomas DeWeese</a>
* @version $Id: AbstractGraphicsNode.java,v 1.49 2003/07/08 00:16:57 deweese Exp $
*/
public abstract class AbstractGraphicsNode implements GraphicsNode {
/**
* The listeners list.
*/
protected EventListenerList listeners;
/**
* The transform of this graphics node.
*/
protected AffineTransform transform;
/**
* The inverse transform for this node, i.e., from parent node
* to this node.
*/
protected AffineTransform inverseTransform;
/**
* The compositing operation to be used when a graphics node is
* painted on top of another one.
*/
protected Composite composite;
/**
* This flag bit indicates whether or not this graphics node is visible.
*/
protected boolean isVisible = true;
/**
* The clipping filter for this graphics node.
*/
protected ClipRable clip;
/**
* The rendering hints that control the quality to use when rendering
* this graphics node.
*/
protected RenderingHints hints;
/**
* The parent of this graphics node.
*/
protected CompositeGraphicsNode parent;
/**
* The root of the GVT tree.
*/
protected RootGraphicsNode root;
/**
* The mask of this graphics node.
*/
protected Mask mask;
/**
* The filter of this graphics node.
*/
protected Filter filter;
/**
* Indicates how this graphics node reacts to events.
*/
protected int pointerEventType = VISIBLE_PAINTED;
/**
* The GraphicsNodeRable for this node.
*/
protected WeakReference graphicsNodeRable;
/**
* The GraphicsNodeRable for this node with all filtering applied
*/
protected WeakReference enableBackgroundGraphicsNodeRable;
/**
* A Weak Reference to this.
*/
protected WeakReference weakRef;
/**
* Internal Cache: node bounds
*/
private Rectangle2D bounds;
protected GraphicsNodeChangeEvent changeStartedEvent = null;
protected GraphicsNodeChangeEvent changeCompletedEvent = null;
/**
* Constructs a new graphics node.
*/
protected AbstractGraphicsNode() {}
/**
* Returns a canonical WeakReference to this GraphicsNode.
* This is suitable for use as a key value in a hash map
*/
public WeakReference getWeakReference() {
if (weakRef == null)
weakRef = new WeakReference(this);
return weakRef;
}
//
// Properties methods
//
/**
* Returns the type that describes how this graphics node reacts to events.
*
* @return VISIBLE_PAINTED | VISIBLE_FILL | VISIBLE_STROKE | VISIBLE |
* PAINTED | FILL | STROKE | ALL | NONE
*/
public int getPointerEventType() {
return pointerEventType;
}
/**
* Sets the type that describes how this graphics node reacts to events.
*
* @param pointerEventType VISIBLE_PAINTED | VISIBLE_FILL | VISIBLE_STROKE |
* VISIBLE | PAINTED | FILL | STROKE | ALL | NONE
*/
public void setPointerEventType(int pointerEventType) {
this.pointerEventType = pointerEventType;
}
/**
* Sets the transform of this node.
*
* @param newTransform the new transform of this node
*/
public void setTransform(AffineTransform newTransform) {
fireGraphicsNodeChangeStarted();
invalidateGeometryCache();
this.transform = newTransform;
if(transform.getDeterminant() != 0){
try{
inverseTransform = transform.createInverse();
}catch(NoninvertibleTransformException e){
// Should never happen.
throw new Error();
}
}
else{
// The transform is not invertible. Use the same
// transform.
inverseTransform = transform;
}
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the transform of this node or null if any.
*/
public AffineTransform getTransform() {
return transform;
}
/**
* Returns the inverse transform for this node.
*/
public AffineTransform getInverseTransform(){
return inverseTransform;
}
/**
* Returns the concatenated transform of this node. i.e., this
* node's transform preconcatenated with it's parent's transforms.
*/
public AffineTransform getGlobalTransform(){
AffineTransform ctm = new AffineTransform();
GraphicsNode node = this;
while (node != null) {
if(node.getTransform() != null){
ctm.preConcatenate(node.getTransform());
}
node = node.getParent();
}
return ctm;
}
/**
* Sets the composite of this node.
*
* @param composite the composite of this node
*/
public void setComposite(Composite newComposite) {
fireGraphicsNodeChangeStarted();
invalidateGeometryCache();
this.composite = newComposite;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the composite of this node or null if any.
*/
public Composite getComposite() {
return composite;
}
/**
* Sets if this node is visible or not depending on the specified value.
*
* @param isVisible If true this node is visible
*/
public void setVisible(boolean isVisible) {
fireGraphicsNodeChangeStarted();
this.isVisible = isVisible;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns true if this node is visible, false otherwise.
*/
public boolean isVisible() {
return isVisible;
}
public void setClip(ClipRable newClipper) {
fireGraphicsNodeChangeStarted();
invalidateGeometryCache();
this.clip = newClipper;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the clipping filter of this node or null if any.
*/
public ClipRable getClip() {
return clip;
}
/**
* Maps the specified key to the specified value in the rendering hints of
* this node.
*
* @param key the key of the hint to be set
* @param value the value indicating preferences for the specified
* hint category.
*/
public void setRenderingHint(RenderingHints.Key key, Object value) {
fireGraphicsNodeChangeStarted();
if (this.hints == null) {
this.hints = new RenderingHints(key, value);
} else {
hints.put(key, value);
}
fireGraphicsNodeChangeCompleted();
}
/**
* Copies all of the mappings from the specified Map to the
* rendering hints of this node.
*
* @param hints the rendering hints to be set
*/
public void setRenderingHints(Map hints) {
fireGraphicsNodeChangeStarted();
if (this.hints == null) {
this.hints = new RenderingHints(hints);
} else {
this.hints.putAll(hints);
}
fireGraphicsNodeChangeCompleted();
}
/**
* Sets the rendering hints of this node.
*
* @param newHints the new rendering hints of this node
*/
public void setRenderingHints(RenderingHints newHints) {
fireGraphicsNodeChangeStarted();
this.hints = newHints;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the rendering hints of this node or null if any.
*/
public RenderingHints getRenderingHints() {
return hints;
}
/**
* Sets the mask of this node.
*
* @param newMask the new mask of this node
*/
public void setMask(Mask newMask) {
fireGraphicsNodeChangeStarted();
invalidateGeometryCache();
this.mask = newMask;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the mask of this node or null if any.
*/
public Mask getMask() {
return mask;
}
/**
* Sets the filter of this node.
*
* @param newFilter the new filter of this node
*/
public void setFilter(Filter newFilter) {
fireGraphicsNodeChangeStarted();
invalidateGeometryCache();
this.filter = newFilter;
fireGraphicsNodeChangeCompleted();
}
/**
* Returns the filter of this node or null if any.
*/
public Filter getFilter() {
return filter;
}
/**
* Returns the GraphicsNodeRable for this node. This
* GraphicsNodeRable is the Renderable (Filter) before any of the
* filter operations have been applied.
*/
public Filter getGraphicsNodeRable(boolean createIfNeeded) {
GraphicsNodeRable ret = null;
if (graphicsNodeRable != null) {
ret = (GraphicsNodeRable)graphicsNodeRable.get();
if (ret != null) return ret;
}
if (createIfNeeded) {
ret = new GraphicsNodeRable8Bit(this);
graphicsNodeRable = new WeakReference(ret);
}
return ret;
}
/**
* Returns the GraphicsNodeRable for this node. This
* GraphicsNodeRable is the Renderable (Filter) after all of the
* filter operations have been applied.
*/
public Filter getEnableBackgroundGraphicsNodeRable
(boolean createIfNeeded) {
GraphicsNodeRable ret = null;
if (enableBackgroundGraphicsNodeRable != null) {
ret = (GraphicsNodeRable)enableBackgroundGraphicsNodeRable.get();
if (ret != null) return ret;
}
if (createIfNeeded) {
ret = new GraphicsNodeRable8Bit(this);
ret.setUsePrimitivePaint(false);
enableBackgroundGraphicsNodeRable = new WeakReference(ret);
}
return ret;
}
//
// Drawing methods
//
/**
* Paints this node.
*
* @param g2d the Graphics2D to use
*/
public void paint(Graphics2D g2d){
// first, make sure we haven't been interrupted
if (Thread.currentThread().isInterrupted()) {
return;
}
if ((composite != null) &&
(composite instanceof AlphaComposite)) {
AlphaComposite ac = (AlphaComposite)composite;
if (ac.getAlpha() < 0.001)
return; // No point in drawing
}
// Set up graphic context. It is important to setup the transform first,
// because the clip is defined in this node's user space.
Shape defaultClip = g2d.getClip();
Composite defaultComposite = g2d.getComposite();
AffineTransform defaultTransform = g2d.getTransform();
RenderingHints defaultHints = null;
if (hints != null) {
defaultHints = g2d.getRenderingHints();
g2d.addRenderingHints(hints);
}
if (transform != null) {
g2d.transform(transform);
}
if (composite != null) {
g2d.setComposite(composite);
}
if (clip != null){
g2d.clip(clip.getClipPath());
}
Shape curClip = g2d.getClip();
g2d.setRenderingHint(RenderingHintsKeyExt.KEY_AREA_OF_INTEREST,
curClip);
// Check if any painting is needed at all. Get the clip (in user space)
// and see if it intersects with this node's bounds (in user space).
boolean paintNeeded = true;
Rectangle2D bounds = getBounds();
Shape g2dClip = curClip; //g2d.getClip();
if (g2dClip != null) {
Rectangle2D clipBounds = g2dClip.getBounds2D();
if(bounds != null && !bounds.intersects(clipBounds.getX(),
clipBounds.getY(),
clipBounds.getWidth(),
clipBounds.getHeight())){
paintNeeded = false;
}
}
// Only paint if needed.
if (paintNeeded){
AffineTransform txf = g2d.getTransform();
boolean antialiasedClip = false;
if(clip != null){
antialiasedClip = false;
/* isAntialiasedClip(g2d.getTransform(),
g2d.getRenderingHints(),
clip.getClipPath());*/
}
boolean useOffscreen = isOffscreenBufferNeeded();
useOffscreen |= antialiasedClip;
if (!useOffscreen) {
// Render on this canvas.
primitivePaint(g2d);
} else {
Filter filteredImage = null;
if(filter == null){
filteredImage = getGraphicsNodeRable(true);
}
else {
// traceFilter(filter, "=====>> ");
filteredImage = filter;
}
if (mask != null) {
if (mask.getSource() != filteredImage){
mask.setSource(filteredImage);
}
filteredImage = mask;
}
if (clip != null && antialiasedClip) {
if (clip.getSource() != filteredImage){
clip.setSource(filteredImage);
}
filteredImage = clip;
}
if(antialiasedClip){
// Remove hard edged clip
g2d.setClip(null);
}
Rectangle2D filterBounds = filteredImage.getBounds2D();
g2d.clip(filterBounds);
org.apache.batik.ext.awt.image.GraphicsUtil.drawImage
(g2d, filteredImage);
}
}
// Restore default rendering attributes
if (defaultHints != null) {
g2d.setRenderingHints(defaultHints);
}
g2d.setTransform(defaultTransform);
g2d.setClip(defaultClip);
if (composite != null) {
g2d.setComposite(defaultComposite);
}
}
/**
* DEBUG: Trace filter chain
*/
private void traceFilter(Filter filter, String prefix){
System.out.println(prefix + filter.getClass().getName());
System.out.println(prefix + filter.getBounds2D());
java.util.Vector sources = filter.getSources();
int nSources = sources != null ? sources.size() : 0;
prefix += "\t";
for(int i=0; i<nSources; i++){
Filter source = (Filter)sources.elementAt(i);
traceFilter(source, prefix);
}
System.out.flush();
}
/**
* Returns true of an offscreen buffer is needed to render this node, false
* otherwise.
*/
protected boolean isOffscreenBufferNeeded() {
return ((filter != null) ||
(mask != null) ||
(composite != null &&
!AlphaComposite.SrcOver.equals(composite)));
}
/**
* Returns true if there is a clip and it should be antialiased
*/
protected boolean isAntialiasedClip(AffineTransform usr2dev,
RenderingHints hints,
Shape clip){
//
// Antialias clip if:
// + Antialiasing is on *or* rendering quality is on
// *and*
// + clip is not null
// *and*
// + clip is not a rectangle in device space.
//
// This leaves out the case where the node clip is a
// rectangle and the current clip (i.e., the intersection
// of the current Graphics2D's clip and this node's clip)
// is not a rectangle.
//
boolean antialiased = false;
if((hints.get(RenderingHints.KEY_ANTIALIASING) ==
RenderingHints.VALUE_ANTIALIAS_ON) ||
(hints.get(RenderingHints.KEY_RENDERING) ==
RenderingHints.VALUE_RENDER_QUALITY)){
if(!(clip instanceof Rectangle2D &&
usr2dev.getShearX() == 0 &&
usr2dev.getShearY() == 0)){
antialiased = true;
}
}
return antialiased;
}
//
// Event support methods
//
public void fireGraphicsNodeChangeStarted(GraphicsNode changeSrc) {
if (changeStartedEvent == null)
changeStartedEvent = new GraphicsNodeChangeEvent
(this, GraphicsNodeChangeEvent.CHANGE_STARTED);
changeStartedEvent.setChangeSrc(changeSrc);
fireGraphicsNodeChangeStarted(changeStartedEvent);
changeStartedEvent.setChangeSrc(null);
}
//
// Event support methods
//
public void fireGraphicsNodeChangeStarted() {
if (changeStartedEvent == null)
changeStartedEvent = new GraphicsNodeChangeEvent
(this, GraphicsNodeChangeEvent.CHANGE_STARTED);
else {
changeStartedEvent.setChangeSrc(null);
}
fireGraphicsNodeChangeStarted(changeStartedEvent);
}
public void fireGraphicsNodeChangeStarted
(GraphicsNodeChangeEvent changeStartedEvent) {
// If we had per node listeners we would fire them here...
RootGraphicsNode rootGN = getRoot();
if (rootGN == null) return;
List l = rootGN.getTreeGraphicsNodeChangeListeners();
if (l == null) return;
Iterator i = l.iterator();
GraphicsNodeChangeListener gncl;
while (i.hasNext()) {
gncl = (GraphicsNodeChangeListener)i.next();
gncl.changeStarted(changeStartedEvent);
}
}
public void fireGraphicsNodeChangeCompleted() {
if (changeCompletedEvent == null) {
changeCompletedEvent = new GraphicsNodeChangeEvent
(this, GraphicsNodeChangeEvent.CHANGE_COMPLETED);
}
// If we had per node listeners we would fire them here...
RootGraphicsNode rootGN = getRoot();
if (rootGN == null) return;
List l = rootGN.getTreeGraphicsNodeChangeListeners();
if (l == null) return;
Iterator i = l.iterator();
GraphicsNodeChangeListener gncl;
while (i.hasNext()) {
gncl = (GraphicsNodeChangeListener)i.next();
gncl.changeCompleted(changeCompletedEvent);
}
}
//
// Structural methods
//
/**
* Returns the parent of this node or null if any.
*/
public CompositeGraphicsNode getParent() {
return parent;
}
/**
* Returns the root of the GVT tree or null if the node is not part of a GVT
* tree.
*/
public RootGraphicsNode getRoot() {
return root;
}
/**
* Sets the root node of this graphics node.
*
* @param newRoot the new root node of this node
*/
protected void setRoot(RootGraphicsNode newRoot) {
this.root = newRoot;
}
/**
* Sets the parent node of this graphics node.
*
* @param newParent the new parent node of this node
*/
protected void setParent(CompositeGraphicsNode newParent) {
this. parent = newParent;
}
//
// Geometric methods
//
/**
* Invalidates the cached geometric bounds. This method is called
* each time an attribute that affects the bounds of this node
* changed.
*/
protected void invalidateGeometryCache() {
// If our bounds are invalid then our parents bounds
// must be invalid also. So just return.
//if (bounds == null) return;
if (parent != null) {
((AbstractGraphicsNode) parent).invalidateGeometryCache();
}
bounds = null;
}
/**
* Returns the bounds of this node in user space. This includes primitive
* paint, filtering, clipping and masking.
*/
public Rectangle2D getBounds(){
// Get the primitive bounds
// Rectangle2D bounds = null;
if (bounds == null) {
// The painted region, before cliping, masking and compositing is
// either the area painted by the primitive paint or the area
// painted by the filter.
if(filter == null){
bounds = getPrimitiveBounds();
} else {
bounds = filter.getBounds2D();
}
// Factor in the clipping area, if any
if(bounds != null){
if (clip != null) {
Rectangle2D clipR = clip.getClipPath().getBounds2D();
if (clipR.intersects(bounds))
Rectangle2D.intersect(bounds, clipR, bounds);
}
// Factor in the mask, if any
if (mask != null) {
Rectangle2D maskR = mask.getBounds2D();
if (maskR.intersects(bounds))
Rectangle2D.intersect(bounds, maskR, bounds);
}
}
bounds = normalizeRectangle(bounds);
// Make sure we haven't been interrupted
if (Thread.currentThread().isInterrupted()) {
// The Thread has been interrupted. Invalidate
// any cached values and proceed.
invalidateGeometryCache();
}
}
return bounds;
}
/**
* Returns the bounds of this node after applying the input transform
* (if any), concatenated with this node's transform (if any).
*
* @param txf the affine transform with which this node's transform should
* be concatenated. Should not be null.
*/
public Rectangle2D getTransformedBounds(AffineTransform txf){
AffineTransform t = txf;
if (transform != null) {
t = new AffineTransform(txf);
t.concatenate(transform);
}
// The painted region, before cliping, masking and compositing
// is either the area painted by the primitive paint or the
// area painted by the filter.
Rectangle2D tBounds = null;
if (filter == null) {
// Use txf, not t
tBounds = getTransformedPrimitiveBounds(txf);
} else {
tBounds = t.createTransformedShape
(filter.getBounds2D()).getBounds2D();
}
// Factor in the clipping area, if any
if (tBounds != null) {
if (clip != null) {
tBounds.intersect
(tBounds,
t.createTransformedShape(clip.getClipPath()).getBounds2D(),
tBounds);
}
// Factor in the mask, if any
if(mask != null) {
tBounds.intersect
(tBounds,
t.createTransformedShape(mask.getBounds2D()).getBounds2D(),
tBounds);
}
}
return tBounds;
}
/**
* Returns the bounds of this node's primitivePaint after applying
* the input transform (if any), concatenated with this node's
* transform (if any).
*
* @param txf the affine transform with which this node's transform should
* be concatenated. Should not be null. */
public Rectangle2D getTransformedPrimitiveBounds(AffineTransform txf) {
Rectangle2D tpBounds = getPrimitiveBounds();
if (tpBounds == null) {
return null;
}
AffineTransform t = txf;
if (transform != null) {
t = new AffineTransform(txf);
t.concatenate(transform);
}
return t.createTransformedShape(tpBounds).getBounds2D();
}
/**
* Returns the bounds of the area covered by this node, without
* taking any of its rendering attribute into accoun. i.e.,
* exclusive of any clipping, masking, filtering or stroking, for
* example. The returned value is transformed by the concatenation
* of the input transform and this node's transform.
*
* @param txf the affine transform with which this node's transform should
* be concatenated. Should not be null.
*/
public Rectangle2D getTransformedGeometryBounds(AffineTransform txf) {
Rectangle2D tpBounds = getGeometryBounds();
if (tpBounds == null) {
return null;
}
AffineTransform t = txf;
if (transform != null) {
t = new AffineTransform(txf);
t.concatenate(transform);
}
return t.createTransformedShape(tpBounds).getBounds2D();
}
/**
* Returns the bounds of the sensitive area covered by this node,
* This includes the stroked area but does not include the effects
* of clipping, masking or filtering. The returned value is
* transformed by the concatenation of the input transform and
* this node's transform.
*
* @param txf the affine transform with which this node's
* transform should be concatenated. Should not be null.
*/
public Rectangle2D getTransformedSensitiveBounds(AffineTransform txf) {
Rectangle2D sBounds = getSensitiveBounds();
if (sBounds == null) {
return null;
}
AffineTransform t = txf;
if (transform != null) {
t = new AffineTransform(txf);
t.concatenate(transform);
}
return t.createTransformedShape(sBounds).getBounds2D();
}
/**
* Returns true if the specified Point2D is inside the boundary of this
* node, false otherwise.
*
* @param p the specified Point2D in the user space
*/
public boolean contains(Point2D p) {
Rectangle2D b = getSensitiveBounds();
if (b == null || !b.contains(p)) {
return false;
}
switch(pointerEventType) {
case VISIBLE_PAINTED:
case VISIBLE_FILL:
case VISIBLE_STROKE:
case VISIBLE:
return isVisible;
case PAINTED:
case FILL:
case STROKE:
case ALL:
return true;
case NONE:
default:
return false;
}
}
/**
* Returns true if the interior of this node intersects the interior of a
* specified Rectangle2D, false otherwise.
*
* @param r the specified Rectangle2D in the user node space
*/
public boolean intersects(Rectangle2D r) {
Rectangle2D b = getBounds();
if (b == null) return false;
return b.intersects(r);
}
/**
* Returns the GraphicsNode containing point p if this node or one of its
* children is sensitive to mouse events at p.
*
* @param p the specified Point2D in the user space
*/
public GraphicsNode nodeHitAt(Point2D p) {
return (contains(p) ? this : null);
}
static double EPSILON = 1e-6;
/**
* This method makes sure that neither the width nor height of the
* rectangle is zero. But it tries to make them very small
* relatively speaking.
*/
protected Rectangle2D normalizeRectangle(Rectangle2D bounds) {
if (bounds == null) return null;
if ((bounds.getWidth() < EPSILON)) {
if (bounds.getHeight() < EPSILON) {
AffineTransform gt = getGlobalTransform();
double det = Math.sqrt(gt.getDeterminant());
return new Rectangle2D.Double
(bounds.getX(), bounds.getY(), EPSILON/det, EPSILON/det);
} else {
double tmpW = bounds.getHeight()*EPSILON;
if (tmpW < bounds.getWidth())
tmpW = bounds.getWidth();
return new Rectangle2D.Double
(bounds.getX(), bounds.getY(),
tmpW, bounds.getHeight());
}
} else if (bounds.getHeight() < EPSILON) {
double tmpH = bounds.getWidth()*EPSILON;
if (tmpH < bounds.getHeight())
tmpH = bounds.getHeight();
return new Rectangle2D.Double
(bounds.getX(), bounds.getY(),
bounds.getWidth(), tmpH);
}
return bounds;
}
}