/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
* Copyright (C) 2015 University of South Florida
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.utility;
import java.util.Arrays;
import java.util.Iterator;
import java.util.SortedMap;
/**
* Generic methods to support interpolation of values against a sorted key-value
* map given a new target key.
*
* Note that these interpolation methods do not conform to the GTFS-rt spec. For GTFS-rt
* compliant interpolation/extrapolation, see {@link TransitInterpolationLibrary}.
*
* @author bdferris
*/
public class InterpolationLibrary {
private static final String OUT_OF_RANGE = "attempt to interpolate key outside range of key-value data";
private static final NumberInterpolationStrategy _numberInterpolation = new NumberInterpolationStrategy();
/**
* Same behavior as
* {@link #interpolate(SortedMap, Number, EOutOfRangeStrategy)} but with a
* default {@link EOutOfRangeStrategy} of
* {@link EOutOfRangeStrategy#INTERPOLATE}.
*
* @param values a sorted-map of key-value number pairs
* @param target the target key used to interpolate a value
* @param outOfRangeStrategy the strategy to use for a target key that outside
* the key-range of the value map
* @return an interpolated value for the target key
*/
public static <K extends Number, V extends Number> double interpolate(
SortedMap<K, V> values, K target) {
return interpolate(values, target, EOutOfRangeStrategy.INTERPOLATE);
}
/**
* Given a {@link SortedMap} with key-values that all extend from
* {@link Number}, interpolate using linear interpolation a value for a target
* key within the key-range of the map. For a key outside the range of the
* keys of the map, the {@code outOfRange} {@link EOutOfRangeStrategy}
* strategy will determine the interpolation behavior.
*
* @param values a sorted-map of key-value number pairs
* @param target the target key used to interpolate a value
* @param outOfRangeStrategy the strategy to use for a target key that outside
* the key-range of the value map
* @return an interpolated value for the target key
*/
public static <K extends Number, V extends Number> double interpolate(
SortedMap<K, V> values, K target, EOutOfRangeStrategy outOfRangeStrategy) {
Number result = interpolate(_numberInterpolation, outOfRangeStrategy,
values, target);
return result.doubleValue();
}
public static <K extends Number, V> V nearestNeighbor(SortedMap<K, V> values,
K target) {
if (values.isEmpty())
return null;
SortedMap<K, V> before = values.headMap(target);
SortedMap<K, V> after = values.tailMap(target);
if (before.isEmpty()) {
return after.get(after.firstKey());
} else if (after.isEmpty()) {
return before.get(before.lastKey());
} else {
K a = before.lastKey();
K b = after.firstKey();
if (Math.abs(b.doubleValue() - target.doubleValue()) < Math.abs(a.doubleValue()
- target.doubleValue())) {
return after.get(b);
} else {
return before.get(a);
}
}
}
public static double interpolate(double[] keys, double[] values,
double target, EOutOfRangeStrategy outOfRangeStrategy) {
return interpolate(keys, values, target, outOfRangeStrategy, null);
}
public static double interpolate(double[] keys, double[] values,
double target, EOutOfRangeStrategy outOfRangeStrategy, EInRangeStrategy inRangeStrategy) {
if (values.length == 0)
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
int index = Arrays.binarySearch(keys, target);
if (index >= 0)
return values[index];
index = -(index + 1);
if (index == values.length) {
switch (outOfRangeStrategy) {
case INTERPOLATE:
if (values.length > 1)
return interpolatePair(keys[index - 2], values[index - 2],
keys[index - 1], values[index - 1], target);
return values[index - 1];
case LAST_VALUE:
return values[index - 1];
case EXCEPTION:
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
}
}
if (index == 0) {
switch (outOfRangeStrategy) {
case INTERPOLATE:
if (values.length > 1)
return interpolatePair(keys[0], values[0], keys[1], values[1],
target);
return values[0];
case LAST_VALUE:
return values[0];
case EXCEPTION:
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
}
}
if (inRangeStrategy == null) {
inRangeStrategy = EInRangeStrategy.INTERPOLATE;
}
switch (inRangeStrategy) {
case PREVIOUS_VALUE:
return values[index - 1];
default:
return interpolatePair(keys[index - 1], values[index - 1], keys[index],
values[index], target);
}
}
/**
* Simple numeric interpolation between two double values using the equation
* {@code ratio * (toValue - fromValue) + fromValue}
*
* @param fromValue
* @param toValue
* @param ratio
* @return {@code ratio * (toValue - fromValue) + fromValue}
*/
public static double interpolatePair(double fromValue, double toValue,
double ratio) {
return ratio * (toValue - fromValue) + fromValue;
}
/**
* Simple numeric interpolation between two pairs of key-values and a third
* key. Here, {@code ratio = (targetKey - keyA) / (keyB - keyA)} and the
* result is {@code ratio * (valueB - valueA) + valueA}.
*
* @param fromValue
* @param toValue
* @param ratio
* @return {@code ratio * (toValue - fromValue) + fromValue}
*/
public static double interpolatePair(double keyA, double valueA, double keyB,
double valueB, double targetKey) {
double ratio = (targetKey - keyA) / (keyB - keyA);
return interpolatePair(valueA, valueB, ratio);
}
/**
* Given a {@link SortedMap} with key-values that all extend from
* {@link Number}, interpolate using linear interpolation a value for a target
* key within the key-range of the map. For a key outside the range of the
* keys of the map, the {@code outOfRange} {@link EOutOfRangeStrategy}
* strategy will determine the interpolation behavior.
*
* @param values
* @param target the target key used to interpolate a value
* @param outOfRangeStrategy the strategy to use for a target key that outside
* the key-range of the value map
* @return an interpolated value for the target key
*/
/**
*
* Given a {@link SortedMap} with key-values that of arbitrary type and a
* {@link InterpolationStrategy} to define interpolation over those types,
* interpolate a value for a target key within the key-range of the map. For a
* key outside the range of the keys of the map, the {@code outOfRange}
* {@link EOutOfRangeStrategy} strategy will determine the interpolation
* behavior.
*
* @param interpolationStrategy the interpolation strategy used to perform
* interpolation between key-value pairs of arbitrary type
* @param outOfRangeStrategy the strategy to use for a target key that outside
* the key-range of the value map
* @param values a sorted-map of key-value pairs
* @param target the target key used to interpolate a value
* @return an interpolated value for the target key
*/
public static <KEY extends Number, VALUE, ANY_KEY extends KEY, ANY_VALUE extends VALUE> VALUE interpolate(
InterpolationStrategy<KEY, VALUE> interpolationStrategy,
EOutOfRangeStrategy outOfRangeStrategy,
SortedMap<ANY_KEY, ANY_VALUE> values, ANY_KEY target) {
if (values.containsKey(target))
return values.get(target);
SortedMap<ANY_KEY, ANY_VALUE> before = values.headMap(target);
SortedMap<ANY_KEY, ANY_VALUE> after = values.tailMap(target);
ANY_KEY prevKey = null;
ANY_KEY nextKey = null;
if (before.isEmpty()) {
if (after.isEmpty())
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
switch (outOfRangeStrategy) {
case INTERPOLATE:
if (after.size() == 1)
return after.get(after.firstKey());
Iterator<ANY_KEY> it = after.keySet().iterator();
prevKey = it.next();
nextKey = it.next();
break;
case LAST_VALUE:
return after.get(after.firstKey());
case EXCEPTION:
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
}
} else if (after.isEmpty()) {
if (before.isEmpty())
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
switch (outOfRangeStrategy) {
case INTERPOLATE:
if (before.size() == 1)
return before.get(before.lastKey());
nextKey = before.lastKey();
before = before.headMap(nextKey);
prevKey = before.lastKey();
break;
case LAST_VALUE:
return before.get(before.lastKey());
case EXCEPTION:
throw new IndexOutOfBoundsException(OUT_OF_RANGE);
}
} else {
prevKey = before.lastKey();
nextKey = after.firstKey();
}
VALUE prevValue = values.get(prevKey);
VALUE nextValue = values.get(nextKey);
double keyRatio = (target.doubleValue() - prevKey.doubleValue())
/ (nextKey.doubleValue() - prevKey.doubleValue());
VALUE result = interpolationStrategy.interpolate(prevKey, prevValue,
nextKey, nextValue, keyRatio);
return result;
}
private static class NumberInterpolationStrategy implements
InterpolationStrategy<Number, Number> {
@Override
public Number interpolate(Number prevKey, Number prevValue, Number nextKey,
Number nextValue, double ratio) {
double result = interpolatePair(prevValue.doubleValue(),
nextValue.doubleValue(), ratio);
return new Double(result);
}
}
}