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&lt;=w(x)&lt;=1
078     * @param x number: 0&lt;=x&lt;=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}