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