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}