1   /****************************************************************************
2    **
3    ** This file is part of yFiles-2.9. 
4    ** 
5    ** yWorks proprietary/confidential. Use is subject to license terms.
6    **
7    ** Redistribution of this file or of an unauthorized byte-code version
8    ** of this file is strictly forbidden.
9    **
10   ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28, 
11   ** 72070 Tuebingen, Germany. All rights reserved.
12   **
13   ***************************************************************************/
14  package demo.view.orgchart;
15  
16  import java.awt.Color;
17  import java.awt.Component;
18  import java.awt.Dimension;
19  import java.awt.GradientPaint;
20  import java.awt.Graphics;
21  import java.awt.Graphics2D;
22  import java.awt.Insets;
23  import java.awt.Rectangle;
24  import java.awt.RenderingHints;
25  import java.awt.Shape;
26  import java.awt.RenderingHints.Key;
27  import java.awt.event.MouseEvent;
28  import java.awt.event.MouseWheelListener;
29  import java.awt.geom.AffineTransform;
30  import java.awt.geom.Ellipse2D;
31  import java.awt.geom.GeneralPath;
32  import java.awt.geom.Rectangle2D;
33  import java.beans.PropertyChangeEvent;
34  import java.beans.PropertyChangeListener;
35  import java.util.Map;
36  
37  import javax.swing.Icon;
38  
39  import demo.view.orgchart.OrgChartTreeModel.Employee;
40  
41  import y.anim.AnimationFactory;
42  import y.anim.AnimationObject;
43  import y.anim.AnimationPlayer;
44  import y.base.DataProvider;
45  import y.base.Edge;
46  import y.base.Node;
47  import y.base.NodeMap;
48  import y.geom.OrientedRectangle;
49  import y.geom.YInsets;
50  import y.geom.YPoint;
51  import y.geom.YVector;
52  import y.layout.NormalizingGraphElementOrderStage;
53  import y.layout.Layouter;
54  import y.util.D;
55  import y.util.DataProviderAdapter;
56  import y.view.EdgeRealizer;
57  import y.view.GenericNodeRealizer;
58  import y.view.Graph2D;
59  import y.view.Graph2DSelectionEvent;
60  import y.view.Graph2DSelectionListener;
61  import y.view.Graph2DView;
62  import y.view.Graph2DViewMouseWheelZoomListener;
63  import y.view.HitInfo;
64  import y.view.LineType;
65  import y.view.MagnifierViewMode;
66  import y.view.NodeLabel;
67  import y.view.NodeRealizer;
68  import y.view.Overview;
69  import y.view.PolyLineEdgeRealizer;
70  import y.view.Scroller;
71  import y.view.ShapeNodePainter;
72  import y.view.ShapeNodeRealizer;
73  import y.view.SmartNodeLabelModel;
74  import y.view.TooltipMode;
75  import y.view.ViewAnimationFactory;
76  import y.view.ViewCoordDrawableAdapter;
77  import y.view.ViewMode;
78  import y.view.YLabel;
79  import y.view.GenericNodeRealizer.Painter;
80  import y.view.YRenderingHints;
81  import y.view.hierarchy.DefaultHierarchyGraphFactory;
82  import y.view.hierarchy.GroupNodeRealizer;
83  
84  /** 
85   * This class builds upon the more generic tree chart component {@link demo.view.orgchart.JTreeChart}. 
86   * It visualizes a {@link demo.view.orgchart.OrgChartTreeModel}. 
87   * Also it customizes the look and feel of the component to make it suitable for
88   * organization charts. 
89   */
90  public class JOrgChart extends JTreeChart {
91  
92    /**
93     * Defines the colors being used for the graph elements. There are four different states a node can be in: unselected, selected, highlighted, highlighted and selected.
94     * The colors below define the colors used for each state.
95     */
96    static final Color FILL_COLOR = new Color( 0xCCFFFF );
97    static final Color FILL_COLOR2 = new Color( 0x249AE7 );  
98    static final Color LINE_COLOR = new Color( 0x249AE7 );
99    
100   static final Color SELECTED_FILL_COLOR = Color.WHITE; 
101   static final Color SELECTED_LINE_COLOR = Color.ORANGE;
102   static final Color SELECTED_FILL_COLOR2 = Color.ORANGE;
103   
104   static final Color SELECTED_HOVER_FILL_COLOR = SELECTED_FILL_COLOR;
105   static final Color SELECTED_HOVER_LINE_COLOR = SELECTED_LINE_COLOR;
106   static final Color SELECTED_HOVER_FILL_COLOR2 = new Color( 0xFFEE55 ); 
107   
108   static final Color HOVER_FILL_COLOR2 = new Color( 0x63CCEE );
109   static final Color HOVER_FILL_COLOR = FILL_COLOR;
110   static final Color HOVER_LINE_COLOR = LINE_COLOR;
111 
112   
113   static final Color EDGE_COLOR = new Color( 0x999999 );  
114   static final Color GROUP_FILL_COLOR = new Color(231,219,182,100);  
115   
116   /**
117    * NodeMap used to maintain the highlighted state of a node.
118    */
119   private NodeMap highlightMap;  
120 
121   public JOrgChart(OrgChartTreeModel model) {
122     super(model, createUserObjectDP(), createGroupIdDataProvider());
123   
124     highlightMap = getGraph2D().createNodeMap();
125 
126     getGraph2D().addGraph2DSelectionListener(new TreeChartSelectionListener());
127       
128     setPaintDetailThreshold(0);
129     setAntialiasedPainting(true);    
130 
131 //TURN ON FRACTIONAL FONT METRICS for precise font measurement
132 //    getRenderingHints().put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
133 //    YLabel.setFractionMetricsForSizeCalculationEnabled(true);
134 
135     /**
136      * Listener that adjusts the LoD (level of detail) of the diagram whenever the
137      * zoom level of the view changes. 
138      */
139     getCanvasComponent().addPropertyChangeListener(new PropertyChangeListener() {
140       public void propertyChange(PropertyChangeEvent evt) {
141         if("Zoom".equals(evt.getPropertyName())) {
142           double zoom = getZoom();
143           Object lodValue = null;
144           if(zoom >= 0.8) {
145             lodValue = LODRenderingHint.VALUE_LOD_HIGH;
146           }
147           else if(zoom < 0.8 && zoom >= 0.3) {
148             lodValue = LODRenderingHint.VALUE_LOD_MEDIUM;
149           }
150           else if(zoom < 0.3) {
151             lodValue = LODRenderingHint.VALUE_LOD_LOW;
152           }
153           getRenderingHints().put(LODRenderingHint.KEY_LOD, lodValue);
154         }          
155       }      
156     });           
157     
158     updateChart();
159   }
160   
161   /**
162    * RenderingHint that conveys information about the desired level of detail when rendering graph elements.
163    */
164   static class LODRenderingHint {    
165     public static final Object VALUE_LOD_LOW = "LODRenderingHint#VALUE_LOD_LOW";
166     public static final Object VALUE_LOD_MEDIUM = "LODRenderingHint#VALUE_LOD_MEDIUM";
167     public static final Object VALUE_LOD_HIGH = "LODRenderingHint#VALUE_LOD_HIGH";
168     public static final Object VALUE_LOD_OVERVIEW = "LODRenderingHint#VALUE_LOD_OVERVIEW";
169     
170     public static final Key KEY_LOD = new RenderingHints.Key(0) {
171       public boolean isCompatibleValue(Object val) {
172         return true; //allow all kinds of values 
173       }    
174     };    
175   }
176 
177   /**
178    * DataProvider that returns the userObject (--> Employee) for a tree model item.
179    */
180   static DataProvider createUserObjectDP() {
181     return new DataProviderAdapter() {
182       public Object get(Object dataHolder) {
183         return dataHolder;
184       }
185     };
186   }
187   
188   /**
189    * DataProvider that returns the group Id for a tree model user object.
190    * Group Ids are used to convey grouping information to the JTreeChart component.
191    */
192   static DataProvider createGroupIdDataProvider() {
193     return new DataProviderAdapter() {
194       public Object get(Object dataHolder) {
195         return ((Employee)dataHolder).businessUnit;
196       }
197     };    
198   }
199       
200   /**
201    * Adds mouse interaction to the component. We inherit all that JTreeChart has, plus we add tool tip display,
202    * a nifty local view magnifier, and a roll-over effect for nodes.    
203    */
204   protected void addMouseInteraction() {
205     super.addMouseInteraction();
206     TooltipMode tooltipMode = new TooltipMode() {
207       /**
208        * Overwrites {@link TooltipMode#getNodeTip(y.base.Node)} to set a tooltip text with some information about an
209        * employee if the level of detail is set to {@link LODRenderingHint#VALUE_LOD_LOW} or {@link
210        * LODRenderingHint#VALUE_LOD_MEDIUM}.
211        *
212        * @param node the node for which the tooltip is set.
213        * @return a tooltip text if the level of detail is low or medium, <code>null</code> otherwise.
214        */
215       protected String getNodeTip(Node node) {
216         String tipText = null;
217         Object lod = view.getRenderingHints().get(LODRenderingHint.KEY_LOD);
218         if (lod == LODRenderingHint.VALUE_LOD_LOW || lod == LODRenderingHint.VALUE_LOD_MEDIUM) { //tiny
219           if (getGraph2D().getHierarchyManager().isNormalNode(node)) {
220             Employee e = (Employee) ((JOrgChart) view).getUserObject(node);
221             tipText = "<html><b>" + e.name + "</b><br>" + e.position + "<br>Status " + e.status + "</html>";
222           }
223         }
224         return tipText;
225       }
226 
227       /**
228        * Overwrites {@link TooltipMode#getNodeLabelTip(y.view.NodeLabel)} to set a tooltip text with the employee's
229        * presence status if the level of detail is set to {@link LODRenderingHint#VALUE_LOD_HIGH} and the given label
230        * equals the status label in the upper right corner of the node.
231        * @param label the label for which the tooltip is set.
232        * @return a tooltip text for the status label if the level of detail is high, <code>null</code> otherwise.
233        */
234       protected String getNodeLabelTip(NodeLabel label) {
235         String tipText = null;
236         Object lod = view.getRenderingHints().get(LODRenderingHint.KEY_LOD);
237         if (lod == LODRenderingHint.VALUE_LOD_HIGH) {
238           NodeRealizer realizer = label.getRealizer();
239           if (realizer.labelCount() > 6 && label == realizer.getLabel(6)) {
240             Node node = label.getNode();
241             if (getGraph2D().getHierarchyManager().isNormalNode(node)) {
242               Employee e = (Employee) ((JOrgChart) view).getUserObject(node);
243               tipText = "Status " + e.status;
244             }
245           }
246         }
247         return tipText;
248       }
249     };
250     tooltipMode.setEdgeTipEnabled(false);
251     tooltipMode.setNodeLabelTipEnabled(true);
252     addViewMode(tooltipMode);
253     addViewMode(new MiddleClickMagnifierViewMode());
254     addViewMode(new RollOverViewMode());
255   }
256 
257   /**
258    * Returns a JTreeChartViewMode that will use a customized Scroller Drawable.   
259    */
260   protected JTreeChartViewMode createTreeChartViewMode() {
261     JTreeChartViewMode viewMode = super.createTreeChartViewMode();
262     Scroller scroller = viewMode.getScroller();
263     scroller.setScrollSpeedFactor(2.0);
264     scroller.setDrawable(new ScrollerDrawable(this,scroller));
265     return viewMode;
266   }
267   
268   /**
269    * Customize and return the MouseWheelListener to be used. 
270    */
271   protected MouseWheelListener createMouseWheelListener() {
272     Graph2DViewMouseWheelZoomListener mwl = new Graph2DViewMouseWheelZoomListener();
273     mwl.setZoomIndicatorShowing(true);
274     mwl.setCenterZooming(false);
275     mwl.setMaximumZoom(4);
276     mwl.setLimitMinimumZoomByContentSize(true);
277     mwl.setZoomIndicatorColor(new Color(170, 160,125));
278     return mwl;
279   }
280     
281   /**
282    * Set NodeRealizer and EdgeRealizer defaults. We use a mixed style here. Employees will be visualized using 
283    * GenericNodeRealizer with a custom Painter implementation. Edges are represented using
284    * a PolyLineEdgeRealizer, while group nodes get displayed by a customized GroupNodeRealizer.
285    */
286   protected void setRealizerDefaults() {
287     PolyLineEdgeRealizer er = new PolyLineEdgeRealizer();
288     er.setSmoothedBends(true);
289     er.setLineColor(EDGE_COLOR);
290     er.setLineType(LineType.LINE_2);
291 
292     //register default node realizer
293     getGraph2D().setDefaultEdgeRealizer(er);
294     
295     GenericNodeRealizer.Factory factory = GenericNodeRealizer.getFactory();
296     
297     Painter painter = new EmployeePainter();
298     //uncomment this for a nice (but rather expensive) drop shadow effect 
299     //painter = new ShadowNodePainter(painter); 
300     Map implementationsMap = factory.createDefaultConfigurationMap();        
301     implementationsMap.put(GenericNodeRealizer.Painter.class, painter);
302             
303     factory.addConfiguration("employee", implementationsMap);      
304     GenericNodeRealizer nr = new GenericNodeRealizer();
305     nr.setFillColor(FILL_COLOR);
306     nr.setFillColor2(FILL_COLOR2);
307     nr.setLineColor(LINE_COLOR);
308     nr.setLineType(LineType.LINE_2);
309     nr.setSize(220,110);
310 
311     nr.setConfiguration("employee");
312     
313     //register default node realizer
314     getGraph2D().setDefaultNodeRealizer(nr);
315     
316     DefaultHierarchyGraphFactory gf = (DefaultHierarchyGraphFactory) getGraph2D().getHierarchyManager().getGraphFactory();
317     CustomGroupNodeRealizer gnr = new CustomGroupNodeRealizer();
318     gnr.setConsiderNodeLabelSize(true);
319     gnr.setShapeType(ShapeNodeRealizer.ROUND_RECT);
320     gnr.setFillColor(GROUP_FILL_COLOR);  
321     gnr.setLineType(LineType.LINE_1);
322     gnr.setLineColor(Color.DARK_GRAY); 
323     gnr.getLabel().setBackgroundColor(new Color(102,204,255,200));
324     gnr.getLabel().setTextColor(new Color(31,104,163).darker());
325     gnr.getLabel().setFontSize(20);
326     gnr.getLabel().setAlignment(NodeLabel.ALIGN_LEFT);
327     gnr.getStateLabel().setVisible(false);
328 
329     //register default group and folder nodes
330     gf.setDefaultFolderNodeRealizer(gnr);
331     gf.setDefaultGroupNodeRealizer(gnr.createCopy());
332   }
333      
334   /**
335    * Configures a realizer for a group node that is identified by a group Id.
336    */
337   protected void configureGroupRealizer(Node groupNode, Object groupId, boolean collapsed) {
338     GroupNodeRealizer nr = (GroupNodeRealizer) getGraph2D().getRealizer(groupNode);
339     nr.setGroupClosed(collapsed);    
340     nr.setBorderInsets(new YInsets(0,0,0,0));
341     nr.getLabel().setText(groupId.toString());            
342   }  
343   
344   /**
345    * Configures a realizer for a node representing an employee. This implementation uses node labels
346    * to represent and cache visual representations used at different levels of detail. Labels
347    * not displayed at a certain level of detail will be set to invisible.
348    */
349   protected void configureNodeRealizer(Node n) {
350     Employee employee = (Employee) getUserObject(n);
351     
352     GenericNodeRealizer gnr = (GenericNodeRealizer) getGraph2D().getRealizer(n);
353     gnr.setUserData(employee);
354         
355     if(gnr.labelCount() == 1) { //NOT CONFIGURED YET
356       //LABEL 0: EMPLOYEE ICON
357       NodeLabel label = gnr.getLabel();
358       label.setIcon(employee.icon);
359       SmartNodeLabelModel model = new SmartNodeLabelModel();
360       label.setLabelModel(model);
361       label.setModelParameter(model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_TOP_LEFT));
362       label.setInsets(new Insets(2,2,2,2));
363 
364       //LABEL 1-5: EMPLOYEE INFORMATION
365       label = addTextLabel(gnr, employee.name,     65, 4, true); 
366       label = addTextLabel(gnr, employee.position, 65, label.getOffsetY() + label.getHeight() + 4, true); // label 2
367       label = addTextLabel(gnr, employee.email,    65, label.getOffsetY() + label.getHeight() + 4, true); // label 3
368       label = addTextLabel(gnr, employee.phone,    65, label.getOffsetY() + label.getHeight() + 4, true); // label 4
369       label = addTextLabel(gnr, employee.fax,      65, label.getOffsetY() + label.getHeight() + 4, true); // label 5
370       
371       //LABEL 6: STATE ICON 
372       label = gnr.createNodeLabel(); 
373       gnr.addLabel(label);
374       label.setIcon(new StateIcon(employee.status));
375       model = new SmartNodeLabelModel();
376       label.setLabelModel(model);
377       label.setModelParameter(model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_TOP_RIGHT));
378       label.setInsets(new Insets(2,2,2,2));
379 
380       //LABEL 7: USED FOR LOD & MEDIUM LOD ONLY  
381       label = addTextLabel(gnr, employee.name,0,0, false);
382       model = new SmartNodeLabelModel();
383       label.setLabelModel(model);
384       label.setModelParameter(model.createDiscreteModelParameter(SmartNodeLabelModel.POSITION_CENTER));
385       label.setFontSize(20);
386       label.setVisible(false);
387     }
388   }
389   
390   /**
391    * Configures an edge realizer for the links between employees. Links that connect an employee with his/her assistant
392    * will be represented by a dashed line.
393    */ 
394   protected void configureEdgeRealizer(Edge e) {
395     EdgeRealizer er = getGraph2D().getRealizer(e);
396     Employee target = (Employee) getUserObject(e.target());    
397     if(target != null && "true".equals(target.assistant)) {
398       er.setLineType(LineType.DASHED_2);
399     }
400     super.configureEdgeRealizer(e);
401   }
402   
403   /**
404    * Helper method that creates and returns node labels.
405    */
406   NodeLabel addTextLabel(NodeRealizer nr, String text, double x, double y, boolean cropping) {
407     NodeLabel label = nr.createNodeLabel();
408     nr.addLabel(label);
409     label.setFontSize(11);
410     if(text == null) text = "";
411     label.setText(text);
412     SmartNodeLabelModel labelModel = new SmartNodeLabelModel();
413     label.setLabelModel(labelModel);
414     final double h = label.getHeight();
415     OrientedRectangle box = new OrientedRectangle(nr.getX() + x, nr.getY() + y + h, label.getWidth(), h);
416     label.setModelParameter(labelModel.createAlignedModelParameter(box, nr, SmartNodeLabelModel.ALIGNMENT_TOP_LEFT));
417 
418     if(cropping) {
419       double height = label.getHeight();
420       label.setAutoSizePolicy(YLabel.AUTOSIZE_NONE);
421       label.setConfiguration("CroppingLabel");
422       label.setAlignment(YLabel.ALIGN_LEFT);
423       double width = nr.getWidth() - x;
424       label.setContentSize(width, height);
425     }
426     return label;
427   }
428 
429   /**
430    * State Icon implementation. The state icon is used by the EmployeePainter to represent 
431    * the state of an employee (present, unavailable, travel). Its implementation makes use of
432    * LODRenderingHints.
433    */
434   static class StateIcon implements Icon {
435 
436     static final Color STATUS_UNAVAILABLE_COLOR = new Color(255,120,40);
437     static final Color STATUS_PRESENT_COLOR = new Color(25,205,44);
438     static final Color STATUS_TRAVEL_COLOR = new Color(221,175,233);
439     static final Color STATUS_UNAVAILABLE_COLOR2 = new Color(231,32,0);
440     static final Color STATUS_PRESENT_COLOR2 = new Color(19,157,33);
441     static final Color STATUS_TRAVEL_COLOR2 = new Color(137,44,160);
442 
443     String state;
444     StateIcon(String state) {
445       this.state = state;
446     }
447     
448     public int getIconHeight() {
449       return 24;
450     }
451 
452     public int getIconWidth() {
453       return 24;
454     }
455 
456     public void paintIcon(Component c, Graphics g, int x, int y) {
457       Graphics2D gfx = (Graphics2D) g;
458       
459       Object lod = gfx.getRenderingHint(LODRenderingHint.KEY_LOD);
460       
461       if(lod == LODRenderingHint.VALUE_LOD_MEDIUM) {
462         if("present".equals(state)) {
463           gfx.setColor(STATUS_PRESENT_COLOR);
464         } 
465         else if("travel".equals(state)) {
466           gfx.setColor(STATUS_TRAVEL_COLOR);
467         }
468         else if("unavailable".equals(state)){
469           gfx.setColor(STATUS_UNAVAILABLE_COLOR);
470         }
471         
472         Rectangle2D box = new Rectangle2D.Double(x,y,getIconWidth(), getIconHeight());
473         gfx.fill(box);
474         gfx.setColor(gfx.getColor().darker());
475         gfx.draw(box);       
476       }
477       else if(lod == LODRenderingHint.VALUE_LOD_HIGH) {
478         Ellipse2D.Double circle = new Ellipse2D.Double(x,y,getIconWidth(), getIconHeight());
479         if("present".equals(state)) {
480           gfx.setColor(STATUS_PRESENT_COLOR2);          
481         } 
482         else if("travel".equals(state)) {
483           gfx.setColor(STATUS_TRAVEL_COLOR2);
484         }
485         else if("unavailable".equals(state)){
486           gfx.setColor(STATUS_UNAVAILABLE_COLOR2);
487         }
488         gfx.fill(circle);
489         double size = Math.min(getIconHeight(), getIconWidth());
490         double delta = size*0.12;
491         circle.setFrame(circle.x+delta, circle.y+delta, circle.width-2*delta, circle.height-2*delta);        
492         gfx.setColor(Color.WHITE);
493         gfx.fill(circle);
494         if("present".equals(state)) {
495           gfx.setColor(STATUS_PRESENT_COLOR);          
496         } 
497         else if("travel".equals(state)) {
498           gfx.setColor(STATUS_TRAVEL_COLOR);
499         }
500         else if("unavailable".equals(state)){
501           gfx.setColor(STATUS_UNAVAILABLE_COLOR);
502         }        
503         circle.setFrame(circle.x+delta, circle.y+delta, circle.width-2*delta, circle.height-2*delta);        
504         gfx.fill(circle);        
505       }
506     }    
507   }
508  
509   /**
510    * Painter implementation that renders an Employee node.
511    */
512   static class EmployeePainter extends ShapeNodePainter {
513     
514     public EmployeePainter() {
515       setShapeType(ROUND_RECT);
516     }
517     
518     public void paint(NodeRealizer context, Graphics2D gfx) {
519       
520       if(!context.isVisible()) {
521         return;
522       }
523       
524       GenericNodeRealizer gnr = (GenericNodeRealizer) context;    
525       Employee employee = (Employee) gnr.getUserData();    
526       
527       Object lod = gfx.getRenderingHint(LODRenderingHint.KEY_LOD);
528       
529       if(lod == LODRenderingHint.VALUE_LOD_OVERVIEW) {
530         Color ovc = null;
531         if("present".equals(employee.status)) {
532           ovc = new Color(19,157,33); //green
533         } else if("travel".equals(employee.status)) {
534           ovc = new Color(137,44,160);
535         }
536         else {
537           ovc = Color.RED;
538         }
539         Rectangle2D rect = new Rectangle2D.Double(context.getX()+20, context.getY()+20, context.getWidth()-40, context.getHeight()-40);
540         gfx.setColor(ovc);
541         gfx.fill(rect);
542         gfx.setColor(Color.black);
543         gfx.draw(rect);
544       }
545       else if(lod == LODRenderingHint.VALUE_LOD_LOW || lod == LODRenderingHint.VALUE_LOD_MEDIUM) { 
546         super.paintNode(context, gfx, true);
547         
548         context.getLabel(6).paint(gfx); //state icon 
549         
550         NodeLabel label = context.getLabel(7);
551         label.setVisible(true);
552         label.paint(gfx);
553         label.setVisible(false);        
554       }
555       else { //defaults to LODRenderingHint.VALUE_LOD_HIGH) 
556         super.paintNode(context, gfx, false);
557         context.paintText(gfx);
558       }      
559     }
560     
561     public void paintSloppy(NodeRealizer context, Graphics2D gfx) {
562       paint(context, gfx);
563     }
564     
565     protected Color createSelectionColor(Color original) {
566       // don't modify the fill color here - we are
567       // changing the fill color externally upon selection
568       return original;
569     }
570 
571     /**
572      * Fill the shape using a custom gradient color and a semi-transparent effect  
573      */
574     protected void paintFilledShape(NodeRealizer context, Graphics2D graphics, Shape shape) {
575       double x = context.getX();
576       double y = context.getY();
577       double width = context.getWidth();
578       double height = context.getHeight();
579 
580       boolean selected = useSelectionStyle(context, graphics);
581       Color c1 = getFillColor(context, selected);
582       
583       if (c1 != null && !context.isTransparent()) {
584         Color c2 = getFillColor2(context, selected);
585         if (c2 != null && useGradientStyle(graphics)) {
586           graphics.setPaint(new GradientPaint(
587                   (float) (x + 0.5*width), (float) y, c1,
588                   (float) (x + 0.5*width), (float) (y+height), c2));
589         } else {
590           graphics.setColor(c1);
591         }
592         graphics.fill(shape);
593                 
594         Shape clip = graphics.getClip();        
595         graphics.clip(shape);
596         graphics.clip(new Rectangle2D.Double(x,y,width,0.4*height));
597         graphics.setColor(new Color(1.0f, 1.0f,1.0f, 0.3f));
598         graphics.fill(shape);
599         graphics.setClip(clip);
600       }
601     }
602 
603     private static boolean useGradientStyle( final Graphics2D graphics ) {
604       return YRenderingHints.isGradientPaintingEnabled(graphics);
605     }
606 
607     private static boolean useSelectionStyle(
608             final NodeRealizer context,
609             final Graphics2D graphics
610     ) {
611       return context.isSelected() &&
612              YRenderingHints.isSelectionPaintingEnabled(graphics);
613     }
614   }
615 
616   /**
617    * Customized GroupNodeRealizer that cannot be selected. 
618    */
619   static class CustomGroupNodeRealizer extends GroupNodeRealizer {
620     public CustomGroupNodeRealizer() {
621       super();
622     }
623     public CustomGroupNodeRealizer(NodeRealizer nr) {
624       super(nr);
625     }
626     public NodeRealizer createCopy(NodeRealizer nr) {
627       return new CustomGroupNodeRealizer(nr);
628     }
629     public boolean isSelected() {
630       return false;
631     }
632   }
633   
634   /**
635    * Drawable implementation used by the NavigationMode Scroller. 
636    * The appearance of the Scroller Drawable is zoom invariant. To accomplish this
637    * it is drawn in view coordinate space.  
638    */
639   static class ScrollerDrawable extends ViewCoordDrawableAdapter {
640     Scroller scroller;
641     public ScrollerDrawable(Graph2DView view, Scroller scroller) {
642       super(view, null);
643       this.scroller = scroller;
644     }
645 
646     protected void paintViewCoordinateDrawable(Graphics2D gfx) {     
647       gfx = (Graphics2D)gfx.create();            
648       YVector dir = scroller.getScrollDirection();
649       YPoint p = scroller.getScrollStart();
650       p = new YPoint(view.toViewCoordX(p.x), view.toViewCoordY(p.y));
651       Ellipse2D circle = new Ellipse2D.Double(p.x-15, p.y-15,30,30);
652       gfx.setColor(new Color(204,204,204,100));
653       gfx.fill(circle);
654       gfx.setColor(new Color(100,100,100,100));
655       gfx.setStroke(LineType.LINE_1);
656       AffineTransform trans = new AffineTransform(dir.getX(), dir.getY(),-dir.getY(), dir.getX(),p.x,p.y);
657       GeneralPath arrow = new GeneralPath(GeneralPath.WIND_NON_ZERO,6);
658       arrow.moveTo(15,0);
659       arrow.lineTo(0,5);
660       arrow.lineTo(0,-5);      
661       gfx.fill(trans.createTransformedShape(arrow));      
662       gfx.setStroke(LineType.LINE_2);
663       gfx.draw(circle);      
664       gfx.dispose();
665     }
666 
667     protected Rectangle getViewCoordinateDrawableBounds() {
668       YPoint p = scroller.getScrollStart();
669       p = new YPoint(view.toViewCoordX(p.x), view.toViewCoordY(p.y));        
670       return (new Rectangle2D.Double(p.x-20,p.y-20,40,40)).getBounds();
671     }
672   }
673 
674   /**
675    * Configures and creates an overview component that uses this JOrgChart as its master view. 
676    */
677   public Overview createOverview() {
678     Overview ov = super.createOverview();
679     /* customize the overview */
680     //animates the scrolling
681     //blurs the part of the graph which can currently not be seen
682     ov.putClientProperty("Overview.PaintStyle", "Funky");
683     //allows zooming from within the overview
684     ov.putClientProperty("Overview.AllowZooming", Boolean.TRUE);
685     //provides functionality for navigation via keyboard (zoom in (+), zoom out (-), navigation with arrow keys)
686     ov.putClientProperty("Overview.AllowKeyboardNavigation", Boolean.TRUE);
687     //determines how to differ between the part of the graph that can currently be seen, and the rest
688     ov.putClientProperty("Overview.Inverse", Boolean.TRUE);
689     ov.setPreferredSize(new Dimension(200, 150));
690     ov.setMinimumSize(new Dimension(200, 150));
691     ov.getRenderingHints().put(LODRenderingHint.KEY_LOD, LODRenderingHint.VALUE_LOD_OVERVIEW);
692     return ov;
693   }
694 
695   private void configureColors(
696           final NodeRealizer nr,
697           final boolean selected,
698           final boolean highlighted
699   ) {
700     if(highlighted) {
701       if(selected) {
702         nr.setFillColor(SELECTED_HOVER_FILL_COLOR);
703         nr.setFillColor2(SELECTED_HOVER_FILL_COLOR2);
704         nr.setLineColor(SELECTED_HOVER_LINE_COLOR);
705       } else { //not selected
706         nr.setFillColor(HOVER_FILL_COLOR);
707         nr.setFillColor2(HOVER_FILL_COLOR2);
708         nr.setLineColor(HOVER_LINE_COLOR);
709       }
710     } else { //not highlighted
711       if(selected) {
712         nr.setFillColor(SELECTED_FILL_COLOR);
713         nr.setFillColor2(SELECTED_FILL_COLOR2);
714         nr.setLineColor(SELECTED_LINE_COLOR);
715       } else { // not selected
716         nr.setFillColor(FILL_COLOR);
717         nr.setFillColor2(FILL_COLOR2);
718         nr.setLineColor(LINE_COLOR);
719       }
720     }
721   }
722 
723   /**
724    * Implementation of a ViewMode that activates {@link y.view.MagnifierViewMode} while
725    * the middle mouse button or the mouse wheel is pressed or dragged.
726    */
727   static class MiddleClickMagnifierViewMode extends ViewMode {        
728     MagnifierViewMode magnifierVM;
729     public MiddleClickMagnifierViewMode() {
730       magnifierVM = new MagnifierViewMode() {
731         public void mouseDraggedMiddle(double x, double y) {
732           mouseDraggedLeft(x, y);
733         }
734         protected Graph2DView createMagnifierView() {
735           Graph2DView view = super.createMagnifierView();
736           view.getRenderingHints().put(LODRenderingHint.KEY_LOD, LODRenderingHint.VALUE_LOD_HIGH);
737           return view;
738         }
739       };
740      magnifierVM.setMouseWheelEnabled(false);     
741     }    
742     
743     public void mousePressedMiddle(double x, double y) {
744       double zoom = view.getZoom();
745       magnifierVM.setMagnifierZoomFactor(Math.max(1,1/zoom));
746       magnifierVM.setMagnifierRadius(200);
747       view.addViewMode(magnifierVM);
748       magnifierVM.mouseMoved(lastPressEvent);
749       view.updateView();
750     }
751     
752     public void mouseReleasedMiddle(double x, double y) {
753       view.removeViewMode(magnifierVM);
754     }    
755   }
756 
757   /**
758    * A <code>ViewMode</code> that produces a roll-over effect for nodes
759    * under the mouse cursor.
760    */
761   private class RollOverViewMode extends ViewMode {
762     
763     /** Preferred duration for roll over effect animations */
764     private static final int PREFERRED_DURATION = 200;
765 
766     /** Stores the last node that was marked with the roll over effect */
767     private Node lastHitNode;
768     private AnimationPlayer player;
769     private PropertyChangeListener pcl;
770     private MouseEvent lastRelevantMouseEvent;
771     
772     public RollOverViewMode() {
773     }
774 
775     public void activate(boolean b) {    
776       super.activate(b);
777       if(b) {
778         player = new AnimationPlayer();
779         player.addAnimationListener(view);
780         player.setBlocking(false);
781         pcl = new CanvasPropertyChangeListener();
782         view.getCanvasComponent().addPropertyChangeListener(pcl);
783       } else if(pcl != null){        
784         view.getCanvasComponent().removePropertyChangeListener(pcl);
785       }
786     }
787     
788     public void mouseExited(MouseEvent e) {
789       lastRelevantMouseEvent = null;
790       if(lastHitNode != null) {
791         unmark(lastHitNode);
792       }
793     }
794     
795     public void mouseDragged(MouseEvent e) {
796       lastRelevantMouseEvent = e;
797       super.mouseDragged(e);
798     }
799 
800     public void mouseMoved(MouseEvent e) {
801       lastRelevantMouseEvent = e;
802       super.mouseMoved(e);
803     }
804 
805     /**
806      * Triggers a roll-over effect for the first node at the specified location.
807      */
808     public void mouseMoved( final double x, final double y ) {      
809       final HitInfo hi = getHitInfo(x, y);
810       
811       if (hi.hasHitNodes()) {
812         final Node node = (Node) hi.hitNodes().current();
813         if (node != lastHitNode) {
814           unmark(lastHitNode);
815         }
816         JOrgChart treeView = (JOrgChart) view;
817         Object userObject = treeView.getUserObject(node);        
818         if(userObject != null && !treeView.highlightMap.getBool(node)) {
819           mark(node);
820           lastHitNode = node;
821         }
822       } else {
823         unmark(lastHitNode);
824         lastHitNode = null;
825       }
826     }
827     
828      /**
829      * Overridden to take only nodes into account for hit testing.
830      */
831     protected HitInfo getHitInfo( final double x, final double y ) {
832       final HitInfo hi = new HitInfo(view, x, y, true, HitInfo.NODE);
833       setLastHitInfo(hi);
834       return hi;
835     }
836         
837     /**
838      * Triggers a <em>mark</em> animation for the specified node.
839      * Sets the animation state of the given node to <em>MARKED</em>.
840      */
841     protected void mark( final Node node ) {
842       // only start a mark animation if no other animation is playing
843       // for the given node
844       JOrgChart treeView = (JOrgChart) view;
845       boolean highlighted = treeView.highlightMap.getBool(node);
846       Object userObject = treeView.getUserObject(node);
847       
848       if(userObject != null && !highlighted) {
849         treeView.highlightMap.setBool(node, true);
850         
851         final NodeRealizer nr = getGraph2D().getRealizer(node);
852         final NodeRealizer newTheme = nr.createCopy();
853         configureColors(newTheme, getGraph2D().isSelected(node), true);
854         
855         ViewAnimationFactory animFac = new ViewAnimationFactory(view);
856         final AnimationObject ao = animFac.color(
857                 nr,
858                 newTheme.getFillColor(), newTheme.getFillColor2(), newTheme.getLineColor(),
859                 ViewAnimationFactory.APPLY_EFFECT, PREFERRED_DURATION);
860 
861         final AnimationObject eao = AnimationFactory.createEasedAnimation(ao);        
862         player.animate(eao); 
863       }     
864     }
865 
866     class CanvasPropertyChangeListener implements PropertyChangeListener {
867       public void propertyChange(PropertyChangeEvent evt) {
868         if(lastRelevantMouseEvent != null && ("Zoom".equals(evt.getPropertyName()) || "ViewPoint".equals(evt.getPropertyName()))) {
869           if(lastHitNode != null && getHitInfo(lastRelevantMouseEvent).getHitNode() != lastHitNode) {
870             unmark(lastHitNode);
871             lastHitNode = null;
872           }
873           else if(lastHitNode == null && lastRelevantMouseEvent != null) {
874             lastHitNode = getHitInfo(lastRelevantMouseEvent).getHitNode();
875             if(lastHitNode != null) {
876               mark(lastHitNode);
877             }
878           }
879         }          
880       }        
881     }
882     
883     /**
884      * Triggers an <em>unmark</em> animation for the specified node.
885      * Sets the animation state of the given node to <em>UNMARKED</em>.
886      */
887     protected void unmark( final Node node ) {
888       if (node == null || node.getGraph() == null) {
889         player.stop();
890         return;
891       }
892       JOrgChart treeView = (JOrgChart) view;
893       Object userObject = null;      
894       try {
895         userObject = treeView.getUserObject(node);
896       }
897       catch(Exception ex) {
898         D.bug(node);
899       }
900       if(userObject != null && treeView.highlightMap.getBool(node)) {
901         treeView.highlightMap.setBool(node, false);
902     
903         player.stop();
904         configureColors(getGraph2D().getRealizer(node), getGraph2D().isSelected(node), false);
905         getGraph2D().updateViews();
906       }      
907     }    
908   }
909 
910   /**
911    * {@link y.view.Graph2DSelectionListener} that assigns a properly configured 
912    * realizer to a node whenever its selected state changes.
913    */
914   class TreeChartSelectionListener implements Graph2DSelectionListener {
915     public void onGraph2DSelectionEvent(Graph2DSelectionEvent ev) {
916       if(ev.getSubject() instanceof Node) {
917         Node node = (Node) ev.getSubject();        
918         Object userObject = getUserObject(node);
919         if(userObject != null) {
920           Graph2D graph = getGraph2D();
921           configureColors(getGraph2D().getRealizer(node), graph.isSelected(node), highlightMap.getBool(node));
922           graph.updateViews();
923         }
924       }
925     }    
926   }
927   
928   /**
929    * Calculate a layout for organization charts. The layouter takes the Employee's <code>layout</code> 
930    * and <code>assistant</code> properties into account.  
931    */
932   protected Layouter createLayouter() {
933     OrgChartLayouter layouter = new OrgChartLayouter();
934     Graph2D graph = getGraph2D();      
935     DataProvider childLayoutDP = new DataProviderAdapter() {        
936       public Object get(Object n) {
937         Employee employee = (Employee) getUserObject((Node)n);
938         if(employee != null) {
939           if("leftHanging".equals(employee.layout)) {
940             return OrgChartLayouter.CHILD_LAYOUT_LEFT_BELOW;
941           }
942           if("rightHanging".equals(employee.layout)) {
943             return OrgChartLayouter.CHILD_LAYOUT_RIGHT_BELOW;
944           }
945           if("bothHanging".equals(employee.layout)) {
946             return OrgChartLayouter.CHILD_LAYOUT_BELOW;
947           }
948         }
949         return OrgChartLayouter.CHILD_LAYOUT_SAME_LAYER;
950       }          
951     };
952     graph.addDataProvider(OrgChartLayouter.CHILD_LAYOUT_DPKEY, childLayoutDP);
953     
954     DataProvider assistantDP = new DataProviderAdapter() {
955       public boolean getBool(Object n) {
956         Employee employee = (Employee) getUserObject((Node)n);          
957         return employee != null && "true".equals(employee.assistant);
958       }
959     };      
960     graph.addDataProvider(OrgChartLayouter.ASSISTANT_DPKEY, assistantDP);
961     
962     layouter.setDuplicateBendsOnSharedBus(true);
963 
964     return new NormalizingGraphElementOrderStage(layouter);
965   }  
966 }
967