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