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