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