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}