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