/* 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.*; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.Iterator; import javax.swing.*; 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; public class StackedGraphDisplay extends GraphDisplay { public static final Font FONT = FontLib.getFont( "Tahoma", 12 ); // create data description of LABELS, setting colors, fonts ahead of time 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 ); } static final String LABELS = "labels"; static final String LAYOUT_ACTION = "layout"; static final String COLORS_ACTION = "colors"; static final String AUTO_PAN_ACTION = "autoPan"; static int OUTLINE_COLOR = ColorLib.rgb( 33, 115, 170 ); static int OUTLINE_FOCUS_COLOR = ColorLib.rgb( 255, 255, 255 ); // alternative color ColorLib.rgb(150,200,200); protected StackedLayout stackedLayout; protected Activity activity; public 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 ); } } } public void zoomIn() { if( isInProgress() ) { return; } stackedLayout.zoomIn(); run(); } public void zoomOut() { if( isInProgress() ) { return; } stackedLayout.zoomOut(); run(); } protected 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. */ public class BorderColorAction extends ColorAction { public 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. */ public class FillColorAction extends ColorAction { private 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 ); public 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 public 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. */ public class LabelLayout extends Layout { public 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. */ public class NodeRenderer extends AbstractShapeRenderer { private Rectangle2D m_bounds = new Rectangle2D.Double(); public NodeRenderer() { m_manageBounds = false; } @Override protected Shape getRawShape( VisualItem item ) { m_bounds.setRect( item.getBounds() ); return m_bounds; } } // end of inner class NodeRenderer public 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(); } } } ); } } public 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 ); } } public 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( Exception 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 */ public class ExtendedTreeDepthItemSorter extends TreeDepthItemSorter { public ExtendedTreeDepthItemSorter() { this( false ); } public 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; } } }