001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.awt.Color; 005import java.awt.FontMetrics; 006import java.awt.Graphics2D; 007 008/** 009 * Utility class that helps to work with color scale for coloring GPX tracks etc. 010 * @since 7319 011 */ 012public final class ColorScale { 013 private double min, max; 014 private Color noDataColor; 015 private Color belowMinColor; 016 private Color aboveMaxColor; 017 018 private Color[] colors; 019 private String title = ""; 020 private int intervalCount = 5; 021 022 private ColorScale() { 023 024 } 025 026 /** 027 * Gets a fixed color range. 028 * @param colors the fixed colors list 029 * @return The scale 030 * @since 15247 031 */ 032 public static ColorScale createFixedScale(Color[] colors) { 033 ColorScale sc = new ColorScale(); 034 sc.colors = Utils.copyArray(colors); 035 sc.setRange(0, colors.length - 1d); 036 sc.addBounds(); 037 return sc; 038 } 039 040 /** 041 * Gets a HSB color range. 042 * @param count The number of colors the scale should have 043 * @return The scale 044 */ 045 public static ColorScale createHSBScale(int count) { 046 ColorScale sc = new ColorScale(); 047 sc.colors = new Color[count]; 048 for (int i = 0; i < count; i++) { 049 sc.colors[i] = Color.getHSBColor(i / 300.0f, 1, 1); 050 } 051 sc.setRange(0, 255); 052 sc.addBounds(); 053 return sc; 054 } 055 056 /** 057 * Creates a cyclic color scale (red yellow green blue red) 058 * @param count The number of colors the scale should have 059 * @return The scale 060 */ 061 public static ColorScale createCyclicScale(int count) { 062 ColorScale sc = new ColorScale(); 063 // CHECKSTYLE.OFF: SingleSpaceSeparator 064 // red yellow green blue red 065 int[] h = new int[] {0, 59, 127, 244, 360}; 066 int[] s = new int[] {100, 84, 99, 100}; 067 int[] b = new int[] {90, 93, 74, 83}; 068 // CHECKSTYLE.ON: SingleSpaceSeparator 069 070 sc.colors = new Color[count]; 071 for (int i = 0; i < sc.colors.length; i++) { 072 073 float angle = i / 256f * 4; 074 int quadrant = (int) angle; 075 angle -= quadrant; 076 quadrant = Utils.mod(quadrant+1, 4); 077 078 float vh = h[quadrant] * weighted(angle) + h[quadrant+1] * (1 - weighted(angle)); 079 float vs = s[quadrant] * weighted(angle) + s[Utils.mod(quadrant+1, 4)] * (1 - weighted(angle)); 080 float vb = b[quadrant] * weighted(angle) + b[Utils.mod(quadrant+1, 4)] * (1 - weighted(angle)); 081 082 sc.colors[i] = Color.getHSBColor(vh/360f, vs/100f, vb/100f); 083 } 084 sc.setRange(0, 2*Math.PI); 085 sc.addBounds(); 086 return sc; 087 } 088 089 /** 090 * transition function: 091 * w(0)=1, w(1)=0, 0<=w(x)<=1 092 * @param x number: 0<=x<=1 093 * @return the weighted value 094 */ 095 private static float weighted(float x) { 096 if (x < 0.5) 097 return 1 - 2*x*x; 098 else 099 return 2*(1-x)*(1-x); 100 } 101 102 /** 103 * Sets the hint on the range this scale is for 104 * @param min The minimum value 105 * @param max The maximum value 106 */ 107 public void setRange(double min, double max) { 108 this.min = min; 109 this.max = max; 110 } 111 112 /** 113 * Add standard colors for values below min or above max value 114 */ 115 public void addBounds() { 116 aboveMaxColor = colors[colors.length-1]; 117 belowMinColor = colors[0]; 118 } 119 120 /** 121 * Gets a color for the given value. 122 * @param value The value 123 * @return The color for this value, this may be a special color if the value is outside the range but never null. 124 */ 125 public Color getColor(double value) { 126 if (value < min) return belowMinColor; 127 if (value > max) return aboveMaxColor; 128 if (Double.isNaN(value)) return noDataColor; 129 final int n = colors.length; 130 int idx = (int) ((value-min)*colors.length / (max-min)); 131 if (idx < colors.length) { 132 return colors[idx]; 133 } else { 134 return colors[n-1]; // this happens when value==max 135 } 136 } 137 138 /** 139 * Gets a color for the given value. 140 * @param value The value, may be <code>null</code> 141 * @return The color for this value, this may be a special color if the value is outside the range or the value is null but never null. 142 */ 143 public Color getColor(Number value) { 144 return (value == null) ? noDataColor : getColor(value.doubleValue()); 145 } 146 147 /** 148 * Get the color to use if there is no data 149 * @return The color 150 */ 151 public Color getNoDataColor() { 152 return noDataColor; 153 } 154 155 /** 156 * Sets the color to use if there is no data 157 * @param noDataColor The color 158 */ 159 public void setNoDataColor(Color noDataColor) { 160 this.noDataColor = noDataColor; 161 } 162 163 /** 164 * Make all colors transparent 165 * @param alpha The alpha value all colors in the range should have, range 0..255 166 * @return This scale, for chaining 167 */ 168 public ColorScale makeTransparent(int alpha) { 169 for (int i = 0; i < colors.length; i++) { 170 colors[i] = new Color((colors[i].getRGB() & 0xFFFFFF) | ((alpha & 0xFF) << 24), true); 171 } 172 return this; 173 } 174 175 /** 176 * Adds a title to this scale 177 * @param title The new title 178 * @return This scale, for chaining 179 */ 180 public ColorScale addTitle(String title) { 181 this.title = title; 182 return this; 183 } 184 185 /** 186 * Sets the interval count for this scale 187 * @param intervalCount The interval count hint 188 * @return This scale, for chaining 189 */ 190 public ColorScale setIntervalCount(int intervalCount) { 191 this.intervalCount = intervalCount; 192 return this; 193 } 194 195 /** 196 * Reverses this scale 197 * @return This scale, for chaining 198 */ 199 public ColorScale makeReversed() { 200 int n = colors.length; 201 Color tmp; 202 for (int i = 0; i < n/2; i++) { 203 tmp = colors[i]; 204 colors[i] = colors[n-1-i]; 205 colors[n-1-i] = tmp; 206 } 207 tmp = belowMinColor; 208 belowMinColor = aboveMaxColor; 209 aboveMaxColor = tmp; 210 return this; 211 } 212 213 /** 214 * Draws a color bar representing this scale on the given graphics 215 * @param g The graphics to draw on 216 * @param x Rect x 217 * @param y Rect y 218 * @param w Rect width 219 * @param h Rect height 220 * @param valueScale The scale factor of the values 221 */ 222 public void drawColorBar(Graphics2D g, int x, int y, int w, int h, double valueScale) { 223 int n = colors.length; 224 225 for (int i = 0; i < n; i++) { 226 g.setColor(colors[i]); 227 if (w < h) { 228 g.fillRect(x, y+i*h/n, w, h/n+1); 229 } else { 230 g.fillRect(x+i*w/n, y, w/n+1, h); 231 } 232 } 233 234 int fw, fh; 235 FontMetrics fm = g.getFontMetrics(); 236 fh = fm.getHeight()/2; 237 fw = fm.stringWidth(String.valueOf(Math.max((int) Math.abs(max*valueScale), 238 (int) Math.abs(min*valueScale)))) + fm.stringWidth("0.123"); 239 g.setColor(noDataColor); 240 if (title != null) { 241 g.drawString(title, x-fw-3, y-fh*3/2); 242 } 243 for (int i = 0; i <= intervalCount; i++) { 244 g.setColor(colors[(int) (1.0*i*n/intervalCount-1e-10)]); 245 final double val = min+i*(max-min)/intervalCount; 246 final String txt = String.format("%.3f", val*valueScale); 247 if (w < h) { 248 g.drawString(txt, x-fw-3, y+i*h/intervalCount+fh/2); 249 } else { 250 g.drawString(txt, x+i*w/intervalCount-fw/2, y+fh-3); 251 } 252 } 253 } 254}