View Javadoc
1   /*
2    * ObjectLab, http://www.objectlab.co.uk/open is supporting JTreeMap.
3    *
4    * Based in London, we are world leaders in the design and development
5    * of bespoke applications for the securities financing markets.
6    *
7    * <a href="http://www.objectlab.co.uk/open">Click here to learn more</a>
8    *           ___  _     _           _   _          _
9    *          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
10   *         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
11   *         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
12   *          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
13   *                   |__/
14   *
15   *                     www.ObjectLab.co.uk
16   *
17   * $Id$
18   *
19   * Copyright 2006 the original author or authors.
20   *
21   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
22   * use this file except in compliance with the License. You may obtain a copy of
23   * the License at
24   *
25   * http://www.apache.org/licenses/LICENSE-2.0
26   *
27   * Unless required by applicable law or agreed to in writing, software
28   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
29   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
30   * License for the specific language governing permissions and limitations under
31   * the License.
32   */
33  package net.sf.jtreemap.swing;
34  
35  import java.awt.Color;
36  import java.awt.FontMetrics;
37  import java.awt.Graphics;
38  import java.awt.Insets;
39  import java.awt.Point;
40  import java.awt.event.MouseAdapter;
41  import java.awt.event.MouseEvent;
42  import java.awt.event.MouseMotionAdapter;
43  import java.io.Serializable;
44  import java.util.Enumeration;
45  
46  import javax.swing.JComponent;
47  import javax.swing.JToolTip;
48  import javax.swing.JTree;
49  import javax.swing.ToolTipManager;
50  import javax.swing.border.Border;
51  import javax.swing.tree.TreePath;
52  
53  import net.sf.jtreemap.swing.provider.ColorProvider;
54  import net.sf.jtreemap.swing.provider.UniqueColorProvider;
55  
56  /**
57   * JComponent who represents each element of a tree in a rectangle of more or
58   * less big size according to its importance in the tree.
59   * <p>
60   * A tree structure may includes more or less important elements. For example,
61   * in a tree structure of files, there can be files of big size. Then it can be
62   * interesting to know which repertory is the most important on a hard disk.
63   * <p>
64   * Moreover, we can add a code color which makes it possible to introduce new
65   * information into the representation of the tree structure.
66   * <p>
67   * So, in a JTreeMap, you can see the size and the value of an element in a
68   * tree.
69   *
70   * @see net.sf.jtreemap.swing.TreeMapNode
71   * @author Laurent Dutheil
72   */
73  public class JTreeMap extends JComponent {
74      private static final long serialVersionUID = 7255952672238300249L;
75      private static final int BORDER_FOR_FONT = 5;
76      private static final int MAX_NUM_CHAR = 3;
77      private static final int INSET = 4;
78      private static final int DISMISS_DELAY_MS = 100000;
79      private static final Color TRANSPARENCY_COLOR = new Color(204, 204, 204, 128);
80  
81      /**
82       * The optional tree representation of the hierarchical data.
83       */
84      private JTree treeView;
85  
86      // active leaf
87      private TreeMapNode activeLeaf = null;
88  
89      // color provider
90      private ColorProvider colorProvider = null;
91  
92      // displayed root
93      private TreeMapNode displayedRoot = null;
94  
95      // root of the tree
96      private TreeMapNode root = null;
97  
98      // divide strategy
99      private SplitStrategy strategy = null;
100 
101     // tooltip builder
102     private IToolTipBuilder toolTipBuilder;
103 
104     // zoom
105     private final Zoom zoom = new Zoom();
106 
107     /**
108      * Constructor of JTreeMap. <BR>
109      * The chosen strategy is SplitSquarified. <BR>
110      * The chosen color provider is UniqueColorProvider.
111      *
112      * @see SplitSquarified
113      * @see UniqueColorProvider
114      * @param root
115      *            the root of the tree to display
116      */
117     public JTreeMap(final TreeMapNode root) {
118         this(root, new SplitSquarified(), null, null, false);
119     }
120 
121     /**
122      * Constructor of JTreeMap. <BR>
123      * The chosen strategy is SplitSquarified. <BR>
124      * The chosen color provider is UniqueColorProvider.
125      *
126      * @see SplitSquarified
127      * @see UniqueColorProvider
128      * @param root the root of the tree to display
129      * @param treeView The tree representation of the hierarchical data.
130      */
131     public JTreeMap(final TreeMapNode root, final JTree treeView) {
132         this(root, new SplitSquarified(), null, null, false);
133         this.treeView = treeView;
134     }
135 
136     /**
137      * Constructor of JTreeMap. <BR>
138      * The chosen color provider is UniqueColorProvider.
139      *
140      * @see UniqueColorProvider
141      * @param root
142      *            the root of the tree to display
143      * @param strategy
144      *            the split strategy
145      * @param treeView The tree representation of the hierarchical data.
146      */
147     public JTreeMap(final TreeMapNode root, final SplitStrategy strategy, final JTree treeView, final String weightPrefix, final String valuePrefix,
148             final boolean showWeight) {
149         this(root, strategy, weightPrefix, valuePrefix, showWeight);
150         this.treeView = treeView;
151     }
152 
153     /**
154      * Constructor of JTreeMap. <BR>
155      * The chosen color provider is UniqueColorProvider.
156      *
157      * @see UniqueColorProvider
158      * @param root
159      *            the root of the tree to display
160      * @param strategy
161      *            the split strategy
162      */
163     public JTreeMap(final TreeMapNode root, final SplitStrategy strategy, final String weightPrefix, final String valuePrefix,
164             final boolean showWeight) {
165         // ToolTips appears without delay and stay as long as possible
166         final ToolTipManager ttm = ToolTipManager.sharedInstance();
167         ttm.setInitialDelay(0);
168         ttm.setReshowDelay(0);
169         ttm.setDismissDelay(DISMISS_DELAY_MS);
170         ttm.setEnabled(true);
171         ttm.setLightWeightPopupEnabled(true);
172         setToolTipText("");
173 
174         // the default DefaultToolTipBuilder
175         toolTipBuilder = new DefaultToolTipBuilder(this, weightPrefix, valuePrefix, showWeight);
176 
177         setRoot(root);
178         setStrategy(strategy);
179         setColorProvider(new UniqueColorProvider());
180 
181         addMouseMotionListener(new HandleMouseMotion());
182         addMouseListener(new HandleMouseClick());
183     }
184 
185     /**
186      * calculate the postitions for the displayed root. <BR>
187      * The positions of the root must be calculated first.
188      */
189     public void calculatePositions() {
190         if (this.getStrategy() != null && displayedRoot != null) {
191             getStrategy().calculatePositions(this.displayedRoot);
192         }
193     }
194 
195     @Override
196     public JToolTip createToolTip() {
197         return toolTipBuilder.getToolTip();
198     }
199 
200     /**
201      * draw the item.
202      *
203      * @param g
204      *            Graphics where you have to draw
205      * @param item
206      *            item to draw
207      */
208     protected void draw(final Graphics g, final TreeMapNode item) {
209         if (item.isLeaf() && item.getValue() != null) {
210             g.setColor(this.colorProvider.getColor(item.getValue()));
211             g.fillRect(item.getX(), item.getY(), item.getWidth(), item.getHeight());
212         } else {
213             for (final Enumeration e = item.children(); e.hasMoreElements();) {
214                 draw(g, (TreeMapNode) e.nextElement());
215             }
216         }
217     }
218 
219     /**
220      * write the label in the middle of the item. <BR>
221      * You have first to define the font of the Graphics. <BR>
222      * You may override this method to change the position or the color of the
223      * label.
224      *
225      * @param g
226      *            Graphics where you have to draw
227      * @param item
228      *            TreeMapNode to draw
229      */
230     protected void drawLabel(final Graphics g, final TreeMapNode item) {
231         final FontMetrics fm = g.getFontMetrics(g.getFont());
232         // if the height of the item is high enough
233         if (fm.getHeight() < item.getHeight() - 2) {
234             String label = item.getLabel();
235 
236             final int y = (item.getHeight() + fm.getAscent() - fm.getDescent()) / 2;
237             final int stringWidth = fm.stringWidth(label);
238             // the width of the label depends on the font :
239             // if the width of the label is larger than the item
240             if (item.getWidth() - BORDER_FOR_FONT <= stringWidth) {
241                 // We have to truncate the label
242                 // number of chars who can be writen in the item
243                 final int nbChar = label.length() * item.getWidth() / stringWidth;
244                 if (nbChar > MAX_NUM_CHAR) {
245                     // and add "..." at the end
246                     label = label.substring(0, nbChar - MAX_NUM_CHAR) + "...";
247                 } else {
248                     // if it is not enough large, we display nothing
249                     label = "";
250                 }
251             }
252             final int x = (item.getWidth() - fm.stringWidth(label)) / 2;
253 
254             // background in black
255             g.setColor(Color.black);
256             g.drawString(label, item.getX() + x + 1, item.getY() + y + 1);
257             g.drawString(label, item.getX() + x - 1, item.getY() + y + 1);
258             g.drawString(label, item.getX() + x + 1, item.getY() + y - 1);
259             g.drawString(label, item.getX() + x - 1, item.getY() + y - 1);
260             g.drawString(label, item.getX() + x + 1, item.getY() + y);
261             g.drawString(label, item.getX() + x - 1, item.getY() + y);
262             g.drawString(label, item.getX() + x, item.getY() + y + 1);
263             g.drawString(label, item.getX() + x, item.getY() + y - 1);
264             // label in white
265             g.setColor(Color.white);
266             g.drawString(label, item.getX() + x, item.getY() + y);
267         }
268     }
269 
270     /**
271      * Draw all the labels to draw. <BR>
272      * You may override this method to draw the labels you want. <BR>
273      * For exemples, all the leaves, or all the first level children, or all of
274      * them...
275      *
276      * @param g
277      *            Graphics where you have to draw
278      * @param item
279      *            TreeMapNode to draw
280      */
281     protected void drawLabels(final Graphics g, final TreeMapNode item) {
282         // add the labels (level -1)
283         g.setFont(this.getFont());
284         if (this.displayedRoot.isLeaf()) {
285             drawLabel(g, displayedRoot);
286         } else {
287             for (final Enumeration e = displayedRoot.children(); e.hasMoreElements();) {
288                 drawLabel(g, (TreeMapNode) e.nextElement());
289             }
290         }
291 
292         /* uncomment to add the labels of the lowered levels (up to depth > 2) */
293         // int depth = item.getLevel() - displayedRoot.getLevel();
294         // float newSize = Math.max(20, getFont().getSize2D());
295         // java.awt.Font labelFont =
296         // getFont().deriveFont(java.awt.Font.BOLD,
297         // newSize - 3 * depth);
298         // g.setFont(labelFont);
299         // if (depth > 2) {
300         // drawLabel(g, item);
301         // return;
302         // }
303         // if (item.isLeaf()) {
304         // drawLabel(g, item);
305         // } else {
306         // for (Enumeration e = item.children(); e.hasMoreElements();) {
307         // drawLabels(g, (TreeMapNode) (e.nextElement()));
308         // }
309         // }
310     }
311 
312     /**
313      * get the active leaf (the one under the mouse).
314      *
315      * @return Returns the activeLeaf.
316      */
317     public TreeMapNode getActiveLeaf() {
318         return activeLeaf;
319     }
320 
321     /**
322      * get the ColorProvider.
323      *
324      * @return the ColorProvider
325      */
326     public ColorProvider getColorProvider() {
327         return colorProvider;
328     }
329 
330     /**
331      * get the displayed root.
332      * <p>
333      * This may be not the root of the jTreeMap. After a zoom, the displayed
334      * root can be the root of an under-tree.
335      * </p>
336      *
337      * @return the displayed root
338      */
339     public TreeMapNode getDisplayedRoot() {
340         return displayedRoot;
341     }
342 
343     /**
344      * get the root.
345      *
346      * @return the root
347      */
348     public TreeMapNode getRoot() {
349         return root;
350     }
351 
352     /**
353      * get the SplitStrategy.
354      *
355      * @return the SplitStrategy
356      */
357     public SplitStrategy getStrategy() {
358         return strategy;
359     }
360 
361     @Override
362     public Point getToolTipLocation(final MouseEvent event) {
363         int posX;
364         int posY;
365         final JToolTip toolTip = createToolTip();
366         final int xMax = displayedRoot.getX() + displayedRoot.getWidth();
367         final int yMin = displayedRoot.getY();
368         if (this.activeLeaf != null) {
369             if (this.activeLeaf.getWidth() >= toolTip.getWidth() + 2 * INSET && activeLeaf.getHeight() >= toolTip.getHeight() + 2 * INSET) {
370                 posX = activeLeaf.getX() + INSET;
371                 posY = activeLeaf.getY() + INSET;
372             } else {
373                 posX = activeLeaf.getX() + activeLeaf.getWidth() + INSET;
374                 posY = activeLeaf.getY() - toolTip.getHeight() - INSET;
375             }
376 
377             if (posY < yMin + INSET) {
378                 posY = yMin + INSET;
379             }
380             if (posX + toolTip.getWidth() > xMax - INSET && this.activeLeaf.getX() >= toolTip.getWidth() + INSET) {
381                 posX = activeLeaf.getX() - INSET - toolTip.getWidth();
382             }
383 
384             return new Point(posX, posY);
385         }
386         return null;
387     }
388 
389     /*
390      * (non-Javadoc)
391      *
392      * @see javax.swing.JComponent#paintComponent(java.awt.Graphics)
393      */
394     @Override
395     protected void paintComponent(final Graphics g) {
396         super.paintComponent(g);
397         final int width = getSize().width;
398         final int height = getSize().height;
399         final Insets insets = getInsets();
400 
401         final int border = TreeMapNode.getBorder();
402         root.setDimension(this.root.getX(), root.getY(), width - border - insets.left - insets.right, height - border - insets.top - insets.bottom);
403 
404         if (!this.root.equals(this.displayedRoot)) {
405             displayedRoot.setDimension(this.displayedRoot.getX(), displayedRoot.getY(), width - border - insets.left - insets.right,
406                     height - border - insets.top - insets.bottom);
407         }
408 
409         calculatePositions();
410 
411         if (this.displayedRoot.children().hasMoreElements()) {
412             // the background
413             g.setColor(this.getBackground());
414             g.fillRect(this.displayedRoot.getX(), displayedRoot.getY(), displayedRoot.getWidth() + border, displayedRoot.getHeight() + border);
415             // the JTreeMapExample
416             draw(g, displayedRoot);
417             // reveal the active leaf
418             if (this.activeLeaf != null) {
419                 reveal(g, activeLeaf);
420             }
421             // the labels
422             drawLabels(g, displayedRoot);
423         }
424 
425     }
426 
427     /**
428      * reveal the item.
429      *
430      * @param g
431      *            Graphics where you have to draw
432      * @param item
433      *            TreeMapNode to reveal
434      */
435     protected void reveal(final Graphics g, final TreeMapNode item) {
436         if (item.isLeaf()) {
437             g.setColor(TRANSPARENCY_COLOR);
438             g.fillRect(item.getX(), item.getY(), item.getWidth(), item.getHeight());
439         }
440     }
441 
442     /**
443      * set the active leaf.
444      *
445      * @param newActiveLeaf
446      *            the new active leaf
447      */
448     public void setActiveLeaf(final TreeMapNode newActiveLeaf) {
449         if (newActiveLeaf == null || newActiveLeaf.isLeaf()) {
450             activeLeaf = newActiveLeaf;
451         }
452     }
453 
454     /*
455      * (non-Javadoc)
456      *
457      * @see javax.swing.JComponent#setBorder(javax.swing.border.Border)
458      */
459     @Override
460     public void setBorder(final Border border) {
461         // Substract the previous border insets
462         Insets insets = getInsets();
463         displayedRoot.setDimension(this.displayedRoot.getX() - insets.left, displayedRoot.getY() - insets.top,
464                 displayedRoot.getWidth() + insets.left + insets.right, displayedRoot.getHeight() + insets.top + insets.bottom);
465 
466         super.setBorder(border);
467 
468         // Add the new border insets
469         insets = getInsets();
470         displayedRoot.setDimension(this.displayedRoot.getX() + insets.left, displayedRoot.getY() + insets.top,
471                 displayedRoot.getWidth() - insets.left - insets.right, displayedRoot.getHeight() - insets.top - insets.bottom);
472     }
473 
474     /**
475      * set the ColorProvider.
476      *
477      * @param newColorProvider
478      *            the new ColorPorvider
479      */
480     public void setColorProvider(final ColorProvider newColorProvider) {
481         colorProvider = newColorProvider;
482     }
483 
484     /**
485      * set the displayed root.
486      * <p>
487      * This may be not the root of the jTreeMap. After a zoom, the displayed
488      * root can be the root of an under-tree.
489      * </p>
490      *
491      * @param newDisplayedRoot
492      *            new DiplayedRoot
493      */
494     public void setDisplayedRoot(final TreeMapNode newDisplayedRoot) {
495         displayedRoot = newDisplayedRoot;
496     }
497 
498     /**
499      * set the new root.
500      *
501      * @param newRoot
502      *            the new root to set
503      */
504     public void setRoot(final TreeMapNode newRoot) {
505         root = newRoot;
506         final Insets insets = getInsets();
507         root.setX(insets.left);
508         root.setY(insets.top);
509         setDisplayedRoot(this.root);
510 
511     }
512 
513     /**
514      * set the new strategy.
515      *
516      * @param newStrat
517      *            the new strategy to set
518      */
519     public void setStrategy(final SplitStrategy newStrat) {
520         strategy = newStrat;
521     }
522 
523     /**
524      * Set the builder of the toolTip.<BR>
525      *
526      * @param toolTipBuilder
527      *            The toolTipBuilder to set.
528      */
529     public void setToolTipBuilder(final IToolTipBuilder toolTipBuilder) {
530         this.toolTipBuilder = toolTipBuilder;
531     }
532 
533     /**
534      * When you zoom the jTreeMap, you have the choice to keep proportions or
535      * not.
536      *
537      * @param keepProportion
538      *            true if you want to keep proportions, else false
539      */
540     public void setZoomKeepProportion(final boolean keepProportion) {
541         zoom.setKeepProportion(keepProportion);
542     }
543 
544     /**
545      * Undo the zoom to display the root.
546      */
547     public void unzoom() {
548         zoom.undo();
549     }
550 
551     /**
552      * Zoom the JTreeMap to the dest node.
553      *
554      * @param dest
555      *            node we want to zoom
556      */
557     public void zoom(final TreeMapNode dest) {
558         // undo the last zoom
559         unzoom();
560 
561         if (dest != null) {
562             zoom.execute(dest);
563         }
564     }
565 
566     /**
567      * Listener who define the active leaf and set the tooltip text.
568      *
569      * @author Laurent Dutheil
570      */
571     protected class HandleMouseMotion extends MouseMotionAdapter {
572         @Override
573         public void mouseMoved(final MouseEvent e) {
574             if (getDisplayedRoot().children().hasMoreElements()) {
575                 final TreeMapNode t = getDisplayedRoot().getActiveLeaf(e.getX(), e.getY());
576                 if (t == null || !t.equals(getActiveLeaf())) {
577                     setActiveLeaf(t);
578                     repaint();
579                 }
580             }
581         }
582 
583     }
584 
585     /**
586      * Listener which listens for double click to navigate one level down.
587      *
588      * @author Ekin Gulen
589      */
590     protected class HandleMouseClick extends MouseAdapter {
591         @Override
592         public void mouseClicked(final MouseEvent e) {
593             if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 1) {
594                 final TreeMapNode t = getDisplayedRoot().getChild(e.getX(), e.getY());
595                 if (t != null && !t.isLeaf()) {
596                     if (treeView == null) {
597                         zoom(t);
598                     } else {
599                         final TreePath path = new TreePath(t.getPath());
600                         treeView.setSelectionPath(path);
601                         treeView.scrollPathToVisible(path);
602                     }
603 
604                 } else {
605                     if (treeView == null) {
606                         zoom((TreeMapNode) getDisplayedRoot().getParent());
607                     } else {
608                         final TreePath path = new TreePath(((TreeMapNode) getDisplayedRoot().getParent()).getPath());
609                         treeView.setSelectionPath(path);
610                         treeView.scrollPathToVisible(path);
611                     }
612                 }
613                 repaint();
614             }
615         }
616 
617         @Override
618         public void mouseExited(final MouseEvent arg0) {
619             setActiveLeaf(null);
620             repaint();
621         }
622     }
623 
624     /**
625      * Class who zoom and unzoom the JTreeMap.
626      *
627      * @author Laurent Dutheil
628      */
629     private class Zoom implements Serializable {
630         private static final long serialVersionUID = 6708828099608367996L;
631         private boolean enable;
632         private boolean keepProportion = false;
633 
634         public Zoom() {
635             enable = true;
636         }
637 
638         /**
639          * Execute the zoom.
640          *
641          * @param dest
642          *            TreeMapNode where you want to zoom
643          */
644         public void execute(final TreeMapNode dest) {
645             if (this.enable) {
646                 JTreeMap.this.setActiveLeaf(null);
647 
648                 setNewDimension(dest);
649 
650                 JTreeMap.this.setDisplayedRoot(dest);
651 
652                 enable = false;
653             }
654         }
655 
656         /**
657          * @return Returns the keepProportion.
658          */
659         public boolean isKeepProportion() {
660             return keepProportion;
661         }
662 
663         /**
664          * @param keepProportion
665          *            The keepProportion to set.
666          */
667         public void setKeepProportion(final boolean keepProportion) {
668             this.keepProportion = keepProportion;
669         }
670 
671         /**
672          * set the new dimensions of the dest root
673          *
674          * @param dest
675          *            the root to dimension
676          */
677         protected void setNewDimension(final TreeMapNode dest) {
678             if (dest == null) {
679                 return;
680             }
681             dest.setX(JTreeMap.this.getRoot().getX());
682             dest.setY(JTreeMap.this.getRoot().getY());
683 
684             final int rootWidth = JTreeMap.this.getRoot().getWidth();
685             final int rootHeight = JTreeMap.this.getRoot().getHeight();
686 
687             if (isKeepProportion()) {
688                 final int destHeight = dest.getHeight();
689                 final int destWidth = dest.getWidth();
690                 final float divWidth = (float) destWidth / (float) rootWidth;
691                 final float divHeight = (float) destHeight / (float) rootHeight;
692 
693                 if (divWidth >= divHeight) {
694                     dest.setHeight(Math.round(destHeight / divWidth));
695                     dest.setWidth(rootWidth);
696                 } else {
697                     dest.setHeight(rootHeight);
698                     dest.setWidth(Math.round(destWidth / divHeight));
699                 }
700 
701             } else {
702                 dest.setHeight(rootHeight);
703                 dest.setWidth(rootWidth);
704             }
705         }
706 
707         /**
708          * undo the zoom.
709          */
710         public void undo() {
711             if (!this.enable) {
712                 JTreeMap.this.setDisplayedRoot(JTreeMap.this.getRoot());
713                 enable = true;
714             }
715         }
716     }
717 
718     public JTree getTreeView() {
719         return treeView;
720     }
721 
722     public void setTreeView(final JTree treeView) {
723         this.treeView = treeView;
724     }
725 }
726 /*
727  *                 ObjectLab is supporing JTreeMap
728  *
729  * Based in London, we are world leaders in the design and development
730  * of bespoke applications for the securities financing markets.
731  *
732  * <a href="http://www.objectlab.co.uk/open">Click here to learn more about us</a>
733  *           ___  _     _           _   _          _
734  *          / _ \| |__ (_) ___  ___| |_| |    __ _| |__
735  *         | | | | '_ \| |/ _ \/ __| __| |   / _` | '_ \
736  *         | |_| | |_) | |  __/ (__| |_| |__| (_| | |_) |
737  *          \___/|_.__// |\___|\___|\__|_____\__,_|_.__/
738  *                   |__/
739  *
740  *                     www.ObjectLab.co.uk
741  */