001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.math.interpolation;
006
007import edu.wpi.first.math.MathUtil;
008import java.util.NavigableMap;
009import java.util.Optional;
010import java.util.TreeMap;
011
012/**
013 * The TimeInterpolatableBuffer provides an easy way to estimate past measurements. One application
014 * might be in conjunction with the DifferentialDrivePoseEstimator, where knowledge of the robot
015 * pose at the time when vision or other global measurement were recorded is necessary, or for
016 * recording the past angles of mechanisms as measured by encoders.
017 *
018 * @param <T> The type stored in this buffer.
019 */
020public final class TimeInterpolatableBuffer<T> {
021  private final double m_historySize;
022  private final InterpolateFunction<T> m_interpolatingFunc;
023  private final NavigableMap<Double, T> m_pastSnapshots = new TreeMap<>();
024
025  private TimeInterpolatableBuffer(
026      InterpolateFunction<T> interpolateFunction, double historySizeSeconds) {
027    this.m_historySize = historySizeSeconds;
028    this.m_interpolatingFunc = interpolateFunction;
029  }
030
031  /**
032   * Create a new TimeInterpolatableBuffer.
033   *
034   * @param interpolateFunction The function used to interpolate between values.
035   * @param historySizeSeconds The history size of the buffer.
036   * @param <T> The type of data to store in the buffer.
037   * @return The new TimeInterpolatableBuffer.
038   */
039  public static <T> TimeInterpolatableBuffer<T> createBuffer(
040      InterpolateFunction<T> interpolateFunction, double historySizeSeconds) {
041    return new TimeInterpolatableBuffer<>(interpolateFunction, historySizeSeconds);
042  }
043
044  /**
045   * Create a new TimeInterpolatableBuffer that stores a given subclass of {@link Interpolatable}.
046   *
047   * @param historySizeSeconds The history size of the buffer.
048   * @param <T> The type of {@link Interpolatable} to store in the buffer.
049   * @return The new TimeInterpolatableBuffer.
050   */
051  public static <T extends Interpolatable<T>> TimeInterpolatableBuffer<T> createBuffer(
052      double historySizeSeconds) {
053    return new TimeInterpolatableBuffer<>(Interpolatable::interpolate, historySizeSeconds);
054  }
055
056  /**
057   * Create a new TimeInterpolatableBuffer to store Double values.
058   *
059   * @param historySizeSeconds The history size of the buffer.
060   * @return The new TimeInterpolatableBuffer.
061   */
062  public static TimeInterpolatableBuffer<Double> createDoubleBuffer(double historySizeSeconds) {
063    return new TimeInterpolatableBuffer<>(MathUtil::interpolate, historySizeSeconds);
064  }
065
066  /**
067   * Add a sample to the buffer.
068   *
069   * @param timeSeconds The timestamp of the sample.
070   * @param sample The sample object.
071   */
072  public void addSample(double timeSeconds, T sample) {
073    cleanUp(timeSeconds);
074    m_pastSnapshots.put(timeSeconds, sample);
075  }
076
077  /**
078   * Removes samples older than our current history size.
079   *
080   * @param time The current timestamp.
081   */
082  private void cleanUp(double time) {
083    while (!m_pastSnapshots.isEmpty()) {
084      var entry = m_pastSnapshots.firstEntry();
085      if (time - entry.getKey() >= m_historySize) {
086        m_pastSnapshots.remove(entry.getKey());
087      } else {
088        return;
089      }
090    }
091  }
092
093  /** Clear all old samples. */
094  public void clear() {
095    m_pastSnapshots.clear();
096  }
097
098  /**
099   * Sample the buffer at the given time. If the buffer is empty, an empty Optional is returned.
100   *
101   * @param timeSeconds The time at which to sample.
102   * @return The interpolated value at that timestamp or an empty Optional.
103   */
104  public Optional<T> getSample(double timeSeconds) {
105    if (m_pastSnapshots.isEmpty()) {
106      return Optional.empty();
107    }
108
109    // Special case for when the requested time is the same as a sample
110    var nowEntry = m_pastSnapshots.get(timeSeconds);
111    if (nowEntry != null) {
112      return Optional.of(nowEntry);
113    }
114
115    var topBound = m_pastSnapshots.ceilingEntry(timeSeconds);
116    var bottomBound = m_pastSnapshots.floorEntry(timeSeconds);
117
118    // Return null if neither sample exists, and the opposite bound if the other is null
119    if (topBound == null && bottomBound == null) {
120      return Optional.empty();
121    } else if (topBound == null) {
122      return Optional.of(bottomBound.getValue());
123    } else if (bottomBound == null) {
124      return Optional.of(topBound.getValue());
125    } else {
126      // Otherwise, interpolate. Because T is between [0, 1], we want the ratio of (the difference
127      // between the current time and bottom bound) and (the difference between top and bottom
128      // bounds).
129      return Optional.of(
130          m_interpolatingFunc.interpolate(
131              bottomBound.getValue(),
132              topBound.getValue(),
133              (timeSeconds - bottomBound.getKey()) / (topBound.getKey() - bottomBound.getKey())));
134    }
135  }
136
137  /**
138   * Grant access to the internal sample buffer. Used in Pose Estimation to replay odometry inputs
139   * stored within this buffer.
140   *
141   * @return The internal sample buffer.
142   */
143  public NavigableMap<Double, T> getInternalBuffer() {
144    return m_pastSnapshots;
145  }
146
147  public interface InterpolateFunction<T> {
148    /**
149     * Return the interpolated value. This object is assumed to be the starting position, or lower
150     * bound.
151     *
152     * @param start The lower bound, or start.
153     * @param end The upper bound, or end.
154     * @param t How far between the lower and upper bound we are. This should be bounded in [0, 1].
155     * @return The interpolated value.
156     */
157    T interpolate(T start, T end, double t);
158  }
159}