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;
022
023import org.conqat.lib.commons.color.MultiColor;
024
025/**
026 * A tree map renderer using "cushions" as described in J. van Wijk, H. van de
027 * Wetering: "Cushion Treemaps: Visualization of Hierarchical Information".
028 * 
029 * @author Benjamin Hummel
030 */
031public class CushionTreeMapRenderer implements ITreeMapRenderer {
032
033        /** The height parameter for the cushions. */
034        private final double h;
035
036        /** The height scale factor. */
037        private final double f;
038
039        /**
040         * Constructor.
041         * 
042         * @param h
043         *            the height parameter giving the heigt of the cushions relative to
044         *            their size. 0.5 seems to be a reasonable value.
045         * @param f
046         *            the scale factor used to reduce the heights of nested cushions.
047         *            The value should be between 0 and 1, where smaller values will
048         *            reduce the cushion effect.
049         */
050        public CushionTreeMapRenderer(double h, double f) {
051                this.h = h;
052                this.f = f;
053        }
054
055        /** {@inheritDoc} */
056        @Override
057        public <T> void renderTreeMap(ITreeMapNode<T> node, Graphics2D graphics) {
058                // use loop here, to avoid adding cushion to top level node
059                for (ITreeMapNode<T> child : node.getChildren()) {
060                        render(child, graphics, h, new double[4]);
061                }
062        }
063
064        /**
065         * Renders the given node.
066         * 
067         * @param node
068         *            the node to render.
069         * @param g
070         *            the graphics to render into.
071         * @param height
072         *            the current height (already scaled for this level).
073         * @param coefs
074         *            the coefficients of the local parabola. The indices 0 and 1 give
075         *            the coefficients for x^2 and x, while 2 and 3 are for y^2 and y.
076         *            The constant part is not needed.
077         */
078        private <T> void render(ITreeMapNode<T> node, Graphics2D g, double height, double[] coefs) {
079                Rectangle2D rect = node.getLayoutRectangle();
080                if (rect == null) {
081                        return;
082                }
083
084                double[] myCoefs = addLocalParabola(height, coefs, rect);
085                if (node.getChildren().isEmpty()) {
086                        renderCushion(rect, myCoefs, g, node.getColor(), node.getPatternColor(), node.getDrawingPattern());
087                } else if (node.getChildren().size() == 1) {
088                        // do not scale height or add cushion
089                        render(node.getChildren().get(0), g, height, coefs);
090                } else {
091                        for (ITreeMapNode<T> child : node.getChildren()) {
092                                render(child, g, height * f, myCoefs);
093                        }
094                }
095        }
096
097        /** Adds the local parabola to the given coefs and returns the result. */
098        private static double[] addLocalParabola(double height, double[] coefs, Rectangle2D rect) {
099                double[] myCoefs = new double[4];
100                double x1 = rect.getMinX();
101                double x2 = rect.getMaxX();
102                double y1 = rect.getMinY();
103                double y2 = rect.getMaxY();
104                myCoefs[0] = coefs[0] - 4 * height / (x2 - x1);
105                myCoefs[1] = coefs[1] + 4 * height * (x1 + x2) / (x2 - x1);
106                myCoefs[2] = coefs[2] - 4 * height / (y2 - y1);
107                myCoefs[3] = coefs[3] + 4 * height * (y1 + y2) / (y2 - y1);
108                return myCoefs;
109        }
110
111        /** Renders the given cushion. */
112        private static void renderCushion(Rectangle2D rect, double[] coefs, Graphics2D g, Color baseColor,
113                        Color patternColor, IDrawingPattern drawingPattern) {
114
115                // light normal taken from the cited paper.
116                final double lx = 0.09759;
117                final double ly = 0.19518;
118                final double lz = 0.9759;
119
120                int minX = (int) (rect.getMinX() + .5);
121                int minY = (int) (rect.getMinY() + .5);
122                int maxX = (int) (rect.getMaxX() + .5);
123                int maxY = (int) (rect.getMaxY() + .5);
124
125                for (int x = minX; x < maxX; ++x) {
126                        for (int y = minY; y < maxY; ++y) {
127                                double nx = -(2 * coefs[0] * (x + .5) + coefs[1]);
128                                double ny = -(2 * coefs[2] * (y + .5) + coefs[3]);
129                                double norm = Math.sqrt(nx * nx + ny * ny + 1);
130                                double cosa = (nx * lx + ny * ly + lz) / norm;
131
132                                Color color = determineBaseColor(x, y, baseColor, patternColor, drawingPattern, rect);
133
134                                g.setColor(shadeColor(color, .2 + .8 * Math.max(0, cosa)));
135                                g.drawLine(x, y, x, y);
136                        }
137                }
138        }
139
140        /** Determines the base color to be used for a given pixel. */
141        private static Color determineBaseColor(int x, int y, Color baseColor, Color patternColor,
142                        IDrawingPattern drawingPattern, Rectangle2D rect) {
143                if (drawingPattern != null && drawingPattern.isForeground(x, y)) {
144                        return resolveMultiColor(x, y, rect, patternColor);
145                }
146                return resolveMultiColor(x, y, rect, baseColor);
147        }
148
149        /**
150         * Resolves the pixel color with special handling for multi color. The colors
151         * are arranged in a striped pattern, which is arranged horizontally or
152         * vertically depending on the aspect ratio of the rectangle.
153         */
154        private static Color resolveMultiColor(int x, int y, Rectangle2D rect, Color color) {
155                if (!(color instanceof MultiColor)) {
156                        return color;
157                }
158
159                MultiColor multiColor = (MultiColor) color;
160                double relative;
161                if (rect.getWidth() > rect.getHeight()) {
162                        relative = (x - rect.getX()) / rect.getWidth();
163                } else {
164                        relative = (y - rect.getY()) / rect.getHeight();
165                }
166
167                double current = 0;
168                for (int i = 0; i < multiColor.size(); ++i) {
169                        current += multiColor.getRelativeFrequency(i);
170                        if (current > relative) {
171                                return multiColor.getColor(i);
172                        }
173                }
174
175                // can only be reached in case of rounding errors
176                return multiColor.getColor(multiColor.size() - 1);
177        }
178
179        /**
180         * Calculate the shaded color.
181         * 
182         * @param color
183         *            the base color.
184         * @param luminance
185         *            a parameter between 0 and 1, where 0 corresponds to black and 1 to
186         *            white.
187         */
188        private static Color shadeColor(Color color, double luminance) {
189                int base = 0;
190                luminance *= 2;
191                if (luminance > 1) {
192                        luminance = 2 - luminance;
193                        base = (int) (255 * (1 - luminance));
194                }
195
196                return new Color((int) (color.getRed() * luminance) + base, (int) (color.getGreen() * luminance) + base,
197                                (int) (color.getBlue() * luminance) + base);
198        }
199}