package com.github.pfichtner.jrunalyser.base.data.track.comparator;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.endPos;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.segmentStartPoints;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.startPos;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackHeight;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackLeftBottom;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackLength;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackMaxEle;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackMinEle;
import static com.github.pfichtner.jrunalyser.base.data.track.comparator.ComparatorFunctions.trackWidth;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import com.github.pfichtner.jrunalyser.base.comparator.RelDiffNumberComparator;
import com.github.pfichtner.jrunalyser.base.data.Coordinate;
import com.github.pfichtner.jrunalyser.base.data.DefaultDistance;
import com.github.pfichtner.jrunalyser.base.data.DefaultWayPoint;
import com.github.pfichtner.jrunalyser.base.data.Distance;
import com.github.pfichtner.jrunalyser.base.data.DistanceUnit;
import com.github.pfichtner.jrunalyser.base.data.Distances;
import com.github.pfichtner.jrunalyser.base.data.GeoUtil;
import com.github.pfichtner.jrunalyser.base.data.GeoUtil.BearingInfo;
import com.github.pfichtner.jrunalyser.base.data.GeoUtil.DefaultBearingInfo;
import com.github.pfichtner.jrunalyser.base.data.WayPoint;
import com.github.pfichtner.jrunalyser.base.data.segmenter.Segmenter;
import com.github.pfichtner.jrunalyser.base.data.segmenter.Segmenters;
import com.github.pfichtner.jrunalyser.base.data.track.Track;
import com.github.pfichtner.jrunalyser.base.data.track.Tracks;
import com.github.pfichtner.jrunalyser.base.data.track.comparator.CombinedIterator.Pair;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* Utility class holding references to {@link Comparator}s for {@link Track}s.
*
* @author Peter Fichtner
*/
public final class TrackComparators {
public interface FBC<V> {
V applyA(Track a);
V applyB(Track b);
}
/**
* ChainedComparator calls {@link Comparator#compare(Object, Object)} on
* each comparator of the chain until the compare result is
* <code>!= 0</code>, this result of this last {@link Comparator} is
* returned or <code>0</code> if all compare results were <code>0</code>,
*
* @author Peter Fichtner
*
* @param <T>
* type this Comparator compares
*/
public static class ChainedComparator<T> implements Comparator<T> {
private final List<Comparator<T>> comparators;
public ChainedComparator(Comparator<T> comparator) {
this.comparators = ImmutableList.of(comparator);
}
public ChainedComparator(Iterable<Comparator<T>> comparators) {
this.comparators = ImmutableList.copyOf(comparators);
}
public ChainedComparator<T> add(Comparator<T> comparator) {
return new ChainedComparator<T>(base().add(comparator).build());
}
public ChainedComparator<T> addAll(Iterable<Comparator<T>> comparators) {
return new ChainedComparator<T>(base().addAll(comparators).build());
}
private Builder<Comparator<T>> base() {
return ImmutableList.<Comparator<T>> builder().addAll(
this.comparators);
}
/**
* Returns the (immutable) List of underlying {@link Comparator}s.
*
* @return immutable List of underlying {@link Comparator}s
*/
public List<Comparator<T>> getComparators() {
return this.comparators;
}
@Override
public int compare(T t1, T t2) {
ComparisonChain chain = ComparisonChain.start();
for (Comparator<T> comparator : this.comparators) {
chain = chain.compare(t1, t2, comparator);
}
return chain.result();
}
}
private static final Function<Track, Track> revTrack = new Function<Track, Track>() {
@Override
public Track apply(Track track) {
return Tracks.reverse(track);
}
};
private static final BearingInfo ZERO_BEARING = new DefaultBearingInfo(
DefaultDistance.of(0, DistanceUnit.METERS), 0);
private TrackComparators() {
super();
}
/**
* A Comparator that calls the <code>Function</code> to get a <code>T</code>
* from both tracks (e.g. length) and than delegates to the comparator by
* passing those two <code>T</code>s.
*
* @author Peter Fichtner
*
* @param <T>
* type that is compared
*/
public static class FunctionBasedComparator<T> implements
Comparator<Track>, FBC<T> {
private final Function<Track, T> function;
private final Comparator<T> comparator;
public FunctionBasedComparator(Function<Track, T> function,
Comparator<T> comparator) {
this.function = function;
this.comparator = comparator;
}
@Override
public int compare(Track t1, Track t2) {
return this.comparator.compare(applyA(t1), applyB(t2));
}
@Override
public T applyA(Track a) {
return this.function.apply(a);
}
@Override
public T applyB(Track b) {
return this.function.apply(b);
}
@Override
public String toString() {
return "FunctionBasedComparator [function=" + this.function
+ ", comparator=" + this.comparator + "]";
}
}
private static class WaypointDistanceComparator implements
Comparator<Track>, FBC<Coordinate> {
private final Function<Track, ? extends Coordinate> function;
private final Distance maxDiff;
public WaypointDistanceComparator(
Function<Track, ? extends Coordinate> function, Distance maxDiff) {
this.function = function;
this.maxDiff = Distances.abs(maxDiff);
}
@Override
public int compare(Track t1, Track t2) {
Distance diff = Distances.abs(GeoUtil.calcDistance(applyA(t1),
applyB(t2)));
return diff.compareTo(this.maxDiff) <= 0 ? 0 : diff
.compareTo(this.maxDiff);
}
@Override
public Coordinate applyA(Track a) {
return this.function.apply(a);
}
@Override
public Coordinate applyB(Track b) {
return this.function.apply(b);
}
@Override
public String toString() {
return "WaypointDistanceComparator [function=" + this.function
+ ", maxDiff=" + this.maxDiff + "]";
}
}
private static class MaxAbsComparator implements Comparator<Track>,
FBC<Number> {
private final Function<Track, Number> function;
private final BigDecimal maxDiff;
public MaxAbsComparator(Function<Track, Number> function,
BigDecimal maxDiff) {
this.function = function;
this.maxDiff = maxDiff;
}
@Override
public int compare(Track t1, Track t2) {
BigDecimal diff = new BigDecimal(String.valueOf(applyA(t1)))
.subtract(new BigDecimal(String.valueOf(applyB(t2))));
return diff.abs().compareTo(this.maxDiff) <= 0 ? 0
: diff.signum() > 0 ? 1 : -1;
}
@Override
public Number applyA(Track a) {
return this.function.apply(a);
}
@Override
public Number applyB(Track b) {
return this.function.apply(b);
}
@Override
public String toString() {
return "MaxAbsComparator [function=" + this.function + ", maxDiff="
+ this.maxDiff + "]";
}
}
/**
* Returns an Iterator that returns the differences of each waypoint pair of
* <code>it1</code> and <code>it2</code>.
*
* @param it1
* waypoints 1
* @param it2
* waypoints 2
* @return Iterator returning the differences of each waypoint pair
*/
private static <T extends WayPoint> CombinedIterator<T, Distance> wpDistanceDiffIterator(
Iterator<? extends T> it1, Iterator<? extends T> it2) {
return new CombinedIterator<T, Distance>(it1, it2,
new Function<Pair<T>, Distance>() {
@Override
public Distance apply(Pair<T> pair) {
// TODO delegate to MaxAbsComparator
T wp1 = pair.getValue1();
T wp2 = pair.getValue2();
return Distances.abs(GeoUtil.calcDistance(wp1, wp2));
}
@Override
public String toString() {
return "WpDistanceDiffIterator";
}
});
}
/**
* A Comparator that returns <code>true</code> if <b>all</b> waypoints
* returned by the function(s) have a maxDiff of <code>maxDiff</code>.
*
* @author Peter Fichtner
*/
public static class MultiWaypointDistanceComparator implements
Comparator<Track>, FBC<Iterable<? extends WayPoint>> {
private final Function<Track, ? extends Iterable<? extends WayPoint>> f1;
private final Function<Track, ? extends Iterable<? extends WayPoint>> f2;
private final Distance maxDiff;
public MultiWaypointDistanceComparator(
Function<Track, Iterable<WayPoint>> function, Distance maxDiff) {
this(function, function, maxDiff);
}
public MultiWaypointDistanceComparator(
Function<Track, ? extends Iterable<? extends WayPoint>> f1,
Function<Track, ? extends Iterable<? extends WayPoint>> f2,
Distance maxDiff) {
this.f1 = f1;
this.f2 = f2;
this.maxDiff = Distances.abs(maxDiff);
}
@Override
public Iterable<? extends WayPoint> applyA(Track a) {
return this.f1.apply(a);
}
@Override
public Iterable<? extends WayPoint> applyB(Track b) {
return this.f2.apply(b);
}
@Override
public int compare(Track t1, Track t2) {
CombinedIterator<? extends WayPoint, Distance> wpDistanceDiffIterator = getDiffs(
t1, t2);
for (CombinedIterator<? extends WayPoint, Distance> iterator = wpDistanceDiffIterator; iterator
.hasNext();) {
Distance distance = iterator.next();
int cmp = Distances.abs(distance).compareTo(this.maxDiff);
if (cmp > 0) {
return cmp;
}
}
// do not check wpDistanceDiffIterator#isTotallyCollected() since a
// Track with 4,301km could contain 43 elements and the other with
// just 4,299 "only" 42
return 0;
}
/**
* Returns an Iterator holding the differences of the waypoints of the
* two tracks.
*
* @param t1
* Track 1
* @param t2
* Track 2
* @return Iterator holding the differences of the waypoints
* @see TrackComparators#wpDistanceDiffIterator(Track, Track)
*/
public CombinedIterator<? extends WayPoint, Distance> getDiffs(
Track t1, Track t2) {
return wpDistanceDiffIterator(applyA(t1).iterator(), applyB(t2)
.iterator());
}
}
/**
* Creates a new Comparator for Tracks that will return <code>0</code> on
* the following conditions:
* <ul>
* <li>The difference of the length of the tracks must be not more than 8%</li>
* <li>The difference of the width of the tracks must be not more than 8%</li>
* <li>The difference of the height of the tracks must be not more than 8%</li>
* <li>The difference of the tracks' Southwest corner must be not more than
* 80 meters</li>
* <li>The difference of the tracks' startpoints must be not more than 80
* meters</li>
* <li>The difference of the tracks' endpoints must be not more than 80
* meters</li>
* <li>The difference of the tracks' min. elevation must be not more than 30
* meters</li>
* <li>The difference of the tracks' max. elevation must be not more than 30
* meters</li>
* </ul>
*/
public static ChainedComparator<Track> baseAttributes = builder()
.addRel(trackLength(DistanceUnit.METERS), 8)
.addRel(trackWidth(DistanceUnit.METERS), 8)
.addRel(trackHeight(DistanceUnit.METERS), 8)
.addAbs(trackLeftBottom(),
DefaultDistance.of(80, DistanceUnit.METERS))
.addAbs(startPos(), DefaultDistance.of(100, DistanceUnit.METERS))
.addAbs(endPos(), DefaultDistance.of(100, DistanceUnit.METERS))
.addAbsX(trackMinEle(DistanceUnit.METERS), BigDecimal.valueOf(30))
.addAbsX(trackMaxEle(DistanceUnit.METERS), BigDecimal.valueOf(30))
.build();
private static Comparator<Track> rel(Function<Track, Number> function,
int maxDiff) {
return new FunctionBasedComparator<Number>(function,
new RelDiffNumberComparator(maxDiff));
}
private static WaypointDistanceComparator absSingle(
final Function<Track, ? extends Coordinate> function,
final Distance maxDiff) {
return new WaypointDistanceComparator(function, maxDiff);
}
private static MaxAbsComparator absSingleX(
final Function<Track, Number> function, final BigDecimal maxDiff) {
return new MaxAbsComparator(function, maxDiff);
}
private static MultiWaypointDistanceComparator absMulti(
Function<Track, Iterable<WayPoint>> function, Distance maxDiff) {
return absMulti(function, function, maxDiff);
}
private static MultiWaypointDistanceComparator absMulti(
Function<Track, ? extends Iterable<? extends WayPoint>> f1,
Function<Track, ? extends Iterable<? extends WayPoint>> f2,
Distance maxDiff) {
return new MultiWaypointDistanceComparator(f1, f2, maxDiff);
}
private static MultiWaypointDistanceComparator abs(
Function<Track, ? extends Iterable<? extends WayPoint>> f1,
Function<Track, ? extends Iterable<? extends WayPoint>> f2,
Distance maxDiff) {
return new MultiWaypointDistanceComparator(f1, f2, maxDiff);
}
public static MultiWaypointDistanceComparator segmentStartPointsEqual(
Track t1, Track t2, Distance maxDiff) {
return segmentStartPointsEqual(t1, t2, maxDiff, 12);
}
public static MultiWaypointDistanceComparator segmentStartPointsEqual(
Track t1, Track t2, Distance distance, int segments) {
Function<Track, Iterable<? extends WayPoint>> f1 = segmentStartPoints(createSegmenter(
t1, t2, segments));
Function<Track, ? extends Iterable<? extends WayPoint>> f2 = bearingDecorater(
t1, t2, f1);
return absMulti(f1, f2, distance);
}
// same than segmentStartPointsEqual but returns true if the two track's
// segments are reversed order
private static Comparator<Track> segmentStartPointsEqualReverseOrder(
Track t1, Track t2, Distance distance, int segments) {
Segmenter segmenter = createSegmenter(t1, t2, segments);
Function<Track, Iterable<? extends WayPoint>> f1 = segmentStartPoints(segmenter);
Function<Track, ? extends Iterable<? extends WayPoint>> f2 = bearingDecorater(
t1, t2, f1);
return abs(f1, Functions.compose(f2, revTrack), distance);
}
// ----------------------------------------------------------------------------------
/**
* Creates a Decorator that returns each Waypoint projected using the
* {@link BearingInfo} that is created by comparing both start positions.
*
* @param bearingInfo
* bearing data
* @param in
* the Function to decorate
* @return decorator Function
*/
public static Function<Track, ? extends Iterable<? extends WayPoint>> bearingDecorater(
Track track1, Track track2,
final Function<Track, ? extends Iterable<? extends WayPoint>> in) {
final BearingInfo bearingInfo = GeoUtil.bearingInfo(
Tracks.getStartPoint(track1), Tracks.getStartPoint(track2));
return ZERO_BEARING.equals(bearingInfo) ? in
: new Function<Track, Iterable<? extends WayPoint>>() {
@Override
public Iterable<WayPoint> apply(Track track) {
return Iterables.transform(in.apply(track),
bearingFunction(bearingInfo));
}
};
}
/**
* Creates a Decorator that returns the Waypoints starting by 0.0/0.0.
*
* @param bearingInfo
* bearing data
* @param in
* the Function to decorate
* @return decorator Function
*/
public static Function<Track, ? extends Iterable<? extends WayPoint>> bearingZeroZeroDecorater(
final Function<Track, ? extends Iterable<? extends WayPoint>> in) {
return new Function<Track, Iterable<? extends WayPoint>>() {
@Override
public Iterable<WayPoint> apply(Track track) {
final BearingInfo bearingInfo = GeoUtil.bearingInfo(
Tracks.getStartPoint(track), Coordinate.ZERO_ZERO);
return Iterables.transform(in.apply(track),
bearingFunction(bearingInfo));
}
};
}
private static Function<WayPoint, WayPoint> bearingFunction(
final BearingInfo bearingInfo) {
return new Function<WayPoint, WayPoint>() {
@Override
public WayPoint apply(WayPoint wayPoint) {
Coordinate projected = GeoUtil.project(bearingInfo, wayPoint);
return new DefaultWayPoint(projected.getLatitude(),
projected.getLongitude(), wayPoint.getElevation(),
wayPoint.getTime());
}
@Override
public String toString() {
return "BearingDecorator [bearingInfo=" + bearingInfo + "]";
}
};
}
// ----------------------------------------------------------------------------------
/**
* Creates a Segmenter that segments a track based on the length of the two
* tracks and the amount of checkpoints. If there are two tracks of each
* 2000m length and an amount of 4 checkpoints the segments will segment
* into 500m segments.
*
* @param t1
* Track #1
* @param t2
* Track #2
* @param checkpoints
* amount of checkpoints
* @return Segmenter that segments a track based on the length of the two
* tracks and the amount of checkpoints
*/
private static Segmenter createSegmenter(Track t1, Track t2, int checkpoints) {
return Segmenters.distance(DefaultDistance.of(getAvgLength(t1, t2)
/ checkpoints, DistanceUnit.METERS));
}
private static double getAvgLength(Track t1, Track t2) {
return t1.getStatistics().getDistance()
.add(t2.getStatistics().getDistance())
.getValue(DistanceUnit.METERS) / 2;
}
/**
* Creates a new Comparator for Tracks that will return <code>0</code> on
* the following conditions:
* <ul>
* <li>The difference of the length of the tracks must be not more than 8%</li>
* <li>The difference of the width of the tracks must be not more than 8%</li>
* <li>The difference of the height of the tracks must be not more than 8%</li>
* <li>The difference of the tracks' Northwest corner must be not more than
* 80 meters</li>
* <li>The difference of the tracks' startpoints must be not more than 80
* meters</li>
* <li>The difference of the tracks' endpoints must be not more than 80
* meters</li>
* <li>The difference of the tracks' min. elevation must be not more than 8
* percent</li>
* <li>The difference of the tracks' max. elevation must be not more than 8
* percent</li>
* <li>The difference of the tracks' 12 checkpoints all n meters must be not
* more than 100 meters</li>
* </ul>
*/
public static Comparator<Track> byAttributes = new Comparator<Track>() {
@Override
public int compare(Track t1, Track t2) {
Comparator<Track> segmentStartPointsEqual = segmentStartPointsEqual(
t1, t2, DefaultDistance.of(150, DistanceUnit.METERS));
return ComparisonChain.start().compare(t1, t2, baseAttributes)
.compare(t1, t2, segmentStartPointsEqual).result();
}
};
public static Comparator<Track> byDescription = new Comparator<Track>() {
@Override
public int compare(Track t1, Track t2) {
CharMatcher javaLetterOrDigit = CharMatcher.JAVA_LETTER_OR_DIGIT;
String d1 = t1.getMetadata().getDescription();
String d2 = t2.getMetadata().getDescription();
return javaLetterOrDigit.retainFrom(d1).toLowerCase()
.compareTo(javaLetterOrDigit.retainFrom(d2).toLowerCase());
}
};
public static class TrackComparatorBuilder {
private List<Comparator<Track>> comparators = Lists.newArrayList();
public TrackComparatorBuilder addRel(Function<Track, Number> function,
int maxDiff) {
this.comparators.add(rel(function, maxDiff));
return this;
}
public TrackComparatorBuilder addAbs(
Function<Track, ? extends Coordinate> function,
Distance distance) {
this.comparators.add(absSingle(function, distance));
return this;
}
public TrackComparatorBuilder addAbsX(Function<Track, Number> function,
BigDecimal maxDiff) {
this.comparators.add(absSingleX(function, maxDiff));
return this;
}
public ChainedComparator<Track> build() {
return new ChainedComparator<Track>(this.comparators);
}
}
public static TrackComparatorBuilder builder() {
return new TrackComparatorBuilder();
}
}