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