1 package org.opentrafficsim.graphs;
2
3 import java.awt.Color;
4 import java.awt.Paint;
5 import java.io.Serializable;
6 import java.util.Arrays;
7
8 import org.jfree.chart.renderer.PaintScale;
9 import org.opentrafficsim.simulationengine.OTSSimulationException;
10
11 /**
12 * Create a continuous color paint scale. <br>
13 * Primarily intended for the contour plots, but sufficiently abstract for more general use. <br>
14 * A continuous color paint scale creates paints (actually simple Colors) by linearly interpolating between a limited set of RGB
15 * Color values that correspond to given input values.
16 * <p>
17 * Copyright (c) 2013-2016 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18 * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
19 * <p>
20 * $LastChangedDate: 2015-07-26 01:01:13 +0200 (Sun, 26 Jul 2015) $, @version $Revision: 1155 $, by $Author: averbraeck $,
21 * initial version Jul 30, 2014 <br>
22 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
23 */
24 public class ContinuousColorPaintScale implements PaintScale, Serializable
25 {
26 /** */
27 private static final long serialVersionUID = 20140000L;
28
29 /** Boundary values for this ColorPaintScale. */
30 private double[] bounds;
31
32 /** Color values to use at the boundary values. */
33 private Color[] boundColors;
34
35 /** Format string to render values in a human readable format (used in tool tip texts). */
36 private final String format;
37
38 /**
39 * Create a new ContinuousColorPaintScale.
40 * @param format Format string to render the value under the mouse in a human readable format
41 * @param bounds Double[] array of boundary values (all values must be distinct and the number of values must be >= 2)
42 * @param boundColors Color[] array of the colors to use at the boundary values (must have same size as bounds)
43 * @throws OTSSimulationException when bounds are wrong
44 */
45 ContinuousColorPaintScale(final String format, final double[] bounds, final Color[] boundColors)
46 throws OTSSimulationException
47 {
48 this.format = format;
49 if (bounds.length < 2)
50 {
51 throw new OTSSimulationException("bounds must have >= 2 entries");
52 }
53 if (bounds.length != boundColors.length)
54 {
55 throw new OTSSimulationException("bounds must have same length as boundColors");
56 }
57 this.bounds = new double[bounds.length];
58 this.boundColors = new Color[bounds.length];
59 // Store the bounds and boundColors in order of increasing bound value.
60 // This is as inefficient as bubble sorting, but we're dealing with only a few values here.
61 for (int nextBound = 0; nextBound < bounds.length; nextBound++)
62 {
63 // Find the lowest not-yet used bound
64 double currentLowest = Double.POSITIVE_INFINITY;
65 int bestIndex = -1;
66 int index;
67 for (index = 0; index < bounds.length; index++)
68 {
69 if (bounds[index] < currentLowest && (nextBound == 0 || bounds[index] > this.bounds[nextBound - 1]))
70 {
71 bestIndex = index;
72 currentLowest = bounds[index];
73 }
74 }
75 if (bestIndex < 0)
76 {
77 throw new OTSSimulationException("duplicate value in bounds");
78 }
79 this.bounds[nextBound] = bounds[bestIndex];
80 this.boundColors[nextBound] = boundColors[bestIndex];
81 }
82 }
83
84 /** {@inheritDoc} */
85 @Override
86 public final double getLowerBound()
87 {
88 return this.bounds[0];
89 }
90
91 /**
92 * Create a mixed color component. When ratio varies from 0.0 to 1.0, the result varies from <i>low</i> to <i>high</i>. If
93 * ratio is outside the range 0.0 to 1.0, the result value can be outside the <i>range</i> <i>low</i> to <i>high</i>.
94 * However, the result is always limited to the range 0..255.
95 * @param ratio Double; value (normally) between 0.0 and 1.0.
96 * @param low Integer; this value is returned when ratio equals 0.0
97 * @param high Integer; this value is returned when ratio equals 1.0
98 * @return Integer; the ratio-weighted average of <i>low</i> and <i>high</i>
99 */
100 private static int mixComponent(final double ratio, final int low, final int high)
101 {
102 final double mix = low * (1 - ratio) + high * ratio;
103 int result = (int) mix;
104 if (result < 0)
105 {
106 result = 0;
107 }
108 if (result > 255)
109 {
110 result = 255;
111 }
112 return result;
113 }
114
115 /** {@inheritDoc} */
116 @Override
117 public final Paint getPaint(final double value)
118 {
119 int bucket;
120 for (bucket = 0; bucket < this.bounds.length - 1; bucket++)
121 {
122 if (value < this.bounds[bucket + 1])
123 {
124 break;
125 }
126 }
127 if (bucket >= this.bounds.length - 1)
128 {
129 bucket = this.bounds.length - 2;
130 }
131 final double ratio = (value - this.bounds[bucket]) / (this.bounds[bucket + 1] - this.bounds[bucket]);
132 Color mix =
133 new Color(mixComponent(ratio, this.boundColors[bucket].getRed(), this.boundColors[bucket + 1].getRed()),
134 mixComponent(ratio, this.boundColors[bucket].getGreen(), this.boundColors[bucket + 1].getGreen()),
135 mixComponent(ratio, this.boundColors[bucket].getBlue(), this.boundColors[bucket + 1].getBlue()));
136 return mix;
137 }
138
139 /** {@inheritDoc} */
140 @Override
141 public final double getUpperBound()
142 {
143 return this.bounds[this.bounds.length - 1];
144 }
145
146 /**
147 * Retrieve the format string.
148 * @return format string
149 */
150 public final String getFormat()
151 {
152 return this.format;
153 }
154
155 /** {@inheritDoc} */
156 @Override
157 public final String toString()
158 {
159 return "ContinuousColorPaintScale [bounds=" + Arrays.toString(this.bounds) + ", boundColors="
160 + Arrays.toString(this.boundColors) + "]";
161 }
162
163 }