001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| You may obtain a copy of the License at                                  |
008|                                                                          |
009|    http://www.apache.org/licenses/LICENSE-2.0                            |
010|                                                                          |
011| Unless required by applicable law or agreed to in writing, software      |
012| distributed under the License is distributed on an "AS IS" BASIS,        |
013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
014| See the License for the specific language governing permissions and      |
015| limitations under the License.                                           |
016+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.treemap;
018
019import java.awt.Color;
020import java.awt.Graphics2D;
021import java.awt.geom.Rectangle2D;
022import java.util.regex.Pattern;
023
024/**
025 * A simple renderer that draws tree map node texts into the tree map.
026 * 
027 * @author juergens
028 */
029public class NodeTextRenderer implements ITreeMapRenderer {
030
031        /** Padding between a node's text label and its rectangle border */
032        private static final int TEXT_PADDING = 5;
033
034        /** Color in which text is drawn */
035        private final Color textColor;
036
037        /** Separation pattern used to isolate local name */
038        private final Pattern separationPattern;
039
040        /** Constructor */
041        public NodeTextRenderer(Color textColor, Pattern separationPattern) {
042                this.textColor = textColor;
043                this.separationPattern = separationPattern;
044        }
045
046        /** {@inheritDoc} */
047        @Override
048        public <T> void renderTreeMap(ITreeMapNode<T> node, Graphics2D graphics) {
049                if (node.getChildren().isEmpty()) {
050                        Rectangle2D nodeArea = node.getLayoutRectangle();
051                        if (nodeArea == null) {
052                                return;
053                        }
054                        if (enoughSpace(nodeArea)) {
055                                drawText(node.getText(), nodeArea, graphics);
056                        }
057                } else {
058                        for (ITreeMapNode<T> child : node.getChildren()) {
059                                renderTreeMap(child, graphics);
060                        }
061                }
062        }
063
064        /** Determines if node area is large enough */
065        private static boolean enoughSpace(Rectangle2D nodeArea) {
066                return nodeArea.getWidth() > 3 * TEXT_PADDING && nodeArea.getHeight() > 3 * TEXT_PADDING;
067        }
068
069        /** Draws the node text */
070        private void drawText(String text, Rectangle2D availableSpace, Graphics2D graphics) {
071
072                // cut text to size
073                String fittedText = clipTextToWidth(text, availableSpace.getWidth(), graphics);
074                if (fittedText.length() < text.length()) {
075                        fittedText += "...";
076                }
077
078                // compute text position
079                int x = (int) availableSpace.getCenterX() - (actualWidth(fittedText, graphics) / 2);
080                int y = (int) availableSpace.getCenterY() + (actualHeight(fittedText, graphics) / 2);
081
082                // draw label
083                graphics.setColor(textColor);
084                graphics.drawString(fittedText, x, y);
085        }
086
087        /** Clips a string to a certain width */
088        private String clipTextToWidth(String text, double width, Graphics2D graphics) {
089                double availableWidth = width - 2 * TEXT_PADDING;
090
091                // try to prune to last name part
092                if (separationPattern != null && actualWidth(text, graphics) > availableWidth) {
093                        String[] parts = separationPattern.split(text);
094                        if (parts.length > 0) {
095                                text = parts[parts.length - 1];
096                        }
097                }
098
099                // reserve space for trailing "..."
100                if (actualWidth(text, graphics) > availableWidth) {
101                        availableWidth -= actualWidth("...", graphics);
102                }
103
104                // clip until small enough
105                while (text.length() > 0 && actualWidth(text, graphics) > availableWidth) {
106                        text = text.substring(0, text.length() - 1);
107                }
108                return text;
109        }
110
111        /** Determines the width a string requires in the current font */
112        private static int actualWidth(String label, Graphics2D graphics) {
113                return (int) actualBounds(label, graphics).getWidth();
114        }
115
116        /** Determines the height a string requires in the current font */
117        private static int actualHeight(String label, Graphics2D graphics) {
118                return (int) actualBounds(label, graphics).getHeight();
119        }
120
121        /** Computes the bounds of a string in the current font */
122        private static Rectangle2D actualBounds(String label, Graphics2D graphics) {
123                return graphics.getFont().getStringBounds(label, graphics.getFontRenderContext());
124        }
125
126}