/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

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.52 2003/10/31 22:39:41 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();
        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;
        }
        if (parent != null)
            parent.invalidateGeometryCache();
        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){
            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) {
            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) {
                Rectangle2D.intersect
                    (tBounds,
                     t.createTransformedShape(clip.getClipPath()).getBounds2D(),
                     tBounds);
            }

            // Factor in the mask, if any
            if(mask != null) {
                Rectangle2D.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;
    }

}
