package prefuse;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToolTip;
import javax.swing.KeyStroke;
import javax.swing.text.JTextComponent;

import prefuse.activity.Activity;
import prefuse.activity.SlowInSlowOutPacer;
import prefuse.controls.Control;
import prefuse.data.expression.AndPredicate;
import prefuse.data.expression.BooleanLiteral;
import prefuse.data.expression.Predicate;
import prefuse.data.expression.parser.ExpressionParser;
import prefuse.render.Renderer;
import prefuse.util.ColorLib;
import prefuse.util.StringLib;
import prefuse.util.UpdateListener;
import prefuse.util.collections.CopyOnWriteArrayList;
import prefuse.util.display.BackgroundPainter;
import prefuse.util.display.Clip;
import prefuse.util.display.DebugStatsPainter;
import prefuse.util.display.ExportDisplayAction;
import prefuse.util.display.ItemBoundsListener;
import prefuse.util.display.PaintListener;
import prefuse.util.display.RenderingQueue;
import prefuse.visual.VisualItem;
import prefuse.visual.expression.VisiblePredicate;
import prefuse.visual.sort.ItemSorter;


/**
 * <p>User interface component that provides an interactive view onto
 * a visualization. The Display is responsible for drawing items to the
 * screen and providing callbacks for user interface actions such as
 * mouse and keyboard events. A Display must be associated with an
 * {@link prefuse.Visualization} from which it pulls the items to visualize.
 * </p>
 * 
 * <p>To control which {@link prefuse.visual.VisualItem} instances are
 * drawn, the Display also maintains an optional 
 * {@link prefuse.data.expression.Predicate} for filtering items. The
 * drawing order of items is
 * controlled by an {@link prefuse.visual.sort.ItemSorter} instance,
 * which calculates a score for each item. Items with higher scores
 * are drawn later, and hence on top of lower scoring items.
 * </p>
 * 
 * <p>The {@link prefuse.controls.Control Control}
 * interface provides the user interface callbacks for supporting
 * interaction. The {@link prefuse.controls} package contains a number
 * of pre-built <code>Control</code> implementations for common
 * interactions.</p>
 * 
 * <p>The Display class also supports arbitrary graphics transforms through
 * the <code>java.awt.geom.AffineTransform</code> class. The 
 * {@link #setTransform(java.awt.geom.AffineTransform) setTransform} method
 * allows arbitrary transforms to be applied, while the 
 * {@link #pan(double,double) pan} and 
 * {@link #zoom(java.awt.geom.Point2D,double) zoom}
 * methods provide convenience methods that appropriately update the current
 * transform to achieve panning and zooming of the presentation space.</p>
 * 
 * <p>Additionally, each Display instance also supports use of a text editor
 * to facilitate direct editing of text. See the various
 * {@link #editText(prefuse.visual.VisualItem, String)} methods.</p>
 * 
 * @version 1.0
 * @author <a href="http://jheer.org">jeffrey heer</a>
 * @see Visualization
 * @see prefuse.controls.Control
 * @see prefuse.controls
 */
public class Display extends JComponent {

    private static final Logger s_logger 
        = Logger.getLogger(Display.class.getName());
    
    // visual item source
    protected Visualization m_vis;
    protected AndPredicate  m_predicate = new AndPredicate();
    
    // listeners
    protected CopyOnWriteArrayList m_controls = new CopyOnWriteArrayList();
    protected CopyOnWriteArrayList m_painters;
    protected CopyOnWriteArrayList m_bounders;
    
    // display
    protected BufferedImage m_offscreen;
    protected Clip          m_clip = new Clip();
    protected Clip          m_screen = new Clip();
    protected Clip          m_bounds = new Clip();
    protected Rectangle2D   m_rclip = new Rectangle2D.Double();
    protected boolean       m_damageRedraw = true;
    protected boolean       m_highQuality = false;
    
    // optional background image
    protected BackgroundPainter m_bgpainter = null;
    
    // rendering queue
    protected RenderingQueue m_queue = new RenderingQueue();
    protected int            m_visibleCount = 0;
    
    // transform variables
    protected AffineTransform   m_transform  = new AffineTransform();
    protected AffineTransform   m_itransform = new AffineTransform();
    protected TransformActivity m_transact = new TransformActivity();
    protected Point2D m_tmpPoint = new Point2D.Double();
    
    // frame count and debugging output
    protected double frameRate;
    protected int nframes = 0;
    private int sampleInterval = 10;
    private long mark = -1L;
    
    /* Custom tooltip, null to use regular tooltip mechanisms */
    protected JToolTip m_customToolTip = null;
    
    // text editing variables
    private JTextComponent m_editor;
    private boolean        m_editing;
    private VisualItem     m_editItem;
    private String         m_editAttribute;
    
    /**
     * Creates a new Display instance. You will need to associate this
     * Display with a {@link Visualization} for it to display anything.
     */
    public Display() {
        this(null);
    }
    
    /**
     * Creates a new Display associated with the given Visualization.
     * By default, all {@link prefuse.visual.VisualItem} instances in the
     * {@link Visualization} will be drawn by the Display.
     * @param visualization the {@link Visualization} backing this Display
     */
    public Display(Visualization visualization) {
        this(visualization, (Predicate)null);
    }
    
    /**
     * Creates a new Display associated with the given Visualization that
     * draws all VisualItems in the visualization that pass the given
     * Predicate. The predicate string will be parsed by the
     * {@link prefuse.data.expression.parser.ExpressionParser} to get a
     * {@link prefuse.data.expression.Predicate} instance.
     * @param visualization the {@link Visualization} backing this Display
     * @param predicate a predicate expression in the prefuse expression
     * language. This expression will be parsed; if the parsing fails or does
     * not result in a Predicate instance, an exception will result.
     */
    public Display(Visualization visualization, String predicate) {
        this(visualization,
                (Predicate)ExpressionParser.parse(predicate, true));
    }
    
    /**
     * Creates a new Display associated with the given Visualization that
     * draws all VisualItems in the visualization that pass the given
     * Predicate.
     * @param visualization the {@link Visualization} backing this Display
     * @param predicate the filtering {@link prefuse.data.expression.Predicate}
     */
    public Display(Visualization visualization, Predicate predicate) {
        setDoubleBuffered(false);
        setBackground(Color.WHITE);
        
        // initialize text editor
        m_editing = false;
        m_editor = new JTextField();
        m_editor.setBorder(null);
        m_editor.setVisible(false);
        this.add(m_editor);
        
        // register input event capturer
        InputEventCapturer iec = new InputEventCapturer();
        addMouseListener(iec);
        addMouseMotionListener(iec);
        addMouseWheelListener(iec);
        addKeyListener(iec);
        
        registerDefaultCommands();
        
        // invalidate the display when the filter changes
        m_predicate.addExpressionListener(new UpdateListener() {
            public void update(Object src) { damageReport(); }
        });
        
        setVisualization(visualization);
        setPredicate(predicate);
        setSize(400,400); // set a default size
    }
    
    /**
     * Registers default keystroke commands on the Display. The default
     * commands are
     * <ul><li><b>ctrl D</b> - Toggle debug info display</li>
     *     <li><b>ctrl H</b> - Toggle high quality rendering</li>
     *     <li><b>ctrl E</b> - Export display view to an image file</li></ul>
     * Subclasses can override this method to prevent these commands from
     * being set. Additional commands can be registered using the
     * <code>registerKeyboardAction</code> method.
     */
    protected void registerDefaultCommands() {
        // add debugging output control
        registerKeyboardAction(new ActionListener() {
            private PaintListener m_debug = null;

            public void actionPerformed(ActionEvent e) {
                if (m_debug == null) {
                    m_debug = new DebugStatsPainter();
                    addPaintListener(m_debug);
                } else {
                    removePaintListener(m_debug);
                    m_debug = null;
                }
                repaint();
            }
        }, "debug info", KeyStroke.getKeyStroke("ctrl D"), WHEN_FOCUSED);
        
        // add quality toggle
        registerKeyboardAction(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                setHighQuality(!isHighQuality());
                repaint();
            }
        }, "toggle high-quality drawing", KeyStroke.getKeyStroke("ctrl H"),
                WHEN_FOCUSED);
        
        // add image output control, if this is not an applet
        try {
            registerKeyboardAction(new ExportDisplayAction(this),
             "export display", KeyStroke.getKeyStroke("ctrl E"), WHEN_FOCUSED);
        } catch (SecurityException se) {
        }
    }
    
    /**
     * Set the size of the Display.
     * @param width the width of the Display in pixels
     * @param height the height of the Display in pixels
     * @see java.awt.Component#setSize(int, int)
     */
    public void setSize(int width, int height) {
        m_offscreen = null;
        setPreferredSize(new Dimension(width, height));
        super.setSize(width, height);
    }
    
    /**
     * Set the size of the Display.
     * @param d the dimensions of the Display in pixels
     * @see java.awt.Component#setSize(java.awt.Dimension)
     */
    public void setSize(Dimension d) {
        m_offscreen = null;
        setPreferredSize(d);
        super.setSize(d);
    }

    /**
     * Invalidates this component. Overridden to ensure that an
     * internal damage report is generated.
     * @see java.awt.Component#invalidate()
     */
    public void invalidate() {
        damageReport();
        super.invalidate();
    }
    
    /**
     * @see java.awt.Component#setBounds(int, int, int, int)
     */
    public void setBounds(int x, int y, int w, int h) {
        m_offscreen = null;
        super.setBounds(x,y,w,h);
    }
    
    /**
     * Sets the font used by this Display. This determines the font used
     * by this Display's text editor and in any debugging text.
     * @param f the Font to use
     */
    public void setFont(Font f) {
        super.setFont(f);
        m_editor.setFont(f);
    }
    
    /**
     * Returns the running average frame rate for this Display.
     * @return the frame rate
     */
    public double getFrameRate() {
        return frameRate;
    }
    
    /**
     * Determines if the Display uses a higher quality rendering, using
     * anti-aliasing. This causes drawing to be much slower, however, and
     * so is disabled by default.
     * @param on true to enable anti-aliased rendering, false to disable it
     */
    public void setHighQuality(boolean on) {
        if ( m_highQuality != on )
            damageReport();
        m_highQuality = on;
    }
    
    /**
     * Indicates if the Display is using high quality (return value true) or
     * regular quality (return value false) rendering.
     * @return true if high quality rendering is enabled, false otherwise
     */
    public boolean isHighQuality() {
        return m_highQuality;
    }
    
    /**
     * Returns the Visualization backing this Display.
     * @return this Display's {@link Visualization}
     */
    public Visualization getVisualization() {
        return m_vis;
    }
    
    /**
     * Set the Visualiztion associated with this Display. This Display
     * will render the items contained in the provided visualization. If this
     * Display is already associated with a different Visualization, the
     * Display unregisters itself with the previous one.
     * @param vis the backing {@link Visualization} to use.
     */
    public void setVisualization(Visualization vis) {
        // TODO: synchronization?
        if ( m_vis == vis ) {
            // nothing need be done
            return;
        } else if ( m_vis != null ) {
            // remove this display from it's previous registry
            m_vis.removeDisplay(this);
        }
        m_vis = vis;
        if ( m_vis != null )
            m_vis.addDisplay(this);
    }
    
    /**
     * Returns the filtering Predicate used to control what items are drawn
     * by this display.
     * @return the filtering {@link prefuse.data.expression.Predicate}
     */
    public Predicate getPredicate() {
        if ( m_predicate.size() == 1 ) {
            return BooleanLiteral.TRUE;
        } else {
            return m_predicate.get(0);
        }
    }

    /**
     * Sets the filtering Predicate used to control what items are drawn by
     * this Display.
     * @param expr the filtering predicate to use. The predicate string will be
     * parsed by the {@link prefuse.data.expression.parser.ExpressionParser}.
     * If the parse fails or does not result in a
     * {@link prefuse.data.expression.Predicate} instance, an exception will
     * be thrown.
     */
    public void setPredicate(String expr) {
        Predicate p = (Predicate)ExpressionParser.parse(expr, true);
        setPredicate(p);
    }
    
    /**
     * Sets the filtering Predicate used to control what items are drawn by
     * this Display.
     * @param p the filtering {@link prefuse.data.expression.Predicate} to use
     */
    public synchronized void setPredicate(Predicate p) {
        if ( p == null ) {
            m_predicate.set(VisiblePredicate.TRUE);
        } else {
            m_predicate.set(new Predicate[] {p, VisiblePredicate.TRUE});
        }
    }
    
    /**
     * Returns the number of visible items processed by this Display. This
     * includes items not currently visible on screen due to the current
     * panning or zooming state.
     * @return the count of visible items
     */
    public int getVisibleItemCount() {
        return m_visibleCount;
    }
    
    /**
     * Get the ItemSorter that determines the rendering order of the
     * VisualItems. Items are drawn in ascending order of the scores provided
     * by the ItemSorter.
     * @return this Display's {@link prefuse.visual.sort.ItemSorter}
     */
    public ItemSorter getItemSorter() {
        return m_queue.sort;
    }

    /**
     * Set the ItemSorter that determines the rendering order of the
     * VisualItems. Items are drawn in ascending order of the scores provided
     * by the ItemSorter.
     * @return the {@link prefuse.visual.sort.ItemSorter} to use
     */
    public synchronized void setItemSorter(ItemSorter cmp) {
        damageReport();
        m_queue.sort = cmp;
    }
    

    /**
     * Set a background image for this display.
     * @param image the background Image. If a null value is provided, 
     * than no background image will be shown.
     * @param fixed true if the background image should stay in a fixed
     * position, invariant to panning, zooming, or rotation; false if
     * the image should be subject to view transforms
     * @param tileImage true to tile the image across the visible background,
     * false to only include the image once
     */
    public synchronized void setBackgroundImage(Image image,
                                      boolean fixed, boolean tileImage)
    {
        BackgroundPainter bg = null;
        if ( image != null )
            bg = new BackgroundPainter(image, fixed, tileImage);
        setBackgroundPainter(bg);
    }

    /**
     * Set a background image for this display.
     * @param location a location String of where to retrieve the
     * image file from. Uses
     * {@link prefuse.util.io.IOLib#urlFromString(String)} to resolve
     * the String. If a null value is provided, than no background
     * image will be shown.
     * @param fixed true if the background image should stay in a fixed
     * position, invariant to panning, zooming, or rotation; false if
     * the image should be subject to view transforms
     * @param tileImage true to tile the image across the visible background,
     * false to only include the image once
     */
    public synchronized void setBackgroundImage(String location,
                                      boolean fixed, boolean tileImage)
    {
        BackgroundPainter bg = null;
        if ( location != null )
            bg = new BackgroundPainter(location, fixed, tileImage);
        setBackgroundPainter(bg);        
    }
    
    private void setBackgroundPainter(BackgroundPainter bg) {
        if ( m_bgpainter != null )
            removePaintListener(m_bgpainter);
        m_bgpainter = bg;
        if ( bg != null )
            addPaintListener(bg);
    }
    
    // ------------------------------------------------------------------------
    // ToolTips

    /**
     * Returns the tooltip instance to use for this Display. By default, uses
     * the normal Swing tooltips, returning the result of this same method
     * invoked on the JComponent super-class. If a custom tooltip has been
     * set, that is returned instead.
     * @see #setCustomToolTip(JToolTip)
     * @see javax.swing.JComponent#createToolTip()
     */
    public JToolTip createToolTip() {
        if ( m_customToolTip == null ) {
            return super.createToolTip();
        } else {
            return m_customToolTip;
        }
    }
    
    /**
     * Set a custom tooltip to use for this Display. To trigger tooltip
     * display, you must still use the <code>setToolTipText</code> method
     * as usual. The actual text will no longer have any effect, other
     * than that a null text value will result in no tooltip display
     * while a non-null text value will result in a tooltip being
     * shown. Clients are responsible for setting the tool tip
     * text to enable/disable tooltips as well as updating the content
     * of their own custom tooltip instance.
     * @param tooltip the tooltip component to use
     * @see prefuse.util.ui.JCustomTooltip
     */
    public void setCustomToolTip(JToolTip tooltip) {
        m_customToolTip = tooltip;
    }
    
    /**
     * Get the custom tooltip used by this Display. Returns null if normal
     * tooltips are being used.
     * @return the custom tooltip used by this Display, or null if none
     */
    public JToolTip getCustomToolTip() {
        return m_customToolTip;
    }
    
    // ------------------------------------------------------------------------
    // Clip / Bounds Management
    
    /**
     * Indicates if damage/redraw rendering is enabled. If enabled, the display
     * will only redraw within the bounding box of all areas that have changed
     * since the last rendering operation. For small changes, such as a single
     * item being dragged, this can result in a significant performance
     * increase. By default, the damage/redraw optimization is enabled. It can
     * be disabled, however, if rendering artifacts are appearing in your
     * visualization. Be careful though, as this may not be the best solution.
     * Rendering artifacts may result because the item bounds returned by
     * {@link prefuse.visual.VisualItem#getBounds()} are not accurate and the
     * item's {@link prefuse.render.Renderer} is drawing outside of the
     * reported bounds. In this case, there is usually a bug in the Renderer.
     * One reported problem arises from Java itself, however, which
     * inaccurately redraws images outside of their reported bounds. If you
     * have a visulization with a number of images and are seeing rendering
     * artifacts, try disabling damage/redraw.
     * @return true if damage/redraw optimizations are enabled, false
     * otherwise (in which case the entire Display is redrawn upon a repaint)
     */
    public synchronized boolean isDamageRedraw() {
        return m_damageRedraw;
    }
    
    /**
     * Sets if damage/redraw rendering is enabled. If enabled, the display
     * will only redraw within the bounding box of all areas that have changed
     * since the last rendering operation. For small changes, such as a single
     * item being dragged, this can result in a significant performance
     * increase. By default, the damage/redraw optimization is enabled. It can
     * be disabled, however, if rendering artifacts are appearing in your
     * visualization. Be careful though, as this may not be the best solution.
     * Rendering artifacts may result because the item bounds returned by
     * {@link prefuse.visual.VisualItem#getBounds()} are not accurate and the
     * item's {@link prefuse.render.Renderer} is drawing outside of the
     * reported bounds. In this case, there is usually a bug in the Renderer.
     * One reported problem arises from Java itself, however, which
     * inaccurately redraws images outside of their reported bounds. If you
     * have a visulization with a number of images and are seeing rendering
     * artifacts, try disabling damage/redraw.
     * @param b true to enable damage/redraw optimizations, false otherwise
     * (in which case the entire Display will be redrawn upon a repaint)
     */
    public synchronized void setDamageRedraw(boolean b) {
        m_damageRedraw = b;
        m_clip.invalidate();
    }
    
    /**
     * Reports damage to the Display within in the specified region.
     * @param region the damaged region, in absolute coordinates
     */
    public synchronized void damageReport(Rectangle2D region) {
        if ( m_damageRedraw )
            m_clip.union(region);
    }
    
    /**
     * Reports damage to the entire Display.
     */
    public synchronized void damageReport() {
        m_clip.invalidate();
    }
   
    /**
     * Clears any reports of damaged regions, causing the Display to believe
     * that the display contents are up-to-date. If used incorrectly this
     * can cause inaccurate rendering. <strong>Call this method only
     * if you know what you are doing.</strong>
     */
    public synchronized void clearDamage() {
        if ( m_damageRedraw )
            m_clip.reset();
    }
    
    /**
     * Returns the bounds, in absolute (item-space) coordinates, of the total
     * bounds occupied by all currently visible VisualItems. This method
     * allocates a new Rectangle2D instance for the result.
     * @return the bounding box of all visibile VisualItems
     * @see #getItemBounds(Rectangle2D)
     */
    public synchronized Rectangle2D getItemBounds() {
        return getItemBounds(new Rectangle2D.Double());
    }

    /**
     * Returns the bounds, in absolute (item-space) coordinates, of the total
     * bounds occupied by all currently visible VisualItems.
     * @param b the Rectangle2D to use to store the return value
     * @return the bounding box of all visibile VisualItems
     */
    public synchronized Rectangle2D getItemBounds(Rectangle2D b) {
        b.setFrameFromDiagonal(m_bounds.getMinX(), m_bounds.getMinY(),
                               m_bounds.getMaxX(), m_bounds.getMaxY());
        return b;
    }
    
    // ------------------------------------------------------------------------
    // Rendering
    
    /**
     * Returns the offscreen buffer used for double buffering.
     * @return the offscreen buffer
     */
    public BufferedImage getOffscreenBuffer() {
        return m_offscreen;
    }
    
    /**
     * Creates a new buffered image to use as an offscreen buffer.
     */
    protected BufferedImage getNewOffscreenBuffer(int width, int height) {
        BufferedImage img = null;
        if ( !GraphicsEnvironment.isHeadless() ) {
            try {
                img = (BufferedImage)createImage(width, height);
            } catch ( Exception e ) {
                img = null;
            }
        }
        if ( img == null ) {
            return new BufferedImage(width, height,
                                     BufferedImage.TYPE_INT_RGB);
        }
        return img;
    }
    
    /**
     * Saves a copy of this display as an image to the specified output stream.
     * @param output the output stream to write to.
     * @param format the image format (e.g., "JPG", "PNG"). The number and kind
     * of available formats varies by platform. See
     * {@link javax.imageio.ImageIO} and related classes for more.
     * @param scale how much to scale the image by. For example, a value of 2.0
     * will result in an image with twice the pixel width and height of this
     * Display.
     * @return true if image was successfully saved, false if an error occurred.
     */
    public boolean saveImage(OutputStream output, String format, double scale)
    {
        try {
            // get an image to draw into
            Dimension d = new Dimension((int)(scale*getWidth()),
                                        (int)(scale*getHeight()));
            BufferedImage img = getNewOffscreenBuffer(d.width, d.height);
            Graphics2D g = (Graphics2D)img.getGraphics();
            
            // set up the display, render, then revert to normal settings
            Point2D p = new Point2D.Double(0,0);
            zoom(p, scale); // also takes care of damage report
            boolean q = isHighQuality();
            setHighQuality(true);
            paintDisplay(g, d);
            setHighQuality(q);
            zoom(p, 1/scale); // also takes care of damage report
            
            // save the image and return
            ImageIO.write(img, format, output);
            return true;
        } catch ( Exception e ) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * @see java.awt.Component#update(java.awt.Graphics)
     */
    public void update(Graphics g) {
        paint(g);
    }
    
    /**
     * Paints the offscreen buffer to the provided graphics context.
     * @param g the Graphics context to paint to
     */
    protected void paintBufferToScreen(Graphics g) {
        synchronized ( this ) {
            g.drawImage(m_offscreen, 0, 0, null);
        }
    }

    /**
     * Immediately repaints the contents of the offscreen buffer
     * to the screen. This bypasses the usual rendering loop.
     */
    public void repaintImmediate() {
        Graphics g = this.getGraphics();
        if (g != null && m_offscreen != null) {
            paintBufferToScreen(g);
        }
    }

    /**
     * Sets the transform of the provided Graphics context to be the
     * transform of this Display and sets the desired rendering hints.
     * @param g the Graphics context to prepare.
     */
    protected void prepareGraphics(Graphics2D g) {
        if ( m_transform != null )
            g.transform(m_transform);
        setRenderingHints(g);
    }
    
    /**
     * Sets the rendering hints that should be used while drawing
     * the visualization to the screen. Subclasses can override
     * this method to set hints as desired. Such subclasses should
     * consider honoring the high quality flag in one form or another.
     * @param g the Graphics context on which to set the rendering hints
     */
    protected void setRenderingHints(Graphics2D g) {
        if ( m_highQuality ) {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
        } else {
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_OFF);
        }
        g.setRenderingHint(
            RenderingHints.KEY_RENDERING,
            RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BICUBIC);
    }


    /**
     * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
     */
    public void paintComponent(Graphics g) {
        if (m_offscreen == null) {
            m_offscreen = getNewOffscreenBuffer(getWidth(), getHeight());
            damageReport();
        }
        Graphics2D g2D = (Graphics2D)g;
        Graphics2D buf_g2D = (Graphics2D) m_offscreen.getGraphics();
        
        // Why not fire a pre-paint event here?
        // Pre-paint events are fired by the clearRegion method
        
        // paint the visualization
        paintDisplay(buf_g2D, getSize());
        paintBufferToScreen(g2D);       
        
        // fire post-paint events to any painters
        firePostPaint(g2D);
        
        buf_g2D.dispose();
        
        // compute frame rate
        nframes++;
        if ( mark < 0 ) {
            mark = System.currentTimeMillis();
            nframes = 0;
        } else if ( nframes == sampleInterval ){
            long t = System.currentTimeMillis();
            frameRate = (1000.0*nframes)/(t-mark);
            mark = t;
            nframes = 0;
        }
    }
    
    /**
     * Renders the display within the given graphics context and size bounds.
     * @param g2D the <code>Graphics2D</code> context to use for rendering
     * @param d the rendering width and height of the Display
     */
    public void paintDisplay(Graphics2D g2D, Dimension d) {
        // if double-locking *ALWAYS* lock on the visualization first
        synchronized ( m_vis ) {
        synchronized ( this ) {
            
            if ( m_clip.isEmpty() )
                return; // no damage, no render
            
            // map the screen bounds to absolute coords
            m_screen.setClip(0, 0, d.width+1, d.height+1);
            m_screen.transform(m_itransform);
            
            // compute the approximate size of an "absolute pixel"
            // values too large are OK (though cause unnecessary rendering)
            // values too small will cause incorrect rendering
            double pixel = 1.0 + 1.0/getScale();
            
            if ( m_damageRedraw ) {  
                if ( m_clip.isInvalid() ) {
                    // if clip is invalid, we clip to the entire screen
                    m_clip.setClip(m_screen);
                } else {
                    // otherwise intersect damaged region with display bounds
                    m_clip.intersection(m_screen);
                }
  
                // expand the clip by the extra pixel margin
                m_clip.expand(pixel);
                
                // set the transform, rendering keys, etc
                prepareGraphics(g2D);
                
                // now set the actual rendering clip
                m_rclip.setFrameFromDiagonal(
                        m_clip.getMinX(), m_clip.getMinY(), 
                        m_clip.getMaxX(), m_clip.getMaxY());
                g2D.setClip(m_rclip);
                
                // finally, we want to clear the region we'll redraw. we clear
                // a slightly larger area than the clip. if we don't do this,
                // we sometimes get rendering artifacts, possibly due to
                // scaling mismatches in the Java2D implementation
                m_rclip.setFrameFromDiagonal(
                        m_clip.getMinX()-pixel, m_clip.getMinY()-pixel,
                        m_clip.getMaxX()+pixel, m_clip.getMaxY()+pixel);

            } else {
                // set the background region to clear
                m_rclip.setFrame(0, 0, getWidth(), getHeight());
                // set the item clip to the current screen
                m_clip.setClip(m_screen);
                // set the transform, rendering keys, etc
                prepareGraphics(g2D);
            }

            // now clear the region
            clearRegion(g2D, m_rclip);
            
            // -- render ----------------------------
            // the actual rendering  loop
            
            // copy current item bounds into m_rclip, reset item bounds
            getItemBounds(m_rclip);
            m_bounds.reset();
            
            // fill the rendering and picking queues
            m_queue.clear();   // clear the queue
            Iterator items = m_vis.items(m_predicate);
            for ( m_visibleCount=0; items.hasNext(); ++m_visibleCount ) {
                VisualItem item = (VisualItem)items.next();
                Rectangle2D bounds = item.getBounds();
                m_bounds.union(bounds); // add to item bounds
                
                if ( m_clip.intersects(bounds, pixel) )
                    m_queue.addToRenderQueue(item);
                if ( item.isInteractive() )
                    m_queue.addToPickingQueue(item);
            }
            
            // sort the rendering queue
            m_queue.sortRenderQueue();
            
            // render each visual item
            for ( int i=0; i<m_queue.rsize; ++i ) {
                m_queue.ritems[i].render(g2D);
            }
            
            // no more damage so reset the clip
            if ( m_damageRedraw )
                m_clip.reset();
            
            // fire bounds change, if appropriate
            checkItemBoundsChanged(m_rclip);
            
        }} // end synchronized block
    }
    
    /**
     * Immediately render the given VisualItem to the screen. This method
     * bypasses the Display's offscreen buffer.
     * @param item the VisualItem to render immediately
     */
    public void renderImmediate(VisualItem item) {
        Graphics2D g2D = (Graphics2D)this.getGraphics();
        prepareGraphics(g2D);
        item.render(g2D);
    }
    
    /**
     * Paints the graph to the provided graphics context, for output to a
     * printer.  This method does not double buffer the painting, in order to
     * provide the maximum print quality.
     * 
     * <b>This method may not be working correctly,
     * and will be repaired at a later date.</b>
     * 
     * @param g the printer graphics context.
     */
    protected void printComponent(Graphics g) {
        boolean wasHighQuality = m_highQuality;
        try {
            // Set the quality to high for the duration of the printing.
            m_highQuality = true;
            // Paint directly to the print graphics context.
            paintDisplay((Graphics2D) g, getSize());
        } finally {
            // Reset the quality to the state it was in before printing.
            m_highQuality = wasHighQuality;
        }
    }
    
    /**
     * Clears the specified region of the display
     * in the display's offscreen buffer.
     */    
    protected void clearRegion(Graphics2D g, Rectangle2D r) {
        g.setColor(getBackground());
        g.fill(r);
        // fire pre-paint events to any painters
        firePrePaint(g);
    }

    // ------------------------------------------------------------------------
    // Transformations
    
    /**
     * Set the 2D AffineTransform (e.g., scale, shear, pan, rotate) used by
     * this display before rendering visual items. The provided transform
     * must be invertible, otherwise an expection will be thrown. For simple
     * panning and zooming transforms, you can instead use the provided
     * pan() and zoom() methods.
     */
    public synchronized void setTransform(AffineTransform transform) 
        throws NoninvertibleTransformException
    {
        damageReport();
        m_transform = transform;
        m_itransform = m_transform.createInverse();
    }
    
    /**
     * Returns a reference to the AffineTransformation used by this Display.
     * Changes made to this reference WILL corrupt the state of 
     * this display. Use setTransform() to safely update the transform state.
     * @return the AffineTransform
     */
    public AffineTransform getTransform() {
        return m_transform;
    }
    
    /**
     * Returns a reference to the inverse of the AffineTransformation used by
     * this display. Direct changes made to this reference WILL corrupt the
     * state of this display.
     * @return the inverse AffineTransform
     */
    public AffineTransform getInverseTransform() {
        return m_itransform;
    }
    
    /**
     * Gets the absolute co-ordinate corresponding to the given screen
     * co-ordinate.
     * @param screen the screen co-ordinate to transform
     * @param abs a reference to put the result in. If this is the same
     *  object as the screen co-ordinate, it will be overridden safely. If
     *  this value is null, a new Point2D instance will be created and 
     *  returned.
     * @return the point in absolute co-ordinates
     */
    public Point2D getAbsoluteCoordinate(Point2D screen, Point2D abs) {
        return m_itransform.transform(screen, abs);
    }
    
    /**
     * Returns the current scale (zoom) value.
     * @return the current scale. This is the
     *  scaling factor along the x-dimension, so be careful when
     *  using this value in rare non-uniform scaling cases.
     */
    public double getScale() {
        return m_transform.getScaleX();
    }
    
    /**
     * Returns the x-coordinate of the top-left of the display, 
     * in absolute (item-space) co-ordinates.
     * @return the x co-ord of the top-left corner, in absolute coordinates
     */
    public double getDisplayX() {
        return -m_transform.getTranslateX();
    }
    
    /**
     * Returns the y-coordinate of the top-left of the display, 
     * in absolute (item-space) co-ordinates.
     * @return the y co-ord of the top-left corner, in absolute coordinates
     */
    public double getDisplayY() {
        return -m_transform.getTranslateY();
    }
    
    /**
     * Pans the view provided by this display in screen coordinates.
     * @param dx the amount to pan along the x-dimension, in pixel units
     * @param dy the amount to pan along the y-dimension, in pixel units
     */
    public synchronized void pan(double dx, double dy) {
        double panx = dx / m_transform.getScaleX();
        double pany = dy / m_transform.getScaleY();
        panAbs(panx,pany);
    }
    
    /**
     * Pans the view provided by this display in absolute (i.e. item-space)
     * coordinates.
     * @param dx the amount to pan along the x-dimension, in absolute co-ords
     * @param dy the amount to pan along the y-dimension, in absolute co-ords
     */
    public synchronized void panAbs(double dx, double dy) {
        damageReport();
        m_transform.translate(dx, dy);
        try {
            m_itransform = m_transform.createInverse();
        } catch ( Exception e ) { /*will never happen here*/ }
    }
    
    /**
     * Pans the display view to center on the provided point in 
     * screen (pixel) coordinates.
     * @param p the point to center on, in screen co-ords
     */
    public synchronized void panTo(Point2D p) {
        m_itransform.transform(p, m_tmpPoint);
        panToAbs(m_tmpPoint);
    }
    
    /**
     * Pans the display view to center on the provided point in 
     * absolute (i.e. item-space) coordinates.
     * @param p the point to center on, in absolute co-ords
     */
    public synchronized void panToAbs(Point2D p) {
        double sx = m_transform.getScaleX();
        double sy = m_transform.getScaleY();
        double x = p.getX(); x = (Double.isNaN(x) ? 0 : x);
        double y = p.getY(); y = (Double.isNaN(y) ? 0 : y);
        x = getWidth() /(2*sx) - x;
        y = getHeight()/(2*sy) - y;
        
        double dx = x-(m_transform.getTranslateX()/sx);
        double dy = y-(m_transform.getTranslateY()/sy);

        damageReport();
        m_transform.translate(dx, dy);
        try {
            m_itransform = m_transform.createInverse();
        } catch ( Exception e ) { /*will never happen here*/ }
    }

    /**
     * Zooms the view provided by this display by the given scale,
     * anchoring the zoom at the specified point in screen coordinates.
     * @param p the anchor point for the zoom, in screen coordinates
     * @param scale the amount to zoom by
     */
    public synchronized void zoom(final Point2D p, double scale) {
        m_itransform.transform(p, m_tmpPoint);
        zoomAbs(m_tmpPoint, scale);
    }    
    
    /**
     * Zooms the view provided by this display by the given scale,
     * anchoring the zoom at the specified point in absolute coordinates.
     * @param p the anchor point for the zoom, in absolute
     *  (i.e. item-space) co-ordinates
     * @param scale the amount to zoom by
     */
    public synchronized void zoomAbs(final Point2D p, double scale) {;
        double zx = p.getX(), zy = p.getY();
        damageReport();
        m_transform.translate(zx, zy);
        m_transform.scale(scale,scale);
        m_transform.translate(-zx, -zy);
        try {
            m_itransform = m_transform.createInverse();
        } catch ( Exception e ) { /*will never happen here*/ }
    }
    
    /**
     * Rotates the view provided by this display by the given angle in radians,
     * anchoring the rotation at the specified point in screen coordinates.
     * @param p the anchor point for the rotation, in screen coordinates
     * @param theta the angle to rotate by, in radians
     */
    public synchronized void rotate(final Point2D p, double theta) {
        m_itransform.transform(p, m_tmpPoint);
        rotateAbs(m_tmpPoint, theta);
    }    
    
    /**
     * Rotates the view provided by this display by the given angle in radians,
     * anchoring the rotation at the specified point in absolute coordinates.
     * @param p the anchor point for the rotation, in absolute
     *  (i.e. item-space) co-ordinates
     * @param theta the angle to rotation by, in radians
     */
    public synchronized void rotateAbs(final Point2D p, double theta) {
        double zx = p.getX(), zy = p.getY();
        damageReport();
        m_transform.translate(zx, zy);
        m_transform.rotate(theta);
        m_transform.translate(-zx, -zy);
        try {
            m_itransform = m_transform.createInverse();
        } catch ( Exception e ) { /*will never happen here*/ }
    }

    /**
     * Animate a pan along the specified distance in screen (pixel) 
     * co-ordinates using the provided duration.
     * @param dx the amount to pan along the x-dimension, in pixel units
     * @param dy the amount to pan along the y-dimension, in pixel units
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePan(double dx, double dy, long duration) {
        double panx = dx / m_transform.getScaleX();
        double pany = dy / m_transform.getScaleY();
        animatePanAbs(panx,pany,duration);
    }
    
    /**
     * Animate a pan along the specified distance in absolute (item-space)
     * co-ordinates using the provided duration.
     * @param dx the amount to pan along the x-dimension, in absolute co-ords
     * @param dy the amount to pan along the y-dimension, in absolute co-ords
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePanAbs(double dx, double dy, long duration) {
        m_transact.pan(dx,dy,duration);
    }
    
    /**
     * Animate a pan to the specified location in screen (pixel) 
     * co-ordinates using the provided duration.
     * @param p the point to pan to in screen (pixel) units
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePanTo(Point2D p, long duration) {
        Point2D pp = new Point2D.Double();
        m_itransform.transform(p,pp);
        animatePanToAbs(pp,duration);
    }
    
    /**
     * Animate a pan to the specified location in absolute (item-space) 
     * co-ordinates using the provided duration.
     * @param p the point to pan to in absolute (item-space) units
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePanToAbs(Point2D p, long duration) {
        m_tmpPoint.setLocation(0,0);
        m_itransform.transform(m_tmpPoint,m_tmpPoint);
        double x = p.getX(); x = (Double.isNaN(x) ? 0 : x);
        double y = p.getY(); y = (Double.isNaN(y) ? 0 : y);
        double w = getWidth() /(2*m_transform.getScaleX());
        double h = getHeight()/(2*m_transform.getScaleY());
        double dx = w-x+m_tmpPoint.getX();
        double dy = h-y+m_tmpPoint.getY();
        animatePanAbs(dx,dy,duration);
    }
    
    /**
     * Animate a zoom centered on a given location in screen (pixel) 
     * co-ordinates by the given scale using the provided duration.
     * @param p the point to center on in screen (pixel) units
     * @param scale the scale factor to zoom by
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animateZoom(final Point2D p, double scale, long duration) {
        Point2D pp = new Point2D.Double();
        m_itransform.transform(p,pp);
        animateZoomAbs(pp,scale,duration);
    }
    
    /**
     * Animate a zoom centered on a given location in absolute (item-space) 
     * co-ordinates by the given scale using the provided duration.
     * @param p the point to center on in absolute (item-space) units
     * @param scale the scale factor to zoom by
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animateZoomAbs(final Point2D p, double scale, long duration) {
        m_transact.zoom(p,scale,duration);
    }
    
    /**
     * Animate a pan to the specified location in screen (pixel) 
     * co-ordinates and zoom to the given scale using the provided duration.
     * @param p the point to center on in screen (pixel) units
     * @param scale the scale factor to zoom by
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePanAndZoomTo(final Point2D p, double scale, long duration) {
        Point2D pp = new Point2D.Double();
        m_itransform.transform(p,pp);
        animatePanAndZoomToAbs(pp,scale,duration);
    }
    
    /**
     * Animate a pan to the specified location in absolute (item-space) 
     * co-ordinates and zoom to the given scale using the provided duration.
     * @param p the point to center on in absolute (item-space) units
     * @param scale the scale factor to zoom by
     * @param duration the duration of the animation, in milliseconds
     */
    public synchronized void animatePanAndZoomToAbs(final Point2D p, double scale, long duration) {
        m_transact.panAndZoom(p,scale,duration);
    }
    
    /**
     * Indicates if a view transformation is currently underway.
     * @return true if a transform is in progress, false otherwise
     */
    public boolean isTranformInProgress() {
        return m_transact.isRunning();
    }
    
    /**
     * Activity for conducting animated view transformations.
     */
    private class TransformActivity extends Activity {
        // TODO: clean this up to be more general...
        // TODO: change mechanism so that multiple transform
        //        activities can be running at once?
         
        private double[] src, dst;
        private AffineTransform m_at;
        public TransformActivity() {
            super(2000,20,0);
            src = new double[6];
            dst = new double[6];
            m_at = new AffineTransform();
            setPacingFunction(new SlowInSlowOutPacer());
        }
        private AffineTransform getTransform() {
            if ( this.isScheduled() )
                m_at.setTransform(dst[0],dst[1],dst[2],dst[3],dst[4],dst[5]);
            else
                m_at.setTransform(m_transform);
            return m_at;
        }
        public void panAndZoom(final Point2D p, double scale, long duration) {
            AffineTransform at = getTransform();
            this.cancel();
            setDuration(duration);
            
            m_tmpPoint.setLocation(0,0);
            m_itransform.transform(m_tmpPoint,m_tmpPoint);
            double x = p.getX(); x = (Double.isNaN(x) ? 0 : x);
            double y = p.getY(); y = (Double.isNaN(y) ? 0 : y);
            double w = getWidth() /(2*m_transform.getScaleX());
            double h = getHeight()/(2*m_transform.getScaleY());
            double dx = w-x+m_tmpPoint.getX();
            double dy = h-y+m_tmpPoint.getY();
            at.translate(dx,dy);

            at.translate(p.getX(), p.getY());
            at.scale(scale,scale);
            at.translate(-p.getX(), -p.getY());
            
            at.getMatrix(dst);
            m_transform.getMatrix(src);
            this.run();
        }
        public void pan(double dx, double dy, long duration) {
            AffineTransform at = getTransform();
            this.cancel();
            setDuration(duration);
            at.translate(dx,dy);
            at.getMatrix(dst);
            m_transform.getMatrix(src);
            this.run();
        }
        public void zoom(final Point2D p, double scale, long duration) {
            AffineTransform at = getTransform();
            this.cancel();
            setDuration(duration);
            double zx = p.getX(), zy = p.getY();
            at.translate(zx, zy);
            at.scale(scale,scale);
            at.translate(-zx, -zy);
            at.getMatrix(dst);
            m_transform.getMatrix(src);
            this.run();
        }
        protected void run(long elapsedTime) {
            double f = getPace(elapsedTime);
            damageReport();
            m_transform.setTransform(
                src[0] + f*(dst[0]-src[0]),
                src[1] + f*(dst[1]-src[1]),
                src[2] + f*(dst[2]-src[2]),
                src[3] + f*(dst[3]-src[3]),
                src[4] + f*(dst[4]-src[4]),
                src[5] + f*(dst[5]-src[5])
            );
            try {
                m_itransform = m_transform.createInverse();
            } catch ( Exception e ) { /* won't happen */ }
            repaint();
        }
    } // end of inner class TransformActivity
    
    // ------------------------------------------------------------------------
    // Paint Listeners
    
    /**
     * Add a PaintListener to this Display to receive notifications
     * about paint events.
     * @param pl the {@link prefuse.util.display.PaintListener} to add
     */
    public void addPaintListener(PaintListener pl) {
        if ( m_painters == null )
            m_painters = new CopyOnWriteArrayList();
        m_painters.add(pl);
    }
    
    /**
     * Remove a PaintListener from this Display.
     * @param pl the {@link prefuse.util.display.PaintListener} to remove
     */
    public void removePaintListener(PaintListener pl) {
        m_painters.remove(pl);
    }
    
    /**
     * Fires a pre-paint notification to PaintListeners.
     * @param g the current graphics context
     */
    protected void firePrePaint(Graphics2D g) {
        if ( m_painters != null && m_painters.size() > 0 ) {
            Object[] lstnrs = m_painters.getArray();
            for ( int i=0; i<lstnrs.length; ++i ) {
                try {
                    ((PaintListener)lstnrs[i]).prePaint(this, g);
                } catch ( Exception e ) {
                    s_logger.warning(
                        "Exception thrown by PaintListener: " + e + "\n" +
                        StringLib.getStackTrace(e));
                }
            }
        }
    }
    
    /**
     * Fires a post-paint notification to PaintListeners.
     * @param g the current graphics context
     */
    protected void firePostPaint(Graphics2D g) {
        if ( m_painters != null && m_painters.size() > 0 ) {
            Object[] lstnrs = m_painters.getArray();
            for ( int i=0; i<lstnrs.length; ++i ) {
                try {
                    ((PaintListener)lstnrs[i]).postPaint(this, g);
                } catch ( Exception e ) {
                    s_logger.warning(
                        "Exception thrown by PaintListener: " + e + "\n" +
                        StringLib.getStackTrace(e));
                }
            }
        }
    }
    
    
    // ------------------------------------------------------------------------
    // Item Bounds Listeners
    
    /**
     * Add an ItemBoundsListener to receive notifications when the bounds
     * occupied by the VisualItems in this Display change.
     * @param ibl the {@link prefuse.util.display.ItemBoundsListener} to add
     */
    public void addItemBoundsListener(ItemBoundsListener ibl) {
        if ( m_bounders == null )
            m_bounders = new CopyOnWriteArrayList();
        m_bounders.add(ibl);
    }
    
    /**
     * Remove an ItemBoundsListener to receive notifications when the bounds
     * occupied by the VisualItems in this Display change.
     * @param ibl the {@link prefuse.util.display.ItemBoundsListener} to remove
     */
    public void removeItemBoundsListener(ItemBoundsListener ibl) {
        m_bounders.remove(ibl);
    }
    
    /**
     * Check if the item bounds has changed, and if so, fire a notification.
     * @param prev the previous item bounds of the Display
     */
    protected void checkItemBoundsChanged(Rectangle2D prev) {
        if ( m_bounds.equals(prev) )
            return; // nothing to do
        
        if ( m_bounders != null && m_bounders.size() > 0 ) {
            Object[] lstnrs = m_bounders.getArray();
            for ( int i=0; i<lstnrs.length; ++i ) {
                try {
                    ((ItemBoundsListener)lstnrs[i]).itemBoundsChanged(this);
                } catch ( Exception e ) {
                    s_logger.warning(
                        "Exception thrown by ItemBoundsListener: " + e + "\n" +
                        StringLib.getStackTrace(e));
                }
            }
        }
    }
    
    
    // ------------------------------------------------------------------------
    // Control Listeners
    
    /**
     * Adds a ControlListener to receive all input events on VisualItems.
     * @param cl the listener to add.
     */
    public void addControlListener(Control cl) {
        m_controls.add(cl);
    }

    /**
     * Removes a registered ControlListener.
     * @param cl the listener to remove.
     */
    public void removeControlListener(Control cl) {
        m_controls.remove(cl);
    }
    
    /**
     * Returns the VisualItem located at the given point.
     * @param p the Point at which to look
     * @return the VisualItem located at the given point, if any
     */
    public synchronized VisualItem findItem(Point p) {
        // transform mouse point from screen space to item space
        Point2D p2 = (m_itransform==null ? p : 
                      m_itransform.transform(p, m_tmpPoint));
        // ensure that the picking queue has been z-sorted
        if ( !m_queue.psorted )
            m_queue.sortPickingQueue();
        // walk queue from front to back looking for hits
        for ( int i = m_queue.psize; --i >= 0; ) {
            VisualItem vi = m_queue.pitems[i];
            if ( !vi.isValid() ) continue; // in case tuple went invalid
            Renderer r = vi.getRenderer();
            if (r!=null && vi.isInteractive() && r.locatePoint(p2, vi)) {
                return vi;
            }
        }
        return null;
    }
    
    /**
     * Captures all mouse and key events on the display, detects relevant 
     * VisualItems, and informs ControlListeners.
     */
    public class InputEventCapturer implements MouseMotionListener, 
        MouseWheelListener, MouseListener, KeyListener
    {
        private VisualItem activeItem = null;
        private boolean mouseDown = false;

        private boolean validityCheck() {
            if ( activeItem.isValid() )
                return true;
            activeItem = null;
            return false;
        }
        
        public void mouseDragged(MouseEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemDragged(activeItem, e);
                } else {
                    fireMouseDragged(e);
                }
            }
        }

        public void mouseMoved(MouseEvent e) {
            synchronized ( m_vis ) {
                boolean earlyReturn = false;
                //check if we've gone over any item
                VisualItem vi = findItem(e.getPoint());
                if ( activeItem != null && activeItem != vi ) {
                    if ( validityCheck() )
                        fireItemExited(activeItem, e);
                    earlyReturn = true;
                }
                if ( vi != null && vi != activeItem ) {
                    fireItemEntered(vi, e);
                    earlyReturn = true;
                }
                activeItem = vi;
                if ( earlyReturn ) return;
                
                if ( vi != null && vi == activeItem ) {
                    fireItemMoved(vi, e);
                }
                if ( vi == null ) {
                    fireMouseMoved(e);
                }
            }
        }

        public void mouseWheelMoved(MouseWheelEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemWheelMoved(activeItem, e);
                } else {
                    fireMouseWheelMoved(e);
                }
            }
        }

        public void mouseClicked(MouseEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemClicked(activeItem, e);
                } else {
                    fireMouseClicked(e);
                }
            }
        }
        

        public void mousePressed(MouseEvent e) {
            synchronized ( m_vis ) {
                mouseDown = true;
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemPressed(activeItem, e);
                } else {
                    fireMousePressed(e);
                }
            }
        }

        public void mouseReleased(MouseEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemReleased(activeItem, e);
                } else {
                    fireMouseReleased(e);
                }
                if ( activeItem != null && mouseDown && isOffComponent(e) ) {
                    // mouse was dragged off of the component, 
                    // then released, so register an exit
                    fireItemExited(activeItem, e);
                    activeItem = null;
                }
                mouseDown = false;
            }
        }

        public void mouseEntered(MouseEvent e) {
            synchronized ( m_vis ) {
                fireMouseEntered(e);
            }
        }

        public void mouseExited(MouseEvent e) {
            synchronized ( m_vis ) {      
                if ( !mouseDown && activeItem != null ) {
                    // we've left the component and an item 
                    // is active but not being dragged, deactivate it
                    fireItemExited(activeItem, e);
                    activeItem = null;
                }
                fireMouseExited(e);
            }
        }

        public void keyPressed(KeyEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemKeyPressed(activeItem, e);
                } else {
                    fireKeyPressed(e);
                }
            }
        }

        public void keyReleased(KeyEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemKeyReleased(activeItem, e);
                } else {
                    fireKeyReleased(e);
                }
            }
        }

        public void keyTyped(KeyEvent e) {
            synchronized ( m_vis ) {
                if ( activeItem != null ) {
                    if ( validityCheck() )
                        fireItemKeyTyped(activeItem, e);
                } else {
                    fireKeyTyped(e);
                }
            }
        }
        
        private boolean isOffComponent(MouseEvent e) {
            int x = e.getX(), y = e.getY();
            return ( x<0 || x>getWidth() || y<0 || y>getHeight() );
        }
        
        // --------------------------------------------------------------------
        // Fire Event Notifications
        
        private void fireItemDragged(VisualItem item, MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemDragged(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemMoved(VisualItem item, MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemMoved(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemWheelMoved(VisualItem item, MouseWheelEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemWheelMoved(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemClicked(VisualItem item, MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemClicked(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemPressed(VisualItem item, MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemPressed(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemReleased(VisualItem item, MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemReleased(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemEntered(VisualItem item, MouseEvent e) {
            item.setHover(true);
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemEntered(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemExited(VisualItem item, MouseEvent e) {
            if ( item.isValid() ) item.setHover(false);
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemExited(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemKeyPressed(VisualItem item, KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            if (lstnrs.length == 0)
                return;
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemKeyPressed(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemKeyReleased(VisualItem item, KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemKeyReleased(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireItemKeyTyped(VisualItem item, KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.itemKeyTyped(item, e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseEntered(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseEntered(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseExited(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseExited(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMousePressed(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mousePressed(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseReleased(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseReleased(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseClicked(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseClicked(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseDragged(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseDragged(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseMoved(MouseEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseMoved(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireMouseWheelMoved(MouseWheelEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.mouseWheelMoved(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireKeyPressed(KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.keyPressed(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireKeyReleased(KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.keyReleased(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }

        private void fireKeyTyped(KeyEvent e) {
            Object[] lstnrs = m_controls.getArray();
            for (int i = 0; i < lstnrs.length; ++i) {
                Control ctrl = (Control) lstnrs[i];
                if (ctrl.isEnabled())
                    try {
                        ctrl.keyTyped(e);
                    } catch ( Exception ex ) {
                        s_logger.warning(
                            "Exception thrown by Control: " + ex + "\n" +
                            StringLib.getStackTrace(ex));
                    }
            }
        }
        
    } // end of inner class MouseEventCapturer
    
    
    // ------------------------------------------------------------------------
    // Text Editing
    
    /**
     * Returns the TextComponent used for on-screen text editing.
     * @return the TextComponent used for text editing
     */
    public JTextComponent getTextEditor() {
        return m_editor;
    }
    
    /**
     * Sets the TextComponent used for on-screen text editing.
     * @param tc the TextComponent to use for text editing
     */
    public void setTextEditor(JTextComponent tc) {
        this.remove(m_editor);
        m_editor = tc;
        this.add(m_editor, 1);
    }
    
    /**
     * Edit text for the given VisualItem and attribute. Presents a text
     * editing widget spaning the item's bounding box. Use stopEditing()
     * to hide the text widget. When stopEditing() is called, the data field
     * will automatically be updated with the VisualItem.
     * @param item the VisualItem to edit
     * @param attribute the attribute to edit
     */
    public void editText(VisualItem item, String attribute) {
        if ( m_editing ) { stopEditing(); }
        Rectangle2D b = item.getBounds();
        Rectangle r = m_transform.createTransformedShape(b).getBounds();
        
        // hacky placement code that attempts to keep text in same place
        // configured under Windows XP and Java 1.4.2b
        if ( m_editor instanceof JTextArea ) {
            r.y -= 2; r.width += 22; r.height += 2;
        } else {
            r.x += 3; r.y += 1; r.width -= 5; r.height -= 2;
        }
        
        Font f = getFont();
        int size = (int)Math.round(f.getSize()*m_transform.getScaleX());
        Font nf = new Font(f.getFontName(), f.getStyle(), size);
        m_editor.setFont(nf);
        
        editText(item, attribute, r);
    }
    
    /**
     * Edit text for the given VisualItem and field. Presents a text
     * editing widget spaning the given bounding box. Use stopEditing()
     * to hide the text widget. When stopEditing() is called, the field
     * will automatically be updated with the VisualItem.
     * @param item the VisualItem to edit
     * @param attribute the attribute to edit
     * @param r Rectangle representing the desired bounding box of the text
     *  editing widget
     */
    public void editText(VisualItem item, String attribute, Rectangle r) {
        if ( m_editing ) { stopEditing(); }
        String txt = item.getString(attribute);
        m_editItem = item;
        m_editAttribute = attribute;
        Color tc = ColorLib.getColor(item.getTextColor());
        Color fc = ColorLib.getColor(item.getFillColor());
        m_editor.setForeground(tc);
        m_editor.setBackground(fc);
        editText(txt, r);
    }
    
    /**
     * Show a text editing widget containing the given text and spanning the
     * specified bounding box. Use stopEditing() to hide the text widget. Use
     * the method calls getTextEditor().getText() to get the resulting edited
     * text.
     * @param txt the text string to display in the text widget
     * @param r Rectangle representing the desired bounding box of the text
     *  editing widget
     */
    public void editText(String txt, Rectangle r) {
        if ( m_editing ) { stopEditing(); }
        m_editing = true;
        m_editor.setBounds(r.x,r.y,r.width,r.height);
        m_editor.setText(txt);
        m_editor.setVisible(true);
        m_editor.setCaretPosition(txt.length());
        m_editor.requestFocus();
    }
    
    /**
     * Stops text editing on the display, hiding the text editing widget. If
     * the text editor was associated with a specific VisualItem (ie one of the
     * editText() methods which include a VisualItem as an argument was called),
     * the item is updated with the edited text.
     */
    public void stopEditing() {
        m_editor.setVisible(false);
        if ( m_editItem != null ) {
            String txt = m_editor.getText();
            m_editItem.set(m_editAttribute, txt);
            m_editItem = null;
            m_editAttribute = null;
            m_editor.setBackground(null);
            m_editor.setForeground(null);
        }
        m_editing = false;
    }
    
} // end of class Display
