# StackedGraphDisplay.java

    /*  Copyright 2009 Tonny Kohar.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     * http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
     * implied.
     *
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    package org.qi4j.envisage.graph;

    import java.awt.Component;
    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.Shape;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseWheelEvent;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.NoninvertibleTransformException;
    import java.awt.geom.Rectangle2D;
    import java.util.Iterator;
    import javax.swing.JViewport;
    import javax.swing.SwingUtilities;
    import org.qi4j.envisage.event.LinkEvent;
    import prefuse.Constants;
    import prefuse.Visualization;
    import prefuse.action.Action;
    import prefuse.action.ActionList;
    import prefuse.action.ItemAction;
    import prefuse.action.RepaintAction;
    import prefuse.action.assignment.ColorAction;
    import prefuse.action.layout.Layout;
    import prefuse.activity.Activity;
    import prefuse.controls.ControlAdapter;
    import prefuse.controls.FocusControl;
    import prefuse.controls.PanControl;
    import prefuse.data.Graph;
    import prefuse.data.Schema;
    import prefuse.data.expression.Predicate;
    import prefuse.data.expression.parser.ExpressionParser;
    import prefuse.data.tuple.TupleSet;
    import prefuse.render.AbstractShapeRenderer;
    import prefuse.render.DefaultRendererFactory;
    import prefuse.render.EdgeRenderer;
    import prefuse.render.LabelRenderer;
    import prefuse.util.ColorLib;
    import prefuse.util.ColorMap;
    import prefuse.util.FontLib;
    import prefuse.util.PrefuseLib;
    import prefuse.visual.DecoratorItem;
    import prefuse.visual.EdgeItem;
    import prefuse.visual.NodeItem;
    import prefuse.visual.VisualItem;
    import prefuse.visual.expression.InGroupPredicate;
    import prefuse.visual.sort.TreeDepthItemSorter;

    /* package */ class StackedGraphDisplay
        extends GraphDisplay
    {
        /* package */ static final Font FONT = FontLib.getFont( "Tahoma", 12 );

        // create data description of LABELS, setting colors, fonts ahead of time
        private static final Schema LABEL_SCHEMA = PrefuseLib.getVisualItemSchema();

        static
        {
            LABEL_SCHEMA.setDefault( VisualItem.INTERACTIVE, false );
            LABEL_SCHEMA.setDefault( VisualItem.TEXTCOLOR, ColorLib.rgb( 255, 255, 255 ) );
            LABEL_SCHEMA.setDefault( VisualItem.FONT, FONT );
        }

        private static final String LABELS = "labels";

        private static final String LAYOUT_ACTION = "layout";
        private static final String COLORS_ACTION = "colors";
        private static final String AUTO_PAN_ACTION = "autoPan";

        private static int OUTLINE_COLOR = ColorLib.rgb( 33, 115, 170 );
        private static int OUTLINE_FOCUS_COLOR = ColorLib.rgb( 255, 255, 255 );  // alternative color ColorLib.rgb(150,200,200);

        private StackedLayout stackedLayout;

        private Activity activity;

        /* package */ StackedGraphDisplay()
        {
            super( new Visualization() );

            setBackground( ColorLib.getColor( 0, 51, 88 ) );

            LabelRenderer labelRenderer = new LabelRenderer( NAME_LABEL );
            labelRenderer.setVerticalAlignment( Constants.BOTTOM );
            labelRenderer.setHorizontalAlignment( Constants.LEFT );

            EdgeRenderer usesRenderer = new EdgeRenderer( Constants.EDGE_TYPE_CURVE, Constants.EDGE_ARROW_FORWARD );
            usesRenderer.setHorizontalAlignment1( Constants.CENTER );
            usesRenderer.setHorizontalAlignment2( Constants.CENTER );
            usesRenderer.setVerticalAlignment1( Constants.BOTTOM );
            usesRenderer.setVerticalAlignment2( Constants.TOP );

            Predicate usesPredicate = (Predicate) ExpressionParser.parse( "ingroup('graph.edges') AND [" + USES_EDGES + "]==true", true );

            // set up the renderers - one for nodes and one for LABELS
            DefaultRendererFactory rf = new DefaultRendererFactory();
            rf.add( new InGroupPredicate( GRAPH_NODES ), new NodeRenderer() );
            rf.add( new InGroupPredicate( LABELS ), labelRenderer );
            rf.add( usesPredicate, usesRenderer );
            m_vis.setRendererFactory( rf );

            // border colors
            ColorAction borderColor = new BorderColorAction( GRAPH_NODES );
            ColorAction fillColor = new FillColorAction( GRAPH_NODES );

            // uses edge colors
            ItemAction usesColor = new ColorAction( GRAPH_EDGES, usesPredicate, VisualItem.STROKECOLOR, ColorLib.rgb( 50, 50, 50 ) );
            ItemAction usesArrow = new ColorAction( GRAPH_EDGES, usesPredicate, VisualItem.FILLCOLOR, ColorLib.rgb( 50, 50, 50 ) );

            // color settings
            ActionList colors = new ActionList();
            colors.add( fillColor );
            colors.add( borderColor );
            colors.add( usesColor );
            colors.add( usesArrow );
            m_vis.putAction( COLORS_ACTION, colors );

            ActionList autoPan = new ActionList();
            autoPan.add( colors );
            autoPan.add( new AutoPanAction() );
            autoPan.add( new RepaintAction() );
            m_vis.putAction( AUTO_PAN_ACTION, autoPan );

            // create the layout action list
            stackedLayout = new StackedLayout( GRAPH );
            ActionList layout = new ActionList();
            layout.add( stackedLayout );
            layout.add( new LabelLayout( LABELS ) );
            layout.add( autoPan );
            m_vis.putAction( LAYOUT_ACTION, layout );

            // initialize our display
            Dimension size = new Dimension( 400, 400 );
            setSize( size );
            setPreferredSize( size );
            setItemSorter( new ExtendedTreeDepthItemSorter( true ) );
            addControlListener( new HoverControl() );
            addControlListener( new FocusControl( 1, COLORS_ACTION ) );
            addControlListener( new WheelMouseControl() );
            addControlListener( new PanControl( true ) );
            addControlListener( new ItemSelectionControl() );

            setDamageRedraw( true );
        }

        @Override
        public void run( Graph graph )
        {
            // add the GRAPH to the visualization
            m_vis.add( GRAPH, graph );

            // hide edges
            Predicate edgesPredicate = (Predicate) ExpressionParser.parse( "ingroup('graph.edges') AND [" + USES_EDGES + "]==false", true );
            m_vis.setVisible( GRAPH_EDGES, edgesPredicate, false );

            m_vis.setInteractive( GRAPH_EDGES, null, false );

            // make node interactive
            m_vis.setInteractive( GRAPH_NODES, null, true );

            // add LABELS to the visualization
            Predicate labelP = (Predicate) ExpressionParser.parse( "VISIBLE()" );
            m_vis.addDecorators( LABELS, GRAPH_NODES, labelP, LABEL_SCHEMA );

            run();
        }

        @Override
        public void run()
        {
            if( isInProgress() )
            {
                return;
            }

            // perform layout
            m_vis.invalidate( GRAPH_NODES );
            activity = m_vis.run( LAYOUT_ACTION );
        }

        @Override
        public void setSelectedValue( Object object )
        {
            if( object == null )
            {
                return;
            }

            NodeItem item = null;

            Iterator iter = m_vis.items( GRAPH_NODES );
            while( iter.hasNext() )
            {
                NodeItem tItem = (NodeItem) iter.next();
                Object tObj = tItem.get( USER_OBJECT );
                if( tObj.equals( object ) )
                {
                    item = tItem;
                    break;
                }
            }

            if( item != null )
            {
                int depth = item.getDepth();
                boolean relayout = false;
                if( depth > stackedLayout.getZoom() )
                {
                    stackedLayout.zoom( depth );
                    relayout = true;
                }

                TupleSet ts = m_vis.getFocusGroup( Visualization.FOCUS_ITEMS );
                ts.setTuple( item );
                if( relayout )
                {
                    run();
                }
                else
                {
                    m_vis.run( AUTO_PAN_ACTION );
                }
            }
        }

        private void zoomIn()
        {
            if( isInProgress() )
            {
                return;
            }

            stackedLayout.zoomIn();
            run();
        }

        private void zoomOut()
        {
            if( isInProgress() )
            {
                return;
            }

            stackedLayout.zoomOut();
            run();
        }

        private boolean isInProgress()
        {
            if( isTranformInProgress() )
            {
                return true;
            }

            if( activity != null )
            {
                if( activity.isRunning() )
                {
                    return true;
                }
            }

            return false;
        }

        // ------------------------------------------------------------------------
        /**
         * Set the stroke color for drawing border node outlines.
         */
        private static class BorderColorAction
            extends ColorAction
        {

            private BorderColorAction( String group )
            {
                super( group, VisualItem.STROKECOLOR );
            }

            @Override
            public int getColor( VisualItem item )
            {
                if( !( item instanceof NodeItem ) )
                {
                    return 0;
                }
                NodeItem nItem = (NodeItem) item;
                if( m_vis.isInGroup( nItem, Visualization.FOCUS_ITEMS ) )
                {
                    return OUTLINE_FOCUS_COLOR;
                }

                return OUTLINE_COLOR;
            }
        }

        /**
         * Set fill colors for treemap nodes. Normal nodes are shaded according to their
         * depth in the tree.
         */
        private static class FillColorAction
            extends ColorAction
        {
            private static final ColorMap CMAP = new ColorMap( new int[]
            {
                ColorLib.rgb( 11, 117, 188 ),
                ColorLib.rgb( 8, 99, 160 ),
                ColorLib.rgb( 5, 77, 126 ),
                ColorLib.rgb( 2, 61, 100 ),
                ColorLib.rgb( 148, 55, 87 )
            }, 0, 4 );

            private FillColorAction( String group )
            {
                super( group, VisualItem.FILLCOLOR );
            }

            @Override
            public int getColor( VisualItem item )
            {
                if( item instanceof NodeItem )
                {
                    NodeItem nItem = (NodeItem) item;
                    if( m_vis.isInGroup( nItem, Visualization.FOCUS_ITEMS ) )
                    {
                        int c = CMAP.getColor( nItem.getDepth() );
                        return ColorLib.darker( c );
                    }
                    return CMAP.getColor( nItem.getDepth() );
                }
                else
                {
                    return CMAP.getColor( 0 );
                }
            }
        } // end of inner class FillColorAction

        private static class HoverControl
            extends ControlAdapter
        {
            @Override
            public void itemEntered( VisualItem item, MouseEvent evt )
            {
                item.setStrokeColor( OUTLINE_FOCUS_COLOR );
                item.getVisualization().repaint();
            }

            @Override
            public void itemExited( VisualItem item, MouseEvent evt )
            {
                item.setStrokeColor( item.getEndStrokeColor() );
                item.getVisualization().repaint();
            }
        }

        /**
         * Set label positions. Labels are assumed to be DecoratorItem instances,
         * decorating their respective nodes. The layout simply gets the bounds
         * of the decorated node and assigns the label coordinates to the center
         * of those bounds.
         */
        private static class LabelLayout
            extends Layout
        {
            private LabelLayout( String group )
            {
                super( group );
            }

            @Override
            public void run( double frac )
            {
                Iterator iter = m_vis.items( m_group );
                while( iter.hasNext() )
                {
                    DecoratorItem item = (DecoratorItem) iter.next();
                    VisualItem node = item.getDecoratedItem();
                    Rectangle2D bounds = node.getBounds();
                    setX( item, node, bounds.getX() + StackedLayout.INSET );
                    setY( item, node, bounds.getY() + StackedLayout.INSET + 12 );
                }
            }
        } // end of inner class LabelLayout

        /**
         * A renderer for treemap nodes. Draws simple rectangles, but defers
         * the bounds management to the layout.
         */
        private static class NodeRenderer
            extends AbstractShapeRenderer
        {
            private Rectangle2D m_bounds = new Rectangle2D.Double();

            private NodeRenderer()
            {
                m_manageBounds = false;
            }

            @Override
            protected Shape getRawShape( VisualItem item )
            {
                m_bounds.setRect( item.getBounds() );
                return m_bounds;
            }
        } // end of inner class NodeRenderer

        private class WheelMouseControl
            extends ControlAdapter
        {
            @Override
            public void itemWheelMoved( VisualItem item, MouseWheelEvent evt )
            {
                zoom( evt.getWheelRotation() );
            }

            @Override
            public void mouseWheelMoved( MouseWheelEvent evt )
            {
                zoom( evt.getWheelRotation() );
            }

            private void zoom( final int rotation )
            {
                SwingUtilities.invokeLater( new Runnable()
                {
                    @Override
                    public void run()
                    {
                        if( rotation == 0 )
                        {
                            return;
                        }
                        if( rotation < 0 )
                        {
                            zoomOut();
                        }
                        else
                        {
                            zoomIn();
                        }
                    }
                } );
            }
        }

        private class ItemSelectionControl
            extends ControlAdapter
        {
            @Override
            public final void itemClicked( VisualItem anItem, MouseEvent anEvent )
            {
                // update the display
                anItem.getVisualization().repaint();

                if( !anItem.canGet( USER_OBJECT, Object.class ) )
                {
                    return;
                }

                Object object = anItem.get( USER_OBJECT );
                LinkEvent evt = new LinkEvent( StackedGraphDisplay.this, object );
                fireLinkActivated( evt );
            }
        }

        private class AutoPanAction
            extends Action
        {
            @Override
            public void run( double frac )
            {
                Rectangle2D displayBounds = new Rectangle2D.Double( 0, 0, getWidth(), getHeight() );

                Container container = getParent();
                if( container == null )
                {
                    return;
                }

                // HACK check the container size
                if( container instanceof JViewport )
                {
                    Dimension size = ( (JViewport) container ).getExtentSize();
                    displayBounds.setRect( 0, 0, size.getWidth(), size.getHeight() );
                }
                else
                {
                    Dimension size = ( (Component) container ).getSize();
                    displayBounds.setRect( 0, 0, size.getWidth(), size.getHeight() );
                }

                Rectangle2D bounds = stackedLayout.getLayoutRoot().getBounds();

                // Pan center
                double x = ( displayBounds.getWidth() - bounds.getWidth() ) / 2;
                double y = ( displayBounds.getHeight() - bounds.getHeight() ) / 2;

                // reset the transform
                try
                {
                    setTransform( new AffineTransform() );
                }
                catch( NoninvertibleTransformException ex )
                {
                    return;
                }
                if( x < 0 )
                {
                    x = 0;
                }

                if( y < 0 )
                {
                    y = 0;
                }
                pan( x, y );

                TupleSet ts = m_vis.getFocusGroup( Visualization.FOCUS_ITEMS );
                if( ts.getTupleCount() != 0 )
                {
                    // get the first selected item and pan center it
                    VisualItem vi = (VisualItem) ts.tuples().next();

                    //update scrollbar position
                    if( container instanceof JViewport )
                    {
                        // TODO there is a bug on Swing scrollRectToVisible
                        ( (JViewport) container ).scrollRectToVisible( vi.getBounds().getBounds() );
                    }
                }
            }
        }

        /**
         * ExtenedTreeDepthItemSorter to alter the default ordering/sorter,
         * to make sure Edge item is drawn in front. This is for Edge Uses
         */
        private static class ExtendedTreeDepthItemSorter
            extends TreeDepthItemSorter
        {
            private ExtendedTreeDepthItemSorter()
            {
                this( false );
            }

            private ExtendedTreeDepthItemSorter( boolean childrenAbove )
            {
                super( childrenAbove );
            }

            @Override
            public int score( VisualItem item )
            {
                int score = super.score( item );
                if( item instanceof EdgeItem )
                {
                    // make it drawn in front of NODE
                    score = ( 1 << ( 25 + EDGE + NODE ) );
                }

                return score;
            }
        }

    }