removed old mark passing test stuff that Martin Hanysz once started with; it got obsoleted by Nick's work

Change-Id: I8f679a1c39ab1986a6b6e41dc6a65a7bdb5bb912
This commit is contained in:
Axel Uhl committed 2016-02-15 23:09:05 +01:00
1 parent a87cbdc8f0
commit b5e39cabb4
3 files changed
-1173

No files matched your search

@@ -1,731 +0,0 @@
package com.sap.sailing.domain.test.markpassing;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.PassingInstruction;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.common.impl.WindImpl;
import com.sap.sailing.domain.common.impl.WindSourceImpl;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.test.OnlineTracTracBasedTest;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.impl.DynamicTrackedRaceImpl;
import com.sap.sailing.domain.tractracadapter.ReceiverType;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.tractrac.model.lib.api.event.CreateModelException;
import com.tractrac.subscription.lib.api.SubscriberInitializationException;
/**
* Tests the mark passing algorithm by using the data provided by an {@link OnlineTracTracBasedTest}. Subclasses may
* implement different mark passing detection algorithms.
*
* @author Martin Hanysz
*
*/
public abstract class MartinAbstractMarkPassingTest extends OnlineTracTracBasedTest {
/**
* How many milliseconds may the given and computed mark passings drift apart before being counted as a miss.
*/
private static final int TIMEDELTA_TOLERANCE = 10000;
/**
* Reload the test data from the web?
*/
private boolean forceReload = false;
public MartinAbstractMarkPassingTest() throws MalformedURLException, URISyntaxException {
super();
}
public void setUp() throws IOException, InterruptedException, URISyntaxException, ParseException, SubscriberInitializationException, CreateModelException {
super.setUp();
String raceID = "event_20110609_KielerWoch-505_Race_2";
if (!loadData(raceID) && !forceReload) {
System.out.println("Downloading new data from the web.");
this.setUp("event_20110609_KielerWoch",
/* raceId */raceID, /* liveUri */ null, /* storedUri */ null,
new ReceiverType[] { ReceiverType.MARKPASSINGS, ReceiverType.MARKPOSITIONS,
ReceiverType.RACECOURSE, ReceiverType.RAWPOSITIONS });
OnlineTracTracBasedTest.fixApproximateMarkPositionsForWindReadOut(getTrackedRace(),
new MillisecondsTimePoint(new GregorianCalendar(2011, 05, 23).getTime()));
getTrackedRace().recordWind(
new WindImpl(/* position */null, MillisecondsTimePoint.now(), new KnotSpeedWithBearingImpl(12,
new DegreeBearingImpl(65))), new WindSourceImpl(WindSourceType.WEB));
saveData();
}
}
/**
* Loads stored data for the given raceID or returns false if no data is present.
*
* @param raceID
* - ID of the race to load from disk
* @return true if data was loaded, false if not
*/
private boolean loadData(String raceID) {
String path = null;
File file = new File("resources/");
if (file.exists() && file.isDirectory()) {
for (String fileName : file.list()) {
if (fileName.endsWith(".data") && fileName.contains(raceID)) {
path = "resources/" + fileName;
break;
}
}
}
if (path == null)
return false;
FileInputStream fs = null;
ObjectInputStream os = null;
Object obj = null;
try {
System.out.print("Loading cached data for raceID " + raceID + "...");
fs = new FileInputStream(path);
os = new ObjectInputStream(fs);
obj = os.readObject();
System.out.println("done!");
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
return false;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
if (fs != null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
}
if (obj != null && obj instanceof DynamicTrackedRaceImpl) {
setTrackedRace((DynamicTrackedRaceImpl) obj);
setRace(getTrackedRace().getRace());
return true;
}
return false;
}
/**
* Saves current result of getTrackedRace to disk for future reuse.
*/
private void saveData() {
DynamicTrackedRace trackedRace = getTrackedRace();
String racePath = "resources/" + trackedRace.getRace().getId() + ".data";
FileOutputStream fs = null;
ObjectOutputStream os = null;
try {
System.out.println("Caching data for raceID " + trackedRace.getRace().getId());
File f = new File(racePath);
f.createNewFile();
fs = new FileOutputStream(f);
os = new ObjectOutputStream(fs);
os.writeObject(trackedRace);
} catch (IOException e) {
e.printStackTrace();
return;
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
return;
}
}
if (fs != null) {
try {
fs.close();
} catch (IOException e) {
e.printStackTrace();
return;
}
}
}
}
public void testMarkPassings() {
List<MarkPassing> markPassings = computeAllMarkPassings();
// compare computed mark passings to given ones
int overallMisses = compareCalculatedAndGivenPassings(markPassings, true);
assertEquals("Calculation returned less mark passings than the test data contains", 0, overallMisses);
}
/**
* This method starts the actual mark passing detection algorithm by handing a sequence of {@link GPSFix}es of each
* {@link Competitor} into the {@link MartinAbstractMarkPassingTest#computeMarkPassings(Competitor, GPSFixMoving)}
* method. For every competitor, sequences of mark passings for each waypoint of the course can be calculated. One
* sequence may contain multiple possible mark passings for the same waypoint that may represent the same passing of
* that specific waypoint. The list of sequences returned in the result should contain one sequence for each time
* the waypoint has to be passed to complete the course.
*
* @return a {@link Map} from each {@link Competitor} to a {@link Map} from each {@link Waypoint} to a {@link List}
* of mark passing sequences
*/
protected List<MarkPassing> computeAllMarkPassings() {
ArrayList<MarkPassing> markPassings = new ArrayList<MarkPassing>();
// To imitate GPSFixes that are received over time, we hand the GPSFixes to the detection algorithm in
// chronological order
HashMap<Competitor, Iterator<GPSFixMoving>> fixesIterators = new HashMap<Competitor, Iterator<GPSFixMoving>>();
HashMap<Competitor, GPSFixMoving> nextFixes = new HashMap<Competitor, GPSFixMoving>();
// get all the GPSFix iterators
for (Competitor competitor : getRace().getCompetitors()) {
try {
getTrackedRace().getTrack(competitor).lockForRead();
// from the start of tracking or "the dawn of time"
TimePoint start = getTrackedRace().getStartOfTracking() != null ? getTrackedRace().getStartOfTracking()
: new MillisecondsTimePoint(0);
fixesIterators.put(competitor, getTrackedRace().getTrack(competitor).getRawFixesIterator(start, true));
} finally {
getTrackedRace().getTrack(competitor).unlockAfterRead();
}
}
// initially fill the next fix of each competitor to hand into the mark passing algorithm
for (Entry<Competitor, Iterator<GPSFixMoving>> entry : fixesIterators.entrySet()) {
if (entry.getValue().hasNext())
nextFixes.put(entry.getKey(), entry.getValue().next());
}
while (!nextFixes.isEmpty()) {
// Find the chronologically first unhandled fix.
Entry<Competitor, GPSFixMoving> firstUnhandledFix = null;
for (Entry<Competitor, GPSFixMoving> entry : nextFixes.entrySet()) {
if (firstUnhandledFix == null
|| entry.getValue().getTimePoint().compareTo(firstUnhandledFix.getValue().getTimePoint()) < 0) {
firstUnhandledFix = entry;
}
}
if (firstUnhandledFix != null) {
// compute a possible mark passing
MarkPassing computedPassing = computeMarkPassings(firstUnhandledFix.getKey(),
firstUnhandledFix.getValue());
// replace the fix we just handled by the next fix of the competitor's track
if (fixesIterators.get(firstUnhandledFix.getKey()).hasNext()) {
nextFixes.put(firstUnhandledFix.getKey(), fixesIterators.get(firstUnhandledFix.getKey()).next());
} else {
// If no further fixes can be obtained from the iterator, remove the competitor from the nextFixes
// map.
// This ensures the while loop eventually terminates after all fixes have been analyzed.
nextFixes.remove(firstUnhandledFix.getKey());
}
firstUnhandledFix = null;
if (computedPassing != null) {
// A new mark passing has been detected.
// Find out if it overwrites a previous mark passing or is a new one.
MarkPassing latestPassingOfCompetitor = null;
int index = -1;
for (int i = 0; i < markPassings.size(); i++) {
MarkPassing p = markPassings.get(i);
if (p.getCompetitor().equals(computedPassing.getCompetitor())) {
latestPassingOfCompetitor = p;
index = i;
}
}
if (latestPassingOfCompetitor != null
&& latestPassingOfCompetitor.getWaypoint().equals(computedPassing.getWaypoint())) {
// Latest passing of the competitor was at the same waypoint -> replace it with the new one
markPassings.set(index, computedPassing);
} else {
markPassings.add(computedPassing);
}
}
}
}
return markPassings;
}
/**
* This method starts the actual mark passing detection algorithm. It is supposed to analyze if the given
* {@link Competitor} passed a {@link Mark} at or before the given {@link GPSFixMoving}. If a {@link MarkPassing}
* for a previously passed {@link Waypoint} is detected, it must only return a non-null result, if the returned
* {@link MarkPassing} should overwrite the previously returned one.
*
* @param competitor
* - the {@link Competitor} the given {@link GPSFixMoving} belongs to
* @param fix
* - the {@link GPSFixMoving} representing the latest known position of the given {@link Competitor}
* @return a {@link List} of mark passings
*/
abstract MarkPassing computeMarkPassings(Competitor competitor, GPSFixMoving fix);
/**
* Compares the computed mark passings to the given ones. For each given sequence of mark passings, the one with the
* timestamp closest to the given data is selected.
*
* @param markPassings
* - a {@link Map} from each {@link Competitor} to a {@link Map} from each {@link Waypoint} to a
* {@link List} of mark passing sequences
* @return how many mark passings were missed or detected way too early or too late
*/
protected int compareCalculatedAndGivenPassings(List<MarkPassing> markPassings, boolean printDebug) {
int overallMisses = 0;
Iterable<Competitor> competitors = getRace().getCompetitors();
for (Competitor c : competitors) {
int misses = 0;
NavigableSet<MarkPassing> givenPassingsForCompetitor = getTrackedRace().getMarkPassings(c);
List<MarkPassing> calculatedPassingsForCompetitor = new ArrayList<MarkPassing>();
for (MarkPassing p : markPassings) {
if (p.getCompetitor().equals(c)) {
calculatedPassingsForCompetitor.add(p);
}
}
if (printDebug)
System.out.println("Competitor is " + c.getName());
// Simultaneously iterate over given and computed mark passings and calculate the timedelta
ListIterator<MarkPassing> calculatedPassingsIt = calculatedPassingsForCompetitor.listIterator();
for (MarkPassing givenPassing : givenPassingsForCompetitor) {
Waypoint passedWaypoint = givenPassing.getWaypoint();
MarkPassing calculatedPassing;
if (calculatedPassingsIt.hasNext()) {
calculatedPassing = calculatedPassingsIt.next();
} else {
calculatedPassing = null;
}
if (calculatedPassing != null && calculatedPassing.getWaypoint().equals(passedWaypoint)) {
long timedelta = calculatedPassing.getTimePoint().asMillis()
- givenPassing.getTimePoint().asMillis();
String waypointType = isGate(passedWaypoint) ? "Gate" : "Buoy";
if (Math.abs(timedelta) < TIMEDELTA_TOLERANCE) {
// counts as detected
if (printDebug)
System.out.println("\tTimedelta of calculated mark passing for waypoint "
+ passedWaypoint.getName() + " (" + waypointType + "): " + timedelta + "ms, "
+ calculatedPassing.getTimePoint().asDate() + "(computed) vs. "
+ givenPassing.getTimePoint().asDate() + "(given)");
} else {
// timedelta too huge, counts as missed
misses++;
overallMisses++;
if (printDebug)
System.out.println("\tPassings of waypoint " + passedWaypoint.getName() + " ("
+ waypointType + ") are detected way off (" + timedelta + "ms, "
+ calculatedPassing.getTimePoint().asDate() + ")");
}
} else {
// mark passing for given waypoint
misses++;
overallMisses++;
if (printDebug)
System.out.println("\tNo passings of waypoint " + passedWaypoint.getName());
if (calculatedPassingsIt.hasPrevious()) {
// rewind the iterator to the previous entry
calculatedPassingsIt.previous();
}
}
}
if (printDebug)
System.out.println("\t" + misses + " waypoints were missed.");
}
if (printDebug)
System.out.println("Overall, " + overallMisses + " mark passings were missed.");
return overallMisses;
}
/**
* Get the previous {@link Waypoint} of a given {@link Waypoint}.
*
* @param waypoint
* - the {@link Waypoint} to obtain the predecessor of
* @return a {@link Waypoint} that has to be passed before the given {@link Waypoint} of the {@link Course}, or null
* if none
*/
protected Waypoint getPreviousWaypoint(Waypoint waypoint) {
Course course = getRace().getCourse();
try {
course.lockForRead();
int i = course.getIndexOfWaypoint(waypoint);
for (Waypoint w : course.getWaypoints()) {
if (course.getIndexOfWaypoint(w) == i - 1) {
return w;
}
}
} finally {
course.unlockAfterRead();
}
return null;
}
/**
* Get the next {@link Waypoint} after the given {@link Waypoint}.
*
* @param waypoint
* - the {@link Waypoint} to obtain the successor of
* @return a {@link Waypoint} that has to be passed after the given {@link Waypoint} of the {@link Course}, or null
* if none
*/
protected Waypoint getNextWaypoint(Waypoint waypoint) {
Course course = getRace().getCourse();
try {
course.lockForRead();
int i = course.getIndexOfWaypoint(waypoint);
for (Waypoint w : course.getWaypoints()) {
if (course.getIndexOfWaypoint(w) == i + 1) {
return w;
}
}
} finally {
course.unlockAfterRead();
}
return null;
}
/**
* Get the passing side of any given {@link Mark}. For single buoys this returns
* {@link Waypoint#getPassingInstructions()}, for gates, the passing side of the {@link Waypoint}'s first
* {@link Mark} is calculated and depending on the given {@link Mark} is returned unchanged (m = Mark 1) or inverted
* (m = Mark 2). Since the calculation of the passing side uses the {@link DynamicGPSFixTrack} of the
* {@link Waypoint}'s {@link Mark}s, a {@link TimePoint} has to be specified.
*
* @param waypoint
* - the {@link Waypoint} to get the passing side of
* @param m
* - the {@link Mark} of the {@link Waypoint} to get the passing side of
* @param timePoint
* - the {@link TimePoint} to get the passing side at
* @return the {@link NauticSide} the given {@link Mark} of the given {@link WayPoint} has to be passed on at the
* given {@link TimePoint}
*/
protected PassingInstruction getPassingInstructionOfMark(Waypoint waypoint, Mark m, TimePoint timePoint) {
if (isGate(waypoint)) {
PassingInstruction mark1PassingInstruction = getPassingInstructionForMark1OfGate(waypoint, timePoint);
if (m.equals(waypoint.getMarks().iterator().next())) {
// m is mark 1 of gate
return mark1PassingInstruction;
} else if (mark1PassingInstruction.equals(PassingInstruction.Starboard)) {
// m is mark 2 of gate -> opposite PassingInstruction of mark 1
return PassingInstruction.Port;
} else {
return PassingInstruction.Starboard;
}
} else {
// waypoint is not a gate
return waypoint.getPassingInstructions();
}
}
/**
* Calculate the passing side of {@link Mark} 1 of the given {@link Waypoint} at the given {@link TimePoint}.
*
* @param waypoint
* - the {@link Waypoint} to calculate the passing side for
* @param timePoint
* - the {@link TimePoint} to calculate the passing side at
* @return the {@link NauticSide} the {@link Mark} 1 of the given {@link Waypoint} has to be passed on
*/
protected PassingInstruction getPassingInstructionForMark1OfGate(Waypoint waypoint, TimePoint timePoint) {
if (!isGate(waypoint)) {
return null;
}
// calculate the passing side of the first mark in the waypoints marks
Position mark1Pos = null;
Iterator<Mark> markIt = waypoint.getMarks().iterator();
DynamicGPSFixTrack<Mark, GPSFix> mark1Track = getTrackedRace().getOrCreateTrack(markIt.next());
try {
mark1Track.lockForRead();
mark1Pos = mark1Track.getFirstRawFixAtOrAfter(timePoint).getPosition();
} finally {
mark1Track.unlockAfterRead();
}
Position mark2Pos = null;
DynamicGPSFixTrack<Mark, GPSFix> mark2Track = getTrackedRace().getOrCreateTrack(markIt.next());
try {
mark2Track.lockForRead();
mark2Pos = mark2Track.getFirstRawFixAtOrAfter(timePoint).getPosition();
} finally {
mark2Track.unlockAfterRead();
}
if (getRace().getCourse().getFirstWaypoint().equals(waypoint)) {
// first waypoint of the course, racing direction is towards next waypoint
Position nextWpPos = getTrackedRace().getApproximatePosition(getNextWaypoint(waypoint), timePoint);
Bearing mark1ToNextWp = mark1Pos.getBearingGreatCircle(nextWpPos);
Bearing mark2ToNextWp = mark2Pos.getBearingGreatCircle(nextWpPos);
if (mark1ToNextWp.getDifferenceTo(mark2ToNextWp).getDegrees() > 0) {
return PassingInstruction.Starboard;
} else {
return PassingInstruction.Port;
}
} else {
// racing direction is from previous waypoint
Position prevWpPos = getTrackedRace().getApproximatePosition(getPreviousWaypoint(waypoint), timePoint);
Bearing prevWpToMark1 = prevWpPos.getBearingGreatCircle(mark1Pos);
Bearing prevWpToMark2 = prevWpPos.getBearingGreatCircle(mark2Pos);
if (prevWpToMark1.getDifferenceTo(prevWpToMark2).getDegrees() > 0) {
return PassingInstruction.Port;
} else {
return PassingInstruction.Starboard;
}
}
}
/**
* Checks if a {@link Waypoint} has exactly 2 {@link Mark Marks}.
*
* @param wp
* the {@link Waypoint} to check
* @return <code>true</code> if {@link Waypoint#getMarks()} returns an {@link Iterable} with exactly 2 elements.
*/
protected boolean isGate(Waypoint wp) {
Iterator<Mark> it = wp.getMarks().iterator();
if (it != null && it.hasNext()) {
it.next();
if (it.hasNext()) {
it.next();
if (!it.hasNext()) {
return true;
}
}
}
return false;
}
/**
* Calculates the passing {@link Bearing} of the given {@link Mark} which belongs to the given {@link Waypoint} at
* the given {@link TimePoint}.
*
* @param waypoint
* - the {@link Waypoint} the given {@link Mark} belongs to
* @param mark
* - the {@link Mark} to calculate the passing {@link Bearing} for
* @param time
* - the {@link TimePoint} at which to calculate the passing {@link Bearing}
* @return a {@link Bearing} that represents the line a boat has to pass in order to round the given {@link Mark}
*/
protected Bearing getPassingBearing(Waypoint waypoint, Mark mark, TimePoint time) {
Course course = getRace().getCourse();
Bearing passingBearing = null;
Position markPos = null;
DynamicGPSFixTrack<Mark, GPSFix> markTrack = getTrackedRace().getOrCreateTrack(mark);
try {
markTrack.lockForRead();
markPos = markTrack.getFirstRawFixAtOrAfter(time).getPosition();
} finally {
markTrack.unlockAfterRead();
}
if (waypoint.equals(course.getFirstWaypoint()) || waypoint.equals(course.getLastWaypoint())) {
// passing a line
Mark otherMark = null;
for (Mark m : waypoint.getMarks()) {
if (!m.equals(mark)) {
otherMark = m;
}
}
Position otherMarkPos = null;
DynamicGPSFixTrack<Mark, GPSFix> otherMarkTrack = getTrackedRace().getOrCreateTrack(otherMark);
try {
otherMarkTrack.lockForRead();
otherMarkPos = otherMarkTrack.getFirstRawFixAtOrAfter(time).getPosition();
} finally {
otherMarkTrack.unlockAfterRead();
}
passingBearing = markPos.getBearingGreatCircle(otherMarkPos);
} else {
// passing a single buoy
Bearing bearingToNextWp = null;
Bearing bearingfromPrevWp = null;
Bearing bearingDiff = null;
// calculate bearings to next and from previous waypoint
if (!course.getLastWaypoint().equals(waypoint)) {
// not last waypoint of the race
Waypoint nextWp = getNextWaypoint(waypoint);
Position nextWaypointPos = getTrackedRace().getApproximatePosition(nextWp, time);
bearingToNextWp = markPos.getBearingGreatCircle(nextWaypointPos);
}
if (!course.getFirstWaypoint().equals(waypoint)) {
// not first waypoint of the course
Waypoint prevWp = getPreviousWaypoint(waypoint);
Position prevWaypointPos = getTrackedRace().getApproximatePosition(prevWp, time);
bearingfromPrevWp = prevWaypointPos.getBearingGreatCircle(markPos);
}
// depending on passing side, set the bearing difference
PassingInstruction passingInstruction = getPassingInstructionOfMark(waypoint, mark, time);
if (passingInstruction != null && passingInstruction.equals(PassingInstruction.Starboard)) {
bearingDiff = new DegreeBearingImpl(-90);
} else {
bearingDiff = new DegreeBearingImpl(90);
}
// calculate the passing bearing
if (course.getFirstWaypoint().equals(waypoint)) {
passingBearing = bearingToNextWp.add(bearingDiff);
} else if (course.getLastWaypoint().equals(waypoint)) {
passingBearing = bearingfromPrevWp.add(bearingDiff);
} else {
passingBearing = bearingToNextWp.add(bearingDiff).middle(bearingfromPrevWp.add(bearingDiff));
}
}
return passingBearing;
}
/**
* Finds the {@link Entry} with the best {@link MarkPassing} of the key set. A {@link MarkPassing} is better than
* another if:
* <ol>
* <li>it is on the correct side of the mark and the other one is not</li>
* <li>it is closer to the mark than the other one</li>
* <li>its TimePoint is smaller (it happened earlier) than the other one</li>
* </ol>
*
* @param possiblePassings
* @return
*/
protected Entry<MarkPassing, Position> findBestMarkPassing(Map<MarkPassing, Position> passings) {
if (passings.size() == 1) {
return passings.entrySet().iterator().next();
} else if (passings.isEmpty()) {
return null;
}
Map<MarkPassing, Position> possibleResults = new HashMap<MarkPassing, Position>();
// check if mark was passed on correct side
for (Entry<MarkPassing, Position> entry : passings.entrySet()) {
// find out on which side of the mark was passed
MarkPassing passing = entry.getKey();
Position passPos = entry.getValue();
Position markPos = null;
DynamicGPSFixTrack<Mark, GPSFix> markTrack = getTrackedRace().getOrCreateTrack(
passing.getWaypoint().getMarks().iterator().next());
try {
markTrack.lockForRead();
markPos = markTrack.getLastFixAtOrBefore(passing.getTimePoint()).getPosition();
} finally {
markTrack.unlockAfterRead();
}
Bearing bearingDiff = markPos.getBearingGreatCircle(passPos).getDifferenceTo(
getPassingBearing(passing.getWaypoint(), passing.getWaypoint().getMarks().iterator().next(),
passing.getTimePoint()));
if (Math.abs(bearingDiff.getDegrees()) < 90) {
// passed on correct side of mark
possibleResults.put(entry.getKey(), entry.getValue());
}
}
if (possibleResults.size() > 0) {
// All possible passings are on correct side, pass them to the distance test.
// If all passings were on the wrong side, passings is not overwritten and they are still passed to the
// distance test.
passings.putAll(possibleResults);
}
possibleResults.clear();
// find the mark passing with the smallest distance to the mark
for (Entry<MarkPassing, Position> entry : passings.entrySet()) {
// find out how far away the mark was passed
MarkPassing passing = entry.getKey();
Position passPos = entry.getValue();
Position markPos = null;
DynamicGPSFixTrack<Mark, GPSFix> markTrack = getTrackedRace().getOrCreateTrack(
passing.getWaypoint().getMarks().iterator().next());
try {
markTrack.lockForRead();
markPos = markTrack.getLastFixAtOrBefore(passing.getTimePoint()).getPosition();
} finally {
markTrack.unlockAfterRead();
}
if (possibleResults.isEmpty()) {
possibleResults.put(passing, passPos);
} else {
Distance shortestDistance = markPos
.getDistance(possibleResults.entrySet().iterator().next().getValue());
Distance newDistance = markPos.getDistance(passPos);
if (shortestDistance.compareTo(newDistance) == 0) {
// same distance, add to possible results
possibleResults.put(passing, passPos);
} else if (shortestDistance.compareTo(newDistance) > 0) {
// shorter distance, remove all other possible results and add this one
possibleResults.clear();
possibleResults.put(passing, passPos);
}
}
}
if (possibleResults.size() == 1) {
// only one possible result left, we're done
return possibleResults.entrySet().iterator().next();
} else if (possibleResults.size() > 1) {
// all possible passings have the same distance to the mark, pass them to the timing test
passings.putAll(possibleResults);
}
possibleResults.clear();
// find the mark passing with the smallest timestamp
for (Entry<MarkPassing, Position> entry : passings.entrySet()) {
// find out how far away the mark was passed
MarkPassing passing = entry.getKey();
Position passPos = entry.getValue();
if (possibleResults.isEmpty()) {
possibleResults.put(passing, passPos);
} else {
TimePoint smallestTime = possibleResults.entrySet().iterator().next().getKey().getTimePoint();
TimePoint newTime = passing.getTimePoint();
if (smallestTime.compareTo(newTime) == 0) {
// same timestamp, add to possible results
possibleResults.put(passing, passPos);
} else if (smallestTime.compareTo(newTime) > 0) {
// smaller timestamp, remove all other possible results and add this one
possibleResults.clear();
possibleResults.put(passing, passPos);
}
}
}
if (possibleResults.size() > 0) {
// all possible passings have the same timestamp, just return the first one
return possibleResults.entrySet().iterator().next();
}
return null;
}
}
@@ -1,249 +0,0 @@
package com.sap.sailing.domain.test.markpassing;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Distance;
import com.sap.sailing.domain.common.PassingInstruction;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.MarkPassing;
/**
* This class tests a mark passing detection algorithm that is based purely on bearing analyses
* from the {@link Course}'s {@link Mark}s to the GPS fixes of the {@link Competitor}'s boats.
* It uses a "hot zone" around the {@link Mark}s to avoid unnecessary testing of {@link Mark}s
* too far away from the boat.
*
* @author Martin Hanysz
*
*/
public class MartinMarkPassingBearingBasedTest extends MartinAbstractMarkPassingTest {
/**
* Controls the size of the hot zone around buoys. It is multiplied with the boat length.
*/
private static final double HOT_ZONE_TO_BOAT_LENGHT_RATIO = 10.0;
private Map<Competitor, Entry<MarkPassing, Position>> lastCompetitorPassings;
public MartinMarkPassingBearingBasedTest() throws MalformedURLException, URISyntaxException {
super();
lastCompetitorPassings = new HashMap<Competitor, Entry<MarkPassing, Position>>();
}
@Override
MarkPassing computeMarkPassings(Competitor competitor, GPSFixMoving fix) {
Map<MarkPassing, Position> possibleMarkPassings = new HashMap<MarkPassing, Position>();
// find out which waypoints to check (last waypoint passed and the next)
List<Waypoint> waypointsToCheck = new ArrayList<Waypoint>();
Course course = getRace().getCourse();
try {
course.lockForRead();
if (!lastCompetitorPassings.containsKey(competitor)) {
// no previous passings, competitor needs to pass 1st waypoint
waypointsToCheck.add(course.getFirstWaypoint());
} else {
// previous passing, competitor either passes same waypoint again or the next waypoint
waypointsToCheck.add(lastCompetitorPassings.get(competitor).getKey().getWaypoint());
Waypoint next = getNextWaypoint(lastCompetitorPassings.get(competitor).getKey().getWaypoint());
if (next != null) {
waypointsToCheck.add(next);
}
}
} finally {
course.unlockAfterRead();
}
for (Waypoint wp : waypointsToCheck) {
MarkPassing passing = null;
if (isGate(wp)) {
if (wp.equals(course.getFirstWaypoint()) ||
wp.equals(course.getLastWaypoint())) {
// start or finish line
passing = getLinePassing(wp, fix, competitor);
} else {
// waypoint is a gate, either pass mark 1 or mark 2 of the gate
Iterator<Mark> markIterator = wp.getMarks().iterator();
while (passing == null && markIterator.hasNext()) {
passing = getBuoyPassing(wp, markIterator.next(), fix, competitor);
}
}
} else {
// waypoint is single buoy
passing = getBuoyPassing(wp, wp.getMarks().iterator().next(), fix, competitor);
}
if (passing != null) {
// detected a passing
possibleMarkPassings.put(passing, fix.getPosition());
}
}
Entry<MarkPassing, Position> result = null;
if (possibleMarkPassings.size() > 0) {
Entry<MarkPassing, Position> lastPassing = lastCompetitorPassings.get(competitor);
Entry<MarkPassing, Position> possiblePassing = possibleMarkPassings.entrySet().iterator().next();
if (lastPassing != null &&
lastPassing.getKey().getWaypoint().equals(possiblePassing.getKey().getWaypoint())) {
// previous passing detected & passed same waypoint again
possibleMarkPassings.put(lastPassing.getKey(), lastPassing.getValue());
}
result = findBestMarkPassing(possibleMarkPassings);
}
if (result != null) {
lastCompetitorPassings.put(competitor, result);
return result.getKey();
} else {
return null;
}
}
/**
* Check if the given {@link Waypoint} was passed with the given {@link GPSFixMoving} of the given {@link Competitor}.
* @param waypoint - the {@link Waypoint} to check
* @param mark - the mark of the given {@link Waypoint} to be checked
* @param fix - the {@link GPSFixMoving} to check
* @param competitor - the {@link Competitor} the given fix belongs to
* @return a {@link MarkPassing} if the fix represents a passing of the given {@link Waypoint}, null if not
*/
private MarkPassing getBuoyPassing(Waypoint waypoint, Mark mark, GPSFixMoving fix, Competitor competitor) {
if (waypoint == null || getTrackedRace().getStartOfRace().compareTo(fix.getTimePoint()) > 0) {
// no waypoint or race not started yet
return null;
}
// get position of given mark
Position markPos = null;
DynamicGPSFixTrack<Mark, GPSFix> markTrack = getTrackedRace().getOrCreateTrack(mark);
try {
markTrack.lockForRead();
markPos = markTrack.getFirstRawFixAtOrAfter(fix.getTimePoint()).getPosition();
} finally {
markTrack.unlockAfterRead();
}
// check if boat is in "hot zone"
// boat in "hot zone" <=> distance from boat to mark < boat length * x
if (fix.getPosition().getDistance(markPos).compareTo(competitor.getBoat().getBoatClass().getHullLength().scale(HOT_ZONE_TO_BOAT_LENGHT_RATIO)) > 0) {
// not in "hot zone"
return null;
}
// calculate mark passing bearing for waypoint
Bearing markToBoatBearing = markPos.getBearingGreatCircle(fix.getPosition());
Bearing passingBearing = getPassingBearing(waypoint, mark, fix.getTimePoint());
PassingInstruction passingInstruction = getPassingInstructionOfMark(waypoint, mark, fix.getTimePoint());
double passingBearingDelta = markToBoatBearing.getDifferenceTo(passingBearing).getDegrees();
if (passingInstruction == null ||
(passingInstruction.equals(PassingInstruction.Port) &&
passingBearingDelta > 0)) {
// markToBoatBearing is smaller than passing bearing -> passed on port
return DomainFactory.INSTANCE.createMarkPassing(fix.getTimePoint(), waypoint, competitor);
} else if ( passingInstruction.equals(PassingInstruction.Starboard) &&
passingBearingDelta < 0) {
// markToBoatBearing is greater than passing bearing -> passed on stb
return DomainFactory.INSTANCE.createMarkPassing(fix.getTimePoint(), waypoint, competitor);
}
return null;
}
/**
* Check if the line represented by the given {@link Waypoint} was passed with the given {@link GPSFixMoving} of the given {@link Competitor}.
* @param waypoint - the {@link Waypoint} to check
* @param fix - the {@link GPSFixMoving} to check
* @param competitor - the {@link Competitor} the given fix belongs to
* @return a {@link MarkPassing} if the fix represents a passing of the line of the given {@link Waypoint}, null if not
*/
private MarkPassing getLinePassing(Waypoint waypoint, GPSFixMoving fix, Competitor competitor) {
if (waypoint == null || !isGate(waypoint)) {
// no waypoint, waypoint not a gate, or race not started yet
return null;
}
/* A boat passes the line, if the bearing from the boat to mark 1 of the gate equals
* the bearing from mark 2 of the gate to mark 1 of the gate AND the bearing from the boat
* to mark 2 of the gate equals the bearing from mark 1 of the gate to mark 2 of the gate.
*/
// get tracks of both marks
Iterator<Mark> markIt = waypoint.getMarks().iterator();
DynamicGPSFixTrack<Mark, GPSFix> mark1Track = null;
Mark mark1 = null;
DynamicGPSFixTrack<Mark, GPSFix> mark2Track = null;
Mark mark2 = null;
if (markIt.hasNext()) {
mark1 = markIt.next();
mark1Track = getTrackedRace().getOrCreateTrack(mark1);
if (markIt.hasNext())
mark2 = markIt.next();
mark2Track = getTrackedRace().getOrCreateTrack(mark2);
}
// get positions for both marks of the gate
Position mark1Pos = null;
Position mark2Pos = null;
try {
mark1Track.lockForRead();
try {
mark2Track.lockForRead();
mark1Pos = mark1Track.getFirstRawFixAtOrAfter(fix.getTimePoint()).getPosition();
mark2Pos = mark2Track.getFirstRawFixAtOrAfter(fix.getTimePoint()).getPosition();
} finally {
mark2Track.unlockAfterRead();
}
} finally {
mark1Track.unlockAfterRead();
}
// calculate if boat is in "hot zone" of gate
// in "hot zone" <=> distance from any mark of gate to boat < length of line between marks
Distance lineLength = mark1Pos.getDistance(mark2Pos);
if ( mark1Pos.getDistance(fix.getPosition()).compareTo(lineLength) > 0 &&
mark2Pos.getDistance(fix.getPosition()).compareTo(lineLength) > 0) {
// not in "hot zone"
return null;
}
// calculate bearings from boat to 1st and 2nd mark, from 1st mark to 2nd mark, and from 2nd mark to 1st mark
Bearing boatToMark1Bearing = fix.getPosition().getBearingGreatCircle(mark1Pos);
Bearing mark1ToMark2Bearing = mark1Pos.getBearingGreatCircle(mark2Pos);
Bearing boatToMark2Bearing = fix.getPosition().getBearingGreatCircle(mark2Pos);
Bearing mark2ToMark1Bearing = mark2Pos.getBearingGreatCircle(mark1Pos);
PassingInstruction PassingInstructionOfMark1OfGate = getPassingInstructionForMark1OfGate(waypoint, fix.getTimePoint());
double mark1PassingBearingDelta = boatToMark1Bearing.getDifferenceTo(mark2ToMark1Bearing).getDegrees();
double mark2PassingBearingDelta = boatToMark2Bearing.getDifferenceTo(mark1ToMark2Bearing).getDegrees();
//Mark closestMark = mark1;
if (mark2 != null && fix.getPosition().getDistance(mark1Pos).compareTo(fix.getPosition().getDistance(mark2Pos)) < 0) {
//closestMark = mark2;
}
if ( PassingInstructionOfMark1OfGate.equals(PassingInstruction.Starboard) &&
mark1PassingBearingDelta < 0 &&
mark2PassingBearingDelta > 0) {
// gate passed if mark1 had to be passed on stb
return DomainFactory.INSTANCE.createMarkPassing(fix.getTimePoint(), waypoint,competitor);
} else if ( PassingInstructionOfMark1OfGate.equals(PassingInstruction.Port) &&
mark1PassingBearingDelta > 0 &&
mark2PassingBearingDelta < 0) {
// gate passed if mark1 had to be passed on port
return DomainFactory.INSTANCE.createMarkPassing(fix.getTimePoint(), waypoint, competitor);
}
return null;
}
}
@@ -1,193 +0,0 @@
package com.sap.sailing.domain.test.markpassing;
import static org.junit.Assert.assertTrue;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Before;
import org.junit.Test;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.tracking.GPSFix;
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
import com.sap.sailing.domain.markpassingcalculation.splining.HermiteCurve;
import com.sap.sailing.domain.markpassingcalculation.splining.StraightLine;
import com.sap.sailing.domain.markpassingcalculation.splining.Vector2D;
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sse.common.TimePoint;
/**
* This class tests a mark passing detection algorithm that interpolates the boats' positions between
* their GPS fixes using Hermite Splines.
* @author Martin Hanysz
*
*/
public class MartinMarkPassingSplineBasedTest extends MartinAbstractMarkPassingTest {
// a factor to scale the tangents calculated from boat speed and bearing
private static double TANGENT_SCALING_FACTOR = 100;
private Map<Competitor, Entry<MarkPassing, Position>> lastCompetitorPassings;
public MartinMarkPassingSplineBasedTest() throws MalformedURLException, URISyntaxException {
super();
lastCompetitorPassings = new HashMap<Competitor, Entry<MarkPassing, Position>>();
}
@Before
public void setTangentScaling() {
TANGENT_SCALING_FACTOR = 100.0;
}
@Test
public void testTangentScaling() {
Map<Double, Integer> results = new HashMap<Double, Integer>();
double s = 100.0;
double div = 10.0;
while (s > 0.000000001) {
MartinMarkPassingSplineBasedTest.TANGENT_SCALING_FACTOR = s;
List<MarkPassing> markPassings = computeAllMarkPassings();
// compare computed mark passings to given ones
int overallMisses = compareCalculatedAndGivenPassings(markPassings, false);
results.put(s, overallMisses);
s /= div;
}
boolean success = false;
for (Entry<Double, Integer> e : results.entrySet()) {
System.out.println("Scaling: " + e.getKey() + ", Misses: " + e.getValue());
if (e.getValue() == 0) {
success = true;
}
}
assertTrue("No setting for the scaling factor resulted in 0 missed waypoints.", success);
}
/* (non-Javadoc)
* @see com.sap.sailing.domain.test.AbstractMarkPassingTest#computeMarkPassings()
*/
@Override
MarkPassing computeMarkPassings(Competitor competitor, GPSFixMoving fix) {
// interpolate the course of the competitor from the previous fix to the given fix
DynamicGPSFixTrack<Competitor, GPSFixMoving> competitorTrack = getTrackedRace().getTrack(competitor);
GPSFixMoving prevFix = null;
try {
competitorTrack.lockForRead();
prevFix = competitorTrack.getLastRawFixBefore(fix.getTimePoint());
} finally {
competitorTrack.unlockAfterRead();
}
if (prevFix == null) {
return null;
}
Vector2D point1 = new Vector2D(prevFix.getPosition());
Vector2D point2 = new Vector2D(fix.getPosition());
Vector2D tangent1 = new Vector2D(prevFix.getSpeed().getBearing(), prevFix.getSpeed().getKnots() * TANGENT_SCALING_FACTOR /* abh�ngig von Bootsklasse, zeitl. Abstand zw. Fixes */);
Vector2D tangent2 = new Vector2D(fix.getSpeed().getBearing(), fix.getSpeed().getKnots() * TANGENT_SCALING_FACTOR);
HermiteCurve interpolatedCourse = new HermiteCurve(point1, point2, tangent1, tangent2);
// find out which waypoints to check (last waypoint passed and the next)
List<Waypoint> waypointsToCheck = new ArrayList<Waypoint>();
Course raceCourse = getRace().getCourse();
try {
raceCourse.lockForRead();
if (!lastCompetitorPassings.containsKey(competitor)) {
// no previous passings, competitor needs to pass 1st waypoint
waypointsToCheck.add(raceCourse.getFirstWaypoint());
} else {
waypointsToCheck.add(lastCompetitorPassings.get(competitor).getKey().getWaypoint());
Waypoint next = getNextWaypoint(lastCompetitorPassings.get(competitor).getKey().getWaypoint());
if (next != null) {
waypointsToCheck.add(next);
}
}
} finally {
raceCourse.unlockAfterRead();
}
// check if the interpolated course passes one of the waypoints to check
Map<MarkPassing, Position> possiblePassings = new HashMap<MarkPassing, Position>();
for (Waypoint wp : waypointsToCheck) {
StraightLine passingLine = null;
List<Position> markPositions = new ArrayList<Position>();
for (Mark m : wp.getMarks()) {
DynamicGPSFixTrack<Mark, GPSFix> markTrack = getTrackedRace().getOrCreateTrack(m);
try {
markTrack.lockForRead();
markPositions.add(markTrack.getLastFixAtOrBefore(fix.getTimePoint()).getPosition());
} finally {
markTrack.unlockAfterRead();
}
}
Vector2D mark1Pos = new Vector2D(markPositions.get(0));
Map<Vector2D, Double> intersections = null;
//Mark passedMark = wp.getMarks().iterator().next();
if (isGate(wp)) {
if (wp.equals(raceCourse.getFirstWaypoint()) || wp.equals(raceCourse.getLastWaypoint())) {
// passing start or finish line
Vector2D mark2Pos = new Vector2D(markPositions.get(1));
passingLine = new StraightLine(mark1Pos, mark2Pos.subtract(mark1Pos));
intersections = interpolatedCourse.intersectWith(passingLine);
} else {
// passing either mark of the gate
for (Mark mark : wp.getMarks()) {
Bearing passingBearing = getPassingBearing(wp, mark, fix.getTimePoint());
Vector2D passingVector = new Vector2D(passingBearing, 1.0);
passingLine = new StraightLine(mark1Pos, passingVector);
intersections = interpolatedCourse.intersectWith(passingLine);
if (intersections.size() > 0) {
// already found a passing of a mark
//passedMark = mark;
break;
}
}
}
} else {
// passing single buoy
Bearing passingBearing = getPassingBearing(wp, wp.getMarks().iterator().next(), fix.getTimePoint());
Vector2D passingVector = new Vector2D(passingBearing, 1.0);
passingLine = new StraightLine(mark1Pos, passingVector);
intersections = interpolatedCourse.intersectWith(passingLine);
}
for (Entry<Vector2D, Double> entry : intersections.entrySet()) {
TimePoint time = fix.getTimePoint();
TimePoint prevTime = prevFix.getTimePoint();
// t_pass = t_fix - (t_prev - t_fix) * interpolationParameterOfIntersection
// If the intersection is at 75% of the interpolated course, t_pass is at 75% between t_prev and t_fix.
// This cast truncates the milliseconds, which is acceptable at the moment.
TimePoint timePoint = time.minus((long) ((prevTime.asMillis() - time.asMillis()) * entry.getValue()));
possiblePassings.put(DomainFactory.INSTANCE.createMarkPassing(timePoint, wp, competitor), new DegreePosition(entry.getKey().y(), entry.getKey().x()));
}
}
Entry<MarkPassing, Position> result = null;
if (possiblePassings.size() > 0) {
Entry<MarkPassing, Position> lastPassing = lastCompetitorPassings.get(competitor);
Entry<MarkPassing, Position> possiblePassing = possiblePassings.entrySet().iterator().next();
if (lastPassing != null &&
lastPassing.getKey().getWaypoint().equals(possiblePassing.getKey().getWaypoint())) {
// previous passing detected & passed same waypoint again
possiblePassings.put(lastPassing.getKey(), lastPassing.getValue());
}
result = findBestMarkPassing(possiblePassings);
}
if (result != null) {
lastCompetitorPassings.put(competitor, result);
return result.getKey();
} else {
return null;
}
}
}