FractionalLengthData.java

  1. package org.opentrafficsim.core.geometry;

  2. import java.util.Map;
  3. import java.util.Map.Entry;
  4. import java.util.NavigableMap;
  5. import java.util.TreeMap;

  6. import org.djutils.exceptions.Throw;
  7. import org.djutils.immutablecollections.ImmutableLinkedHashSet;
  8. import org.djutils.immutablecollections.ImmutableNavigableSet;
  9. import org.djutils.immutablecollections.ImmutableSet;
  10. import org.djutils.immutablecollections.ImmutableTreeSet;

  11. /**
  12.  * Container for fractional length data.
  13.  * <p>
  14.  * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
  15.  * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
  16.  * </p>
  17.  * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
  18.  */
  19. public class FractionalLengthData
  20. {

  21.     /** Underlying data. */
  22.     private final NavigableMap<Double, Double> data = new TreeMap<>();

  23.     /**
  24.      * Create FractionalLengthData.
  25.      * @param data double...; fractional length - value pairs. Fractional lengths do not need to be in order.
  26.      * @throws IllegalArgumentException when the number of input values is not even or 0.
  27.      * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
  28.      */
  29.     public FractionalLengthData(final double... data) throws IllegalArgumentException
  30.     {
  31.         Throw.when(data.length < 2 || data.length % 2 > 0, IllegalArgumentException.class,
  32.                 "Number of input values must be even and at least 2.");
  33.         for (int i = 0; i < data.length; i = i + 2)
  34.         {
  35.             Throw.when(data[i] < 0.0 || data[i] > 1.0, IllegalArgumentException.class,
  36.                     "Fractional length %s is outside of range [0 ... 1].", data[i]);
  37.             this.data.put(data[i], data[i + 1]);
  38.         }
  39.     }

  40.     /**
  41.      * Create FractionalLengthData.
  42.      * @param data Map&lt;Double, Double&gt;; fractional length - value pairs. Fractional lengths do not need to be in order.
  43.      * @throws IllegalArgumentException when the input data is null or empty.
  44.      * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
  45.      */
  46.     public FractionalLengthData(final Map<Double, Double> data) throws IllegalArgumentException
  47.     {
  48.         Throw.when(data == null || data.isEmpty(), IllegalArgumentException.class, "Input data is empty or null.");
  49.         for (Entry<Double, Double> entry : data.entrySet())
  50.         {
  51.             Throw.when(entry.getKey() < 0.0 || entry.getKey() > 1.0, IllegalArgumentException.class,
  52.                     "Fractional length %s is outside of range [0 ... 1].", entry.getKey());
  53.             this.data.put(entry.getKey(), entry.getValue());
  54.         }
  55.     }

  56.     /**
  57.      * Returns the data at given fractional length. If only data beyond the fractional length is available, the first available
  58.      * value is returned. If only data before the fractional length is available, the last available value is returned.
  59.      * Otherwise data is linearly interpolated.
  60.      * @param fractionalLength double; fractional length, may be outside range [0 ... 1].
  61.      * @return double; interpolated or extended value.
  62.      */
  63.     public double get(final double fractionalLength)
  64.     {
  65.         Double exact = this.data.get(fractionalLength);
  66.         if (exact != null)
  67.         {
  68.             return exact;
  69.         }
  70.         Entry<Double, Double> ceiling = this.data.ceilingEntry(fractionalLength);
  71.         if (ceiling == null)
  72.         {
  73.             return this.data.lastEntry().getValue();
  74.         }
  75.         Entry<Double, Double> floor = this.data.floorEntry(fractionalLength);
  76.         if (floor == null)
  77.         {
  78.             return this.data.firstEntry().getValue();
  79.         }
  80.         double w = (fractionalLength - floor.getKey()) / (ceiling.getKey() - floor.getKey());
  81.         return (1.0 - w) * floor.getValue() + w * ceiling.getValue();
  82.     }

  83.     /**
  84.      * Returns the derivative of the data with respect to fractional length.
  85.      * @param fractionalLength double; fractional length, may be outside range [0 ... 1].
  86.      * @return double; derivative of the data with respect to fractional length.
  87.      */
  88.     public double getDerivative(final double fractionalLength)
  89.     {
  90.         Entry<Double, Double> ceiling, floor;
  91.         if (fractionalLength == 0.0)
  92.         {
  93.             ceiling = this.data.higherEntry(fractionalLength);
  94.             floor = this.data.floorEntry(fractionalLength);
  95.         }
  96.         else
  97.         {
  98.             ceiling = this.data.ceilingEntry(fractionalLength);
  99.             floor = this.data.lowerEntry(fractionalLength);
  100.         }
  101.         if (ceiling == null || floor == null)
  102.         {
  103.             return 0.0;
  104.         }
  105.         return (ceiling.getValue() - floor.getValue()) / (ceiling.getKey() - floor.getKey());
  106.     }

  107.     /**
  108.      * Returns the fractional lengths in the underlying data.
  109.      * @return ImmutableNavigableSet&lt;Double&gt;; fractional lengths in the underlying data.
  110.      */
  111.     public ImmutableNavigableSet<Double> getFractionalLengths()
  112.     {
  113.         return new ImmutableTreeSet<>(this.data.keySet());
  114.     }

  115.     /**
  116.      * Returns the values in the underlying data.
  117.      * @return ImmutableSet&lt;Double&gt;; values in the underlying data.
  118.      */
  119.     public ImmutableSet<Double> getValues()
  120.     {
  121.         return new ImmutableLinkedHashSet<>(this.data.values());
  122.     }

  123.     /**
  124.      * Returns fractional lengths in array form, including 0.0 and 1.0.
  125.      * @return double[]; fractional lengths.
  126.      */
  127.     public double[] getFractionalLengthsAsArray()
  128.     {
  129.         NavigableMap<Double, Double> full = fullRange();
  130.         double[] fractionalLengths = new double[full.size()];
  131.         int i = 0;
  132.         for (double f : full.navigableKeySet())
  133.         {
  134.             fractionalLengths[i++] = f;
  135.         }
  136.         return fractionalLengths;
  137.     }

  138.     /**
  139.      * Returns fractional lengths in array form, including values at 0.0 and 1.0.
  140.      * @return double[]; fractional lengths.
  141.      */
  142.     public double[] getValuesAsArray()
  143.     {
  144.         NavigableMap<Double, Double> full = fullRange();
  145.         double[] values = new double[full.size()];
  146.         int i = 0;
  147.         for (double f : full.navigableKeySet())
  148.         {
  149.             values[i++] = full.get(f);
  150.         }
  151.         return values;
  152.     }

  153.     /**
  154.      * Returns the data including entries at 0.0 and 1.0.
  155.      * @return NavigableMap&lt;Double, Double&gt;; data with fill range.
  156.      */
  157.     private final NavigableMap<Double, Double> fullRange()
  158.     {
  159.         NavigableMap<Double, Double> full = new TreeMap<>(this.data);
  160.         full.put(0.0, full.firstEntry().getValue());
  161.         full.put(1.0, full.lastEntry().getValue());
  162.         return full;
  163.     }

  164.     /**
  165.      * Returns the number of data points.
  166.      * @return int; number of data points.
  167.      */
  168.     public int size()
  169.     {
  170.         return this.data.size();
  171.     }

  172.     /**
  173.      * Create FractionalLengthData.
  174.      * @param data double...; fractional length - value pairs. Fractional lengths do not need to be in order.
  175.      * @return FractionalLengthData; fractional length data.
  176.      * @throws IllegalArgumentException when the number of input values is not even or 0.
  177.      * @throws IllegalArgumentException when a fractional value is not in the range [0 ... 1].
  178.      */
  179.     public static FractionalLengthData of(final double... data) throws IllegalArgumentException
  180.     {
  181.         return new FractionalLengthData(data);
  182.     }

  183. }