From 9f6a38ec7a84b8de03df4a320bb45729337cc1fc Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 19 Mar 2016 22:44:56 +0100 Subject: [PATCH] use two probabilities for start mark passings: one based on start line proximity, one based on variance of distance from start line Change-Id: I2d25867c2b78c242e5dc007b255300d54175725e --- .../impl/CandidateFinderImplWhiteBoxTest.java | 109 ++++++++++ .../impl/CandidateFinderImpl.java | 194 +++++++++++++++--- 2 files changed, 270 insertions(+), 33 deletions(-) create mode 100644 java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java new file mode 100644 index 00000000000..23788331f99 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImplWhiteBoxTest.java @@ -0,0 +1,109 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.impl.MeterDistance; +import com.sap.sailing.domain.markpassingcalculation.impl.CandidateFinderImpl.AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; + +public class CandidateFinderImplWhiteBoxTest { + private CandidateFinderImpl finder; + + @Before + public void setUp() { + DynamicTrackedRace trackedRace = mock(DynamicTrackedRace.class); + final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("29er"); + RaceDefinition race = mock(RaceDefinition.class); + when(trackedRace.getRace()).thenReturn(race); + when(race.getBoatClass()).thenReturn(boatClass); + when(race.getCompetitors()).thenReturn(Collections.emptySet()); + finder = new CandidateFinderImpl(trackedRace); + } + + @Test + public void testOneProbabilityForEmptyList() { + List emptyDistancesToStartLineOfOtherCompetitors = new ArrayList<>(); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(emptyDistancesToStartLineOfOtherCompetitors, + /* startIsLine */ true); + assertEquals(1.0, probability, 0.00001); + } + + @Test + public void testHighProbabilityForCloseProximityAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue(probability > 0.95); + } + + @Test + public void testHighProbabilityForCloseProximityExceptOneAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(5.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(8.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(1.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue("Expected probability to exceed 55% but got "+probability, probability > 0.55); + } + + @Test + public void testLowProbabilityForMostFarAwayAndNoLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(50.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(250.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(200.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(300.0, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2.5, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(3.2, null)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(2739.0, null)); + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ false); + assertTrue("Expected probability to be below 10% but got "+probability, probability <= 0.1); + } + + @Test + public void testHighProbabilityForLateStartAndStartIsLine() { + List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(100.0, -80.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(120.0, -82.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(130.0, -81.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(90.0, -79.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(140.0, -75.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(150, -88.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(135, -77.)); + distancesToStartLineOfOtherCompetitors.add(createDistancePair(20.0, -10.)); // another late starter just across the line + final double probability = finder.getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, + /* startIsLine */ true); + assertTrue("Expected probability to be above 85% but got "+probability, probability >= 0.85); + } + + private AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine createDistancePair(double absoluteDistanceToLine, + Double signedXTEDistanceFromLine) { + return new AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine( + new MeterDistance(absoluteDistanceToLine), signedXTEDistanceFromLine == null ? null : new MeterDistance(signedXTEDistanceFromLine)); + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java index 889f2ae3c98..21396126e67 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java @@ -2,6 +2,7 @@ package com.sap.sailing.domain.markpassingcalculation.impl; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; @@ -35,6 +36,7 @@ import com.sap.sailing.domain.tracking.GPSFixTrack; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; /** * The standard implemantation of {@link CandidateFinder}. There are two kinds of {@link Candidate}s. First of all, @@ -77,8 +79,8 @@ public class CandidateFinderImpl implements CandidateFinder { */ private static final double PENALTY_FOR_DISTANCE_CANDIDATES = 0.9 * PENALTY_FOR_WRONG_DIRECTION; - private static final double WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START = 0.6; - private static final double NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES = 5; + private static final double WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START = 0.1; + private static final double NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES = 10; private static final Logger logger = Logger.getLogger(CandidateFinderImpl.class.getName()); @@ -426,7 +428,7 @@ public class CandidateFinderImpl implements CandidateFinder { : PENALTY_FOR_DISTANCE_CANDIDATES * PENALTY_FOR_WRONG_SIDE; final Double newStartProbabilityBasedOnOtherCompetitors; if (isStartWaypoint(w)) { - newStartProbabilityBasedOnOtherCompetitors = getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); + newStartProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); if (newStartProbabilityBasedOnOtherCompetitors != null) { newProbability *= newStartProbabilityBasedOnOtherCompetitors; } @@ -680,7 +682,7 @@ public class CandidateFinderImpl implements CandidateFinder { // at time point t the other competitors are largely not even close to the start waypoint; // this will make start candidates much more likely if many other competitors are also very close to the // start, and it will help ruling out those candidates where someone is practicing a start just for themselves - startProbabilityBasedOnOtherCompetitors = getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); + startProbabilityBasedOnOtherCompetitors = getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(c, t); if (startProbabilityBasedOnOtherCompetitors != null) { probability *= startProbabilityBasedOnOtherCompetitors; } @@ -690,13 +692,59 @@ public class CandidateFinderImpl implements CandidateFinder { return new XTECandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, passesInTheRightDirectionProbability); } + + static class AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine { + private final Distance absoluteGeometricDistance; + private final Distance signedProjectedDistance; + public AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(Distance absoluteGeometricDistance, + Distance signedProjectedDistance) { + super(); + this.absoluteGeometricDistance = absoluteGeometricDistance; + this.signedProjectedDistance = signedProjectedDistance; + } + public Distance getAbsoluteGeometricDistance() { + return absoluteGeometricDistance; + } + public Distance getSignedProjectedDistance() { + return signedProjectedDistance; + } + @Override + public String toString() { + return "AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine [absoluteGeometricDistance=" + + absoluteGeometricDistance + ", signedProjectedDistance=" + signedProjectedDistance + "]"; + } + } /** - * For a fixed line start it is very likely that all competitors pass the start waypoint at about the same time. - * This requires everyone to at least be pretty close to the start waypoint at the start time. Therefore, a start - * mark passing candidate is very likely when a large share of competitors is very close to the start line, whereas - * it becomes pretty unlikely when everyone is sailing around anywhere on the course or behind the line but not - * being in close proximity to the line. + * For a fixed line start it is very likely that all competitors pass the start waypoint at about the same time, or + * that only a few competitors lag behind, e.g., because of start time penalties. This requires everyone to at least + * be pretty close to the start waypoint at the start time, of if we're looking at a competitor starting late, the + * field would at least have little variance and be consistently on the course side of the start line. + *

+ * + * Therefore, a start mark passing candidate is very likely when a large share of competitors is very close to the + * start line. If the other competitors are not very close to the start line, a start may still be likely if the + * others are on the course side and have similar distance to the start line, whereas it becomes pretty unlikely + * when everyone is sailing around anywhere on the course or behind the line but not being in close proximity to the + * line and not having similar distances to the start line while being on course side. + * + *

    + *
  • check distance variance of other competitors; low variance could mean they started jointly
  • + *
  • for start *line*, consider the side on which the other competitors are; don't rule out a late start if other + * competitors have low distance variance and are on course side of the line
  • + *
  • generally consider distance of other competitors to line
  • + *
  • dampen "outliers" such as trackers forgotten in harbor or trackers not currently sending (80% of competitors + * that look like they are starting seems like a good guess)
  • + *
+ * + * Approach: compute signed distances to start line (positive meaning on course side). Sort by absolute distance and + * compute a {@link #weight(double) weighted} average where the boats farthest away are considered less relevant than + * the ones closer, leading to a natural outlier removal.

+ * + * For the special case of a late starter, consider the weighted variance of the signed distance from the start line. + * A low variance with a positive average (meaning on course side) indicates that everyone else may have started around the + * same time, making this a probable late starter's candidate. Therefore, low variance with positive average shall increase + * a probability that was low because of a large weighted average distance. * * @param t * the time point at which to consider the other competitors behavior relative to the start waypoint @@ -705,18 +753,18 @@ public class CandidateFinderImpl implements CandidateFinder { * completely unlikely that a candidate for {@code c} at time {@code t} could have been a start candidate; 1 * meaning that based on the other competitors' relation to the start line at time point {@code t} is seems * a fact that this must have been the start. + * */ - private Double getPenaltyForStartCandidateBasedOnOtherCompetitorsBehavior(Competitor c, TimePoint t) { + private Double getProbabilityForStartCandidateBasedOnOtherCompetitorsBehavior(Competitor c, TimePoint t) { final Double result; if (race.getStartOfRace(/* inferred */ false) == null && race.isGateStart() != Boolean.TRUE) { - // boats within one hull length of the start line are great indicators that we're at the start: - final double hullLengthInMeters = race.getRace().getBoatClass().getHullLength().getMeters(); final Waypoint start = race.getRace().getCourse().getFirstWaypoint(); + final Iterable> crossingInformationForStart = getCrossingInformation(start, t); if (start == null) { result = 1.0; } else { - int numberOfCompetitorsForWhichStartLineDistanceIsAvailable = 0; - double log10OfMeterDistanceSum = 0; + final boolean startIsLine = start.getPassingInstructions() == PassingInstruction.Line; + final List distancesToStartLineOfOtherCompetitors = new ArrayList<>(); for (final Competitor otherCompetitor : race.getRace().getCompetitors()) { if (otherCompetitor != c) { final DynamicGPSFixTrack track = race.getTrack(otherCompetitor); @@ -724,29 +772,21 @@ public class CandidateFinderImpl implements CandidateFinder { final Position estimatedPositionAtT = track.getEstimatedPosition(t, /* extrapolate */ true); final Distance otherCompetitorsDistanceToStartAtT = getMinDistanceOrNull(calculateDistance(estimatedPositionAtT, start, t)); if (otherCompetitorsDistanceToStartAtT != null) { - numberOfCompetitorsForWhichStartLineDistanceIsAvailable++; - log10OfMeterDistanceSum += Math.log10(otherCompetitorsDistanceToStartAtT.getMeters()); + final Distance crossTrackError; + if (startIsLine) { + Pair crossingInformationForStartLine = crossingInformationForStart.iterator().next(); + crossTrackError = estimatedPositionAtT.crossTrackError(crossingInformationForStartLine.getA(), + crossingInformationForStartLine.getB()); + } else { + crossTrackError = null; + } + distancesToStartLineOfOtherCompetitors.add(new + AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine(otherCompetitorsDistanceToStartAtT, crossTrackError)); } } } } - // TODO use a function that will approach 1 as we get, say, 50% of the other competitors within a reasonable distance from the start line, say 10m; the closer, the more likely - // To make outliers such as trackers left in the harbor not hurt the results unnecessarily, the logarithm of - // distances to the start waypoint is averaged, giving more relevance to the competitors close to the line when - // computing an arithmetic mean across the logarithms. - // f(1) := 1 - // f(NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES) := WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START - // f(x) := 1-(1-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES * (hullLengthInMeters / weightedMeterAverageDistance - 1) - if (numberOfCompetitorsForWhichStartLineDistanceIsAvailable > 0) { - double log10Avg = log10OfMeterDistanceSum / numberOfCompetitorsForWhichStartLineDistanceIsAvailable; - double weightedMeterAverageDistance = Math.pow(10, log10Avg); - result = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0, - (1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES - * (weightedMeterAverageDistance / hullLengthInMeters-1)))); - } else { - // no distance for any other competitor from the start line was available - result = 1.0; - } + result = getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances(distancesToStartLineOfOtherCompetitors, startIsLine); } } else { result = null; @@ -754,6 +794,94 @@ public class CandidateFinderImpl implements CandidateFinder { return result; } + /** + * The method computes two probabilities of the candidate being a start: one based on the other competitors largely + * being close to the start line, meaning a regular start where the competitor starts roughly at the same time as + * everybody else; the other probability computed is based on the other competitors largely being in a similar + * distance to the start line on the course side, suggesting that the competitor is starting late, but mostly + * everybody else did start earlier and around the same time. The probability returned is the probability of + * the start being the one or the other scenario which mathematically is represented by using the inverted + * probabilities of the two start options, multiplying them and inverting the resulting probability. + */ + Double getProbabilityOfStartBasedOnOtherCompetitorsStartLineDistances( + final List distancesToStartLineOfOtherCompetitors, boolean startIsLine) { + final Double result; + // sort by the absolute distance to start line + // boats within one hull length of the start line are great indicators that we're at the start: + final Distance hullLength = race.getRace().getBoatClass().getHullLength(); + final Distance weightedAverageAbsoluteDistance; + if (!distancesToStartLineOfOtherCompetitors.isEmpty()) { + Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getAbsoluteGeometricDistance().compareTo(b.getAbsoluteGeometricDistance())); + Distance weightedAbsoluteDistanceSum = Distance.NULL; + int i=0; + double weightSum = 0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + final Distance weightedDistance = d.getAbsoluteGeometricDistance().scale(weight); + weightedAbsoluteDistanceSum = weightedAbsoluteDistanceSum.add(weightedDistance); + weightSum += weight; + i++; + } + weightedAverageAbsoluteDistance = weightedAbsoluteDistanceSum.scale(1. / weightSum); + final double probabilityOfStartWithOthers = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0, + (1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES + * (weightedAverageAbsoluteDistance.divide(hullLength)-1)))); + if (startIsLine) { + // Now look at the variance, particularly on course side: low variance may mean we are looking at a late starter + // sort such that the boats with the least value (greatest negative amount) comes first and the last ~20% are largely ignored + Collections.sort(distancesToStartLineOfOtherCompetitors, (a, b)->a.getSignedProjectedDistance().compareTo(b.getSignedProjectedDistance())); + Distance weightedSignedDistanceSum = Distance.NULL; + weightSum = 0; + i=0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + weightedSignedDistanceSum = weightedSignedDistanceSum.add(d.getSignedProjectedDistance().scale( + weight)); + weightSum += weight; + i++; + } + final Distance weightedAverageSignedDistance = weightedSignedDistanceSum.scale(1. / weightSum); + i=0; + double signedDistanceVarianceSumInSquareMeters = 0; + weightSum = 0; + for (final AbsoluteGeometricDistanceAndSignedProjectedDistanceToStartLine d : distancesToStartLineOfOtherCompetitors) { + final double differenceFromWeightedAverageInMeters = d.getSignedProjectedDistance().getMeters() - weightedAverageSignedDistance.getMeters(); + final double weight = weight(((double) i)/distancesToStartLineOfOtherCompetitors.size()); + signedDistanceVarianceSumInSquareMeters += weight*differenceFromWeightedAverageInMeters*differenceFromWeightedAverageInMeters; + weightSum += weight; + i++; + } + final double weightedVarianceInSquareMeters = signedDistanceVarianceSumInSquareMeters / weightSum; + final double weightedVarianceRatioToSquaredHullLength = weightedVarianceInSquareMeters / hullLength.getMeters() / hullLength.getMeters(); + final double probabilityOfLateStart = Math.max(WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START, Math.min(1.0, + (1.-(1.-WORST_PENALTY_FOR_OTHER_COMPETITORS_BEING_FAR_FROM_START)/NUMBER_OF_HULL_LENGTHS_DISTANCE_FROM_START_AT_WHICH_WORST_PENALTY_APPLIES + * (weightedVarianceRatioToSquaredHullLength-1)))); + result = 1. - (1.-probabilityOfLateStart) * (1.-probabilityOfStartWithOthers); + } else { + result = probabilityOfStartWithOthers; + } + } else { + // no distance for any other competitor from the start line was available + result = 1.0; + } + return result; + } + + /** + * Idea: sort and cut off the worst 20%. We played with weight functions for averaging the distances such that after sorting them + * in increasing order the smaller ones get greater weight and the "outlier" ones towards the end of the sorted set receive small + * weights. Still, if even the "best" (least) distances are far away, the result will be a low probability. + * A function that could be helpful is this: -atan(20*(x-0.8)) + Pi/2)/Pi. The 0.8 represents the 80% starting from which the + * weight declines sharply. The factor of 20 represents how sharp the decline is. x is between 0 and 1, representing the start + * and the end of the sorted distance collection, respectively. Outputs are between 0 and 1, representing the weight to assign + * to a distance while averaging. + */ + private double weight(double relativePositionInDistanceCollectionInIncreasingOrder) { + final double SHARPNESS_OF_DECLINE = 100; + final double CENTER_OF_DECLINE = 0.8; + return (-Math.atan(SHARPNESS_OF_DECLINE*(relativePositionInDistanceCollectionInIncreasingOrder-CENTER_OF_DECLINE))+Math.PI/2) / Math.PI; + } + private Distance getMinDistanceOrNull(List distances) { Distance result = null; for (Distance d : distances) {