/*
 * @(#)ClippingWrapper.java    1.0  24 October 2004
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.beans.*;

/**
 * <p>The class <code>ClippingWrapper</code> will wrap a <code>Paintable</code>
 * and a clipping <code>Shape</code> to obtain a <code>Paintable</code> that
 * clips the original <code>Paintable</code> to the <code>Shape</code>.</p>
 *
 * <p>In 2.3.2, the method <code>getBounds2D</code> was modified to use the
 * default Bounds2D rectangle if that rectangle is non-<code>null</code>
 * before making any other tests or computations.</p>
 *
 * @author  Richard Rasala
 * @version 2.3.2
 * @since   2.3
 */
public class ClippingWrapper
    extends AbstractPaintable
{
    /** Bound property name for set paintable. */
    public static final String SET_PAINTABLE      = "set.paintable";
    
    /** Bound property name for set paintable. */
    public static final String SET_CLIPPING_SHAPE = "set.clipping.shape";
    
    
    /** The wrapped paintable. */
    private Paintable paintable = null;
    
    /** The clipping shape. */
    private Shape clippingshape = null;
    
    
    /**
     * The constructor that leaves the paintable and clipping shape
     * as <code>null</code>.
     *
     * @see #ClippingWrapper(Paintable)
     * @see #ClippingWrapper(Paintable, Shape)
     */
    public ClippingWrapper() {}
    
    
    /**
     * The constructor to specify the paintable to be wrapped.
     *
     * @param paintable the paintable to be wrapped
     * @see #ClippingWrapper()
     * @see #ClippingWrapper(Paintable, Shape)
     */
    public ClippingWrapper(Paintable paintable) {
        setPaintable(paintable);
    }
    
    
    /**
     * The constructor to specify the paintable to be wrapped
     * and the clipping shape.
     *
     * @param paintable     the paintable to be wrapped
     * @param clippingshape the clipping shape
     * @see #ClippingWrapper()
     * @see #ClippingWrapper(Paintable)
     */
    public ClippingWrapper(Paintable paintable, Shape clippingshape) {
        setPaintable(paintable);
        setClippingShape(clippingshape);
    }
    
    
    /**
     * <p>Paints onto a <code>Graphics</code> context by clipping the
     * painting of the wrapped paintable to the clipping shape.</p>
     *
     * <p>If the wrapped paintable is <code>null</code>, this method will
     * not paint.</p>
     *
     * <p>If the clipping shape is <code>null</code>, no clipping will be
     * done.</p>
     *
     * @param g the graphics context on which to paint
     */
    public final void paint(Graphics g) {
        if ((paintable == null) || (g == null) || !isVisible())
            return;
        
        Graphics2D h = getPreparedGraphics2D(g);
        
        if (clippingshape != null)
            h.clip(clippingshape);
        
        paintable.paint(h);
    }
    
    
    /**
     * <p>Sets the paintable to be wrapped.</p>
     *
     * <p>The paintable may be set to <code>null</code> to eliminate the
     * paintable.  This object will then paint nothing.</p>
     *
     * <p>Fires property change: SET_PAINTABLE.</p>
     * 
     * @param paintable the paintable to be wrapped
     * @see #getPaintable()
     */
    public final void setPaintable(Paintable paintable) {
        if (paintable == this.paintable)
            return;
        
        removeAndAddForwardingListener(this.paintable, paintable);
        
        this.paintable = paintable;
        
        firePropertyChange(SET_PAINTABLE, null, null);
    }
    
    
    /**
     * Returns the wrapped paintable.
     *
     * @return the wrapped paintable
     * @see #setPaintable(Paintable)
     */
    public final Paintable getPaintable() {
        return paintable;
    }
    
    
    /**
     * <p>Sets the clipping shape.</p>
     *
     * <p>The clipping shape may be set to <code>null</code> to eliminate
     * special clipping of the wrapped paintable.</p>
     *
     * <p>Fires property change: SET_CLIPPING_SHAPE.</p>
     * 
     * @param clippingshape the clipping shape
     * @see #getClippingShape()
     */
    public final void setClippingShape(Shape clippingshape) {
        if (clippingshape == this.clippingshape)
            return;
        
        removeAndAddForwardingListener(this.clippingshape, clippingshape);
        
        this.clippingshape = clippingshape;
        
        firePropertyChange(SET_CLIPPING_SHAPE, null, null);
    }
    
    
    /**
     * Returns the clipping shape.
     *
     * @return the clipping shape
     * @see #setClippingShape(Shape)
     */
    public final Shape getClippingShape() {
        return clippingshape;
    }
    
    
    /**
     * <p>Returns the bounds of the paintable based on the default settings or on
     * more detailed computations.</p>
     *
     * <p>If the value of <code>getDefaultBounds2D</code> is non-<code>null</code>,
     * then this value is returned.</p>
     *
     * <p>Otherwise, if the wrapped paintable is <code>null</code>, this method
     * returns </code>new Rectangle2D.Double()</code>.</p>
     *
     * <p>Otherwise, the bounds of the wrapped paintable are used before clipping.
     * If the clipping shape is non-<code>null</code>, then the paintable bounds are
     * clipped to the bounds of the clipping shape.  The resulting bounds are then
     * returned.</p>
     * 
     * @return the bounds of this object
     */
    public final Rectangle2D getBounds2D() {
        // get the default bounds
        Rectangle2D bounds = getDefaultBounds2D();
        
        if (bounds != null)
            return bounds;
        
        // check paintable
        if (paintable == null)
            return new Rectangle2D.Double();
        
        // get the paintable bounds
        bounds = paintable.getBounds2D();
        
        // intersect paintable bounds with clipping bounds
        if (clippingshape != null) {
            Rectangle2D clippingbounds = clippingshape.getBounds2D();
            bounds = bounds.createIntersection(clippingbounds);
            
            double x = bounds.getX();
            double y = bounds.getY();
            double w = bounds.getWidth();
            double h = bounds.getHeight();
            
            if ((w <= 0) || (h <= 0))
                bounds.setRect(x, y, 0, 0);
        }
        
        return bounds;
    }
    
    
    /**
     * <p>If the wrapped paintable is <code>null</code>, this method returns
     * <code>new Point2D.Double()</code>.</p>
     *
     * <p>If the value of <code>getDefaultCenter</code> is non-<code>null</code>,
     * then this value is returned.</p>
     *
     * <p>Otherwise, this method returns the center of the wrapped paintable.</p>
     *
     * @return the center of this object
     */
    public final Point2D getCenter() {
        // check paintable
        if (paintable == null)
            return new Point2D.Double();
        
        // check default center
        Point2D center = getDefaultCenter();
        
        if (center != null)
            return center;
        
        // use paintable center
        return paintable.getCenter();
    }
    
    
    /**
     * <p>Tests if a point specified by coordinates is inside the this object.</p>
     *
     * <p>This method returns <code>false</code> if one or more of the following
     * conditions occurs:</p>
     *
     * <ul>
     *   <li>The wrapped paintable is <code>null</code>.</li>
     *   <li>The point is not in the rectangle <code>getBounds2D</code>.</li>
     *   <li>The method <code>isVisible</code> returns <code>false</code>.</li>
     * </ul>
     *
     * @param  x the x-coordinate of the point
     * @param  y the y-coordinate of the point
     * @return whether or not a specified point is inside the paintable
     */
    public final boolean contains(double x, double y) {
        if ((paintable == null) || !possiblyContains(x, y))
            return false;
        
        if (clippingshape == null)
            return paintable.contains(x, y);
        else
            return paintable.contains(x, y) && clippingshape.contains(x, y);
    }
    
}
