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