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