View Javadoc
1   package org.opentrafficsim.draw;
2   
3   import java.awt.Color;
4   import java.io.Serializable;
5   import java.util.Arrays;
6   
7   import org.djutils.exceptions.Throw;
8   import org.djutils.logger.CategoryLogger;
9   
10  /**
11   * Paint scale interpolating between colors at values.
12   * <p>
13   * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
14   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
15   * </p>
16   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
17   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
18   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
19   */
20  public class BoundsPaintScale implements ColorPaintScale, Serializable
21  {
22  
23      /** 3-color scale from green to red. */
24      public static final Color[] GREEN_RED = new Color[] {Color.GREEN, Color.YELLOW, Color.RED};
25  
26      /** 5-color scale from green to red with dark edges. */
27      public static final Color[] GREEN_RED_DARK =
28              new Color[] {Color.GREEN.darker(), Color.GREEN, Color.YELLOW, Color.RED, Color.RED.darker()};
29  
30      /** */
31      private static final long serialVersionUID = 20181008L;
32  
33      /** Boundary values for this ColorPaintScale. */
34      private double[] bounds;
35  
36      /** Color values to use at the boundary values. */
37      private Color[] boundColors;
38  
39      /**
40       * Constructor.
41       * @param bounds double[]; value bounds
42       * @param boundColors Color[]; colors at bounds
43       * @throws IllegalArgumentException if less than 2 bounds, unequal number of bounds and colors, or duplicate bounds
44       */
45      public BoundsPaintScale(final double[] bounds, final Color[] boundColors) throws IllegalArgumentException
46      {
47          Throw.when(bounds.length < 2, IllegalArgumentException.class, "bounds must have >= 2 entries");
48          Throw.when(bounds.length != boundColors.length, IllegalArgumentException.class,
49                  "bounds must have same length as boundColors");
50          this.bounds = new double[bounds.length];
51          this.boundColors = new Color[bounds.length];
52          // Store the bounds and boundColors in order of increasing bound value.
53          // This is as inefficient as bubble sorting, but we're dealing with only a few values here.
54          for (int nextBound = 0; nextBound < bounds.length; nextBound++)
55          {
56              // Find the lowest not-yet used bound
57              double currentLowest = Double.POSITIVE_INFINITY;
58              int bestIndex = -1;
59              int index;
60              for (index = 0; index < bounds.length; index++)
61              {
62                  if (bounds[index] < currentLowest && (nextBound == 0 || bounds[index] > this.bounds[nextBound - 1]))
63                  {
64                      bestIndex = index;
65                      currentLowest = bounds[index];
66                  }
67              }
68              Throw.when(bestIndex < 0, IllegalArgumentException.class, "duplicate value in bounds");
69              this.bounds[nextBound] = bounds[bestIndex];
70              this.boundColors[nextBound] = boundColors[bestIndex];
71          }
72      }
73  
74      /**
75       * Reverses the color array.
76       * @param colors Color[]; array of colors
77       * @return Color[]; reversed color array
78       */
79      public static Color[] reverse(final Color[] colors)
80      {
81          Color[] out = new Color[colors.length];
82          for (int i = 0; i < colors.length; i++)
83          {
84              out[colors.length - i - 1] = colors[i];
85          }
86          return out;
87      }
88  
89      /**
90       * Creates an array of {@code n} colors with varying hue.
91       * @param n int; number of colors.
92       * @return Color[]; array of {@code n} colors with varying hue
93       */
94      public static Color[] hue(final int n)
95      {
96          Color[] out = new Color[n];
97          for (int i = 0; i < n; i++)
98          {
99              out[i] = new Color(Color.HSBtoRGB(((float) i) / n, 1.0f, 1.0f));
100         }
101         return out;
102     }
103 
104     /** {@inheritDoc} */
105     @Override
106     public Color getPaint(final double value)
107     {
108         if (Double.isNaN(value))
109         {
110             return Color.BLACK;
111         }
112         if (value < this.bounds[0])
113         {
114             return this.boundColors[0];
115         }
116         if (value > this.bounds[this.bounds.length - 1])
117         {
118             return this.boundColors[this.bounds.length - 1];
119         }
120         int index;
121         for (index = 0; index < this.bounds.length - 1; index++)
122         {
123             if (value < this.bounds[index + 1])
124             {
125                 break;
126             }
127         }
128         final double ratio;
129         if (index >= this.bounds.length - 1)
130         {
131             index = this.bounds.length - 2;
132             ratio = 1.0;
133         }
134         else
135         {
136             ratio = (value - this.bounds[index]) / (this.bounds[index + 1] - this.bounds[index]);
137         }
138         if (Double.isInfinite(ratio))
139         {
140             CategoryLogger.always().error("Interpolation value for color is infinite based on {} in {} obtaining index {}.",
141                     value, this.bounds, index);
142         }
143         Color mix = ColorInterpolator.interpolateColor(this.boundColors[index], this.boundColors[index + 1], ratio);
144         return mix;
145     }
146 
147     /** {@inheritDoc} */
148     @Override
149     public final double getLowerBound()
150     {
151         return this.bounds[0];
152     }
153 
154     /** {@inheritDoc} */
155     @Override
156     public final double getUpperBound()
157     {
158         return this.bounds[this.bounds.length - 1];
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public String toString()
164     {
165         return "BoundsPaintScale [bounds=" + Arrays.toString(this.bounds) + ", boundColors=" + Arrays.toString(this.boundColors)
166                 + "]";
167     }
168 
169 }