View Javadoc
1   package org.djutils.draw.function;
2   
3   import java.util.Iterator;
4   import java.util.Map;
5   import java.util.Map.Entry;
6   import java.util.NavigableMap;
7   import java.util.NoSuchElementException;
8   import java.util.TreeMap;
9   
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * Container for piece-wise linear offsets, defined by the offsets at particular fractional positions.
14   * <p>
15   * Copyright (c) 2023-2025 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
17   * </p>
18   * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
19   * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
20   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
21   */
22  public class ContinuousPiecewiseLinearFunction
23          implements Iterable<org.djutils.draw.function.ContinuousPiecewiseLinearFunction.TupleSt>
24  {
25  
26      /** The underlying data. */
27      private final NavigableMap<Double, Double> data = new TreeMap<>();
28  
29      /**
30       * Create ContinuousPiecewiseLinearFunction from an array of double values.
31       * @param data fractional length - value pairs. Fractional lengths do not need to be in order
32       * @throws NullPointerException when <code>data</code> is <code>null</code>
33       * @throws IllegalArgumentException when the number of input values is not even or 0, or a fractional value is not in the
34       *             range [0, 1], or an offset value is not finite, or multiple values are provided for the same fraction
35       */
36      public ContinuousPiecewiseLinearFunction(final double... data)
37      {
38          Throw.when(data.length < 2 || data.length % 2 > 0, IllegalArgumentException.class,
39                  "Number of input values must be even and at least 2");
40          for (int i = 0; i < data.length; i = i + 2)
41          {
42              Throw.when(data[i] < 0.0 || data[i] > 1.0, IllegalArgumentException.class,
43                      "Fractional length %f is outside of range [0 ... 1]", data[i]);
44              Throw.when(1 / data[0] < 0, IllegalArgumentException.class, "Fractional length data may not contain -0.0 fraction");
45              Throw.when(!Double.isFinite(data[i + 1]), IllegalArgumentException.class, "values must be finite (got %f)",
46                      data[i + 1]);
47              Throw.when(this.data.get(data[i]) != null, IllegalArgumentException.class, "Duplicate fraction is not permitted");
48              this.data.put(data[i], data[i + 1]);
49          }
50      }
51  
52      /**
53       * Create ContinuousPiecewiseLinearFunction from a Map of key-value pairs.
54       * @param data fractional length - value pairs. Fractional lengths do not need to be in order.
55       * @throws IllegalArgumentException when the input data is null or empty, or a fractional value is not in the range [0, 1],
56       *             or an offset value is not finite
57       */
58      public ContinuousPiecewiseLinearFunction(final Map<Double, Double> data)
59      {
60          Throw.whenNull(data, "data");
61          Throw.when(data.isEmpty(), IllegalArgumentException.class, "Input data is empty");
62          for (Entry<Double, Double> entry : data.entrySet())
63          {
64              Double key = entry.getKey();
65              Throw.whenNull(key, "key in provided data may not be null");
66              Throw.when(key < 0.0 || entry.getKey() > 1.0, IllegalArgumentException.class,
67                      "Fractional length %s is outside of range [0 ... 1].", entry.getKey());
68              Throw.when(1 / key < 0, IllegalArgumentException.class, "Fractional length data may not contain -0.0 fraction");
69              Double value = entry.getValue();
70              Throw.whenNull(value, "value in provided map may not be null");
71              Throw.when(!Double.isFinite(value), IllegalArgumentException.class, "values must be finite (got %f)", value);
72              this.data.put(key, value);
73          }
74      }
75  
76      /**
77       * Returns the data at given fractional length. If only data beyond the fractional length is available, the first available
78       * value is returned. If only data before the fractional length is available, the last available value is returned.
79       * Otherwise data is linearly interpolated.
80       * @param fractionalLength fractional length, may be outside range [0, 1].
81       * @return interpolated or extended value.
82       */
83      public double get(final double fractionalLength)
84      {
85          Double exact = this.data.get(fractionalLength);
86          if (exact != null)
87          {
88              return exact;
89          }
90          Entry<Double, Double> ceiling = this.data.ceilingEntry(fractionalLength);
91          if (ceiling == null)
92          {
93              return this.data.lastEntry().getValue();
94          }
95          Entry<Double, Double> floor = this.data.floorEntry(fractionalLength);
96          if (floor == null)
97          {
98              return this.data.firstEntry().getValue();
99          }
100         double w = (fractionalLength - floor.getKey()) / (ceiling.getKey() - floor.getKey());
101         return (1.0 - w) * floor.getValue() + w * ceiling.getValue();
102     }
103 
104     /**
105      * Returns the derivative of the data with respect to fractional length.
106      * @param fractionalLength fractional length, may be outside range [0, 1].
107      * @return derivative of the data with respect to fractional length.
108      */
109     public double getDerivative(final double fractionalLength)
110     {
111         Entry<Double, Double> ceiling, floor;
112         if (fractionalLength == 0.0)
113         {
114             ceiling = this.data.higherEntry(fractionalLength);
115             floor = this.data.floorEntry(fractionalLength);
116         }
117         else
118         {
119             ceiling = this.data.ceilingEntry(fractionalLength);
120             floor = this.data.lowerEntry(fractionalLength);
121         }
122         if (ceiling == null || floor == null)
123         {
124             return 0.0;
125         }
126         return (ceiling.getValue() - floor.getValue()) / (ceiling.getKey() - floor.getKey());
127     }
128 
129     /**
130      * Returns the number of data points.
131      * @return number of data points.
132      */
133     public int size()
134     {
135         return this.data.size();
136     }
137 
138     /**
139      * Create ContinuousPiecewiseLinearFunction.
140      * @param data fractional length - value pairs. Fractional lengths do not need to be in order.
141      * @return fractional length data.
142      * @throws IllegalArgumentException when the number of input values is not even or 0, or when a fractional value is not in
143      *             the range [0, 1].
144      */
145     public static ContinuousPiecewiseLinearFunction of(final double... data)
146     {
147         return new ContinuousPiecewiseLinearFunction(data);
148     }
149 
150     @Override
151     public Iterator<TupleSt> iterator()
152     {
153         return new Iterator<TupleSt>()
154         {
155             private Entry<Double, Double> nextEntry = ContinuousPiecewiseLinearFunction.this.data.firstEntry();
156 
157             @Override
158             public boolean hasNext()
159             {
160                 return this.nextEntry != null;
161             }
162 
163             @Override
164             public TupleSt next()
165             {
166                 Throw.when(null == this.nextEntry, NoSuchElementException.class, "Iterator is exhausted");
167                 TupleSt result = new TupleSt(this.nextEntry.getKey(), this.nextEntry.getValue());
168                 this.nextEntry = ContinuousPiecewiseLinearFunction.this.data.higherEntry(result.s);
169                 return result;
170             }
171         };
172     }
173 
174     /**
175      * Wrapper for domain and function value pair.
176      * @param s value in range [0.0, 1.0]
177      * @param t value of the function for <code>s</code>
178      */
179     public record TupleSt(double s, double t)
180     {
181     }
182 }