View Javadoc
1   package org.opentrafficsim.core.geometry;
2   
3   import java.util.Map;
4   import java.util.Map.Entry;
5   import java.util.NavigableMap;
6   import java.util.TreeMap;
7   
8   import org.djutils.exceptions.Throw;
9   import org.djutils.immutablecollections.ImmutableLinkedHashSet;
10  import org.djutils.immutablecollections.ImmutableNavigableSet;
11  import org.djutils.immutablecollections.ImmutableSet;
12  import org.djutils.immutablecollections.ImmutableTreeSet;
13  import org.opentrafficsim.core.geometry.ContinuousLine.ContinuousDoubleFunction;
14  
15  /**
16   * Container for fractional length data.
17   * <p>
18   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
19   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
20   * </p>
21   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
22   */
23  public class FractionalLengthData implements ContinuousDoubleFunction
24  {
25  
26      /** Underlying data. */
27      private final NavigableMap<Double, Double> data = new TreeMap<>();
28  
29      /**
30       * Create FractionalLengthData.
31       * @param data fractional length - value pairs. Fractional lengths do not need to be in order.
32       * @throws IllegalArgumentException when the number of input values is not even or 0.
33       * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
34       */
35      public FractionalLengthData(final double... data) throws IllegalArgumentException
36      {
37          Throw.when(data.length < 2 || data.length % 2 > 0, IllegalArgumentException.class,
38                  "Number of input values must be even and at least 2.");
39          for (int i = 0; i < data.length; i = i + 2)
40          {
41              Throw.when(data[i] < 0.0 || data[i] > 1.0, IllegalArgumentException.class,
42                      "Fractional length %s is outside of range [0 ... 1].", data[i]);
43              this.data.put(data[i], data[i + 1]);
44          }
45      }
46  
47      /**
48       * Create FractionalLengthData.
49       * @param data fractional length - value pairs. Fractional lengths do not need to be in order.
50       * @throws IllegalArgumentException when the input data is null or empty.
51       * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
52       */
53      public FractionalLengthData(final Map<Double, Double> data) throws IllegalArgumentException
54      {
55          Throw.when(data == null || data.isEmpty(), IllegalArgumentException.class, "Input data is empty or null.");
56          for (Entry<Double, Double> entry : data.entrySet())
57          {
58              Throw.when(entry.getKey() < 0.0 || entry.getKey() > 1.0, IllegalArgumentException.class,
59                      "Fractional length %s is outside of range [0 ... 1].", entry.getKey());
60              this.data.put(entry.getKey(), entry.getValue());
61          }
62      }
63  
64      /**
65       * Returns the data at given fractional length. If only data beyond the fractional length is available, the first available
66       * value is returned. If only data before the fractional length is available, the last available value is returned.
67       * Otherwise data is linearly interpolated.
68       * @param fractionalLength fractional length, may be outside range [0 ... 1].
69       * @return interpolated or extended value.
70       */
71      @Override
72      public Double apply(final Double fractionalLength)
73      {
74          Double exact = this.data.get(fractionalLength);
75          if (exact != null)
76          {
77              return exact;
78          }
79          Entry<Double, Double> ceiling = this.data.ceilingEntry(fractionalLength);
80          if (ceiling == null)
81          {
82              return this.data.lastEntry().getValue();
83          }
84          Entry<Double, Double> floor = this.data.floorEntry(fractionalLength);
85          if (floor == null)
86          {
87              return this.data.firstEntry().getValue();
88          }
89          double w = (fractionalLength - floor.getKey()) / (ceiling.getKey() - floor.getKey());
90          return (1.0 - w) * floor.getValue() + w * ceiling.getValue();
91      }
92  
93      /**
94       * Returns the derivative of the data with respect to fractional length.
95       * @param fractionalLength fractional length, may be outside range [0 ... 1].
96       * @return derivative of the data with respect to fractional length.
97       */
98      @Override
99      public double getDerivative(final double fractionalLength)
100     {
101         Entry<Double, Double> ceiling, floor;
102         if (fractionalLength == 0.0)
103         {
104             ceiling = this.data.higherEntry(fractionalLength);
105             floor = this.data.floorEntry(fractionalLength);
106         }
107         else
108         {
109             ceiling = this.data.ceilingEntry(fractionalLength);
110             floor = this.data.lowerEntry(fractionalLength);
111         }
112         if (ceiling == null || floor == null)
113         {
114             return 0.0;
115         }
116         return (ceiling.getValue() - floor.getValue()) / (ceiling.getKey() - floor.getKey());
117     }
118 
119     /**
120      * Returns the fractional lengths in the underlying data.
121      * @return fractional lengths in the underlying data.
122      */
123     public ImmutableNavigableSet<Double> getFractionalLengths()
124     {
125         return new ImmutableTreeSet<>(this.data.keySet());
126     }
127 
128     /**
129      * Returns the values in the underlying data.
130      * @return values in the underlying data.
131      */
132     public ImmutableSet<Double> getValues()
133     {
134         return new ImmutableLinkedHashSet<>(this.data.values());
135     }
136 
137     /**
138      * Returns fractional lengths in array form, including 0.0 and 1.0.
139      * @return fractional lengths.
140      */
141     @Override
142     public double[] getKnots()
143     {
144         NavigableMap<Double, Double> full = fullRange();
145         double[] fractionalLengths = new double[full.size()];
146         int i = 0;
147         for (double f : full.navigableKeySet())
148         {
149             fractionalLengths[i++] = f;
150         }
151         return fractionalLengths;
152     }
153 
154     /**
155      * Returns fractional lengths in array form, including values at 0.0 and 1.0.
156      * @return fractional lengths.
157      */
158     public double[] getValuesAsArray()
159     {
160         NavigableMap<Double, Double> full = fullRange();
161         double[] values = new double[full.size()];
162         int i = 0;
163         for (double f : full.navigableKeySet())
164         {
165             values[i++] = full.get(f);
166         }
167         return values;
168     }
169 
170     /**
171      * Returns the data including entries at 0.0 and 1.0.
172      * @return data with fill range.
173      */
174     private NavigableMap<Double, Double> fullRange()
175     {
176         NavigableMap<Double, Double> full = new TreeMap<>(this.data);
177         full.put(0.0, full.firstEntry().getValue());
178         full.put(1.0, full.lastEntry().getValue());
179         return full;
180     }
181 
182     /**
183      * Returns the number of data points.
184      * @return number of data points.
185      */
186     public int size()
187     {
188         return this.data.size();
189     }
190 
191     /**
192      * Create FractionalLengthData.
193      * @param data fractional length - value pairs. Fractional lengths do not need to be in order.
194      * @return fractional length data.
195      * @throws IllegalArgumentException when the number of input values is not even or 0.
196      * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
197      */
198     public static FractionalLengthData of(final double... data) throws IllegalArgumentException
199     {
200         return new FractionalLengthData(data);
201     }
202 
203 }