progress towards creating a RaceDefinition and TrackedRace from the archived swiss timing data

This commit is contained in:
Axel Uhl
2012-12-18 00:03:39 +01:00
parent 2cbace8c55
commit 5821808628
7 changed files with 290 additions and 38 deletions
@@ -1,7 +1,10 @@
package com.sap.sailing.domain.swisstimingadapter;
import java.util.List;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.ControlPoint;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.Nationality;
import com.sap.sailing.domain.base.RaceDefinition;
@@ -24,9 +27,11 @@ public interface DomainFactory {
Regatta getOrCreateRegatta(String raceID, TrackedRegattaRegistry trackedRegattaRegistry);
Nationality getOrCreateNationality(String nationalityName);
Nationality getOrCreateNationality(String threeLetterIOCCode);
Competitor getOrCreateCompetitor(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass);
Competitor getOrCreateCompetitor(String boatID, String threeLetterIOCCode, String name, BoatClass boatClass);
RaceDefinition createRaceDefinition(Regatta regatta, Race race, StartList startList, com.sap.sailing.domain.swisstimingadapter.Course course);
@@ -46,4 +51,11 @@ public interface DomainFactory {
boolean canSendRequests, long delayToLiveInMillis,
SwissTimingFactory swissTimingFactory, DomainFactory domainFactory, WindStore windStore,
RaceSpecificMessageLoader messageLoader);
BoatClass getOrCreateBoatClassFromRaceID(String raceID);
ControlPoint getOrCreateControlPoint(Iterable<String> devices);
RaceDefinition createRaceDefinition(Regatta regatta, String raceID, Iterable<Competitor> competitors,
List<ControlPoint> courseDefinition);
}
@@ -132,6 +132,28 @@ public class DomainFactoryImpl implements DomainFactory {
return result;
}
@Override
public Competitor getOrCreateCompetitor(String boatID, String threeLetterIOCCode, String name,
BoatClass boatClass) {
return getOrCreateCompetitor(new CompetitorImpl(boatID, threeLetterIOCCode, name), boatClass);
}
@Override
public RaceDefinition createRaceDefinition(Regatta regatta, String raceID, Iterable<Competitor> competitors,
List<ControlPoint> courseDefinition) {
List<Waypoint> waypoints = new ArrayList<>();
for (ControlPoint controlPoint : courseDefinition) {
Waypoint waypoint = baseDomainFactory.createWaypoint(controlPoint);
waypoints.add(waypoint);
}
com.sap.sailing.domain.base.Course domainCourse = new CourseImpl("Course", waypoints);
BoatClass boatClass = getOrCreateBoatClassFromRaceID(raceID);
logger.info("Creating RaceDefinitionImpl for race "+raceID);
RaceDefinition result = new RaceDefinitionImpl(raceID, domainCourse, boatClass, competitors);
regatta.addRace(result);
return result;
}
@Override
public RaceDefinition createRaceDefinition(Regatta regatta, Race race, StartList startList, Course course) {
com.sap.sailing.domain.base.Course domainCourse = createCourse(race.getDescription(), course);
@@ -144,18 +166,19 @@ public class DomainFactoryImpl implements DomainFactory {
return result;
}
private BoatClass getOrCreateBoatClassFromRaceID(String raceID) {
@Override
public BoatClass getOrCreateBoatClassFromRaceID(String raceID) {
BoatClass result;
/*
SAM102000 Men's Windsurfer = Windsufer M�nner RS:X
SAM102000 Men's Windsurfer = Windsurfer Maenner RS:X
SAW102000 Women's Windsurfer = Windsurfer Damen RS:X
SAM004000 Men's One Person Dinghy = Laser M�nner
SAM004000 Men's One Person Dinghy = Laser Maenner
SAW103000 Women's One Person Dinghy = Laser Damen Laser Radial
SAM002000 Men's One Person Dinghy Heavy = Finn Dinghy M�nner
SAM005000 Men's Two Person Dinghy = 470er M�nner
SAM002000 Men's One Person Dinghy Heavy = Finn Dinghy Maenner
SAM005000 Men's Two Person Dinghy = 470er Maenner
SAW005000 Women's Two Person Dinghy = 470er Damen
SAM009000 Men's Skiff = 49er M�nner
SAM007000 Men's Keelboat = Starboot M�nner
SAM009000 Men's Skiff = 49er Maenner
SAM007000 Men's Keelboat = Starboot Maenner
SAW010000 Women's Match Racing = Matchrace Damen Elliott 6M (modified)
*/
if (raceID.toUpperCase().startsWith("SA") && raceID.length() == 9) {
@@ -191,7 +214,8 @@ public class DomainFactoryImpl implements DomainFactory {
return result;
}
private ControlPoint getOrCreateControlPoint(Iterable<String> devices) {
@Override
public ControlPoint getOrCreateControlPoint(Iterable<String> devices) {
ControlPoint result;
synchronized (controlPointCache) {
result = controlPointCache.get(devices);
@@ -270,8 +294,8 @@ public class DomainFactoryImpl implements DomainFactory {
}
@Override
public Nationality getOrCreateNationality(String nationalityName) {
return baseDomainFactory.getOrCreateNationality(nationalityName);
public Nationality getOrCreateNationality(String threeLetterIOCCode) {
return baseDomainFactory.getOrCreateNationality(threeLetterIOCCode);
}
@Override
@@ -9,3 +9,4 @@ Fragment-Host: com.sap.sailing.domain.swisstimingreplayadapter
Import-Package: org.json.simple,
org.json.simple.parser,
org.junit;version="4.8.2"
Require-Bundle: com.sap.sailing.domain.test
@@ -8,9 +8,12 @@ import java.util.List;
import org.junit.Ignore;
import org.junit.Test;
import com.sap.sailing.domain.swisstimingadapter.DomainFactory;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayRace;
import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayParserImpl;
import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayServiceImpl;
import com.sap.sailing.domain.swisstimingreplayadapter.impl.SwissTimingReplayToDomainAdapter;
import com.sap.sailing.domain.test.DummyTrackedRegattaRegistry;
public class SwissTimingReplayAdapterServiceTest {
@@ -76,10 +79,19 @@ public class SwissTimingReplayAdapterServiceTest {
assertEquals(2418, replayCountListener.rankingMarkCount);
}
@Test
public void testRaceData_SAW010955_20120802_WithDomainAdapter() throws Exception {
SwissTimingReplayToDomainAdapter replayListener = new SwissTimingReplayToDomainAdapter(null, DomainFactory.INSTANCE,
new DummyTrackedRegattaRegistry());
new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005906.20120805.replay"), replayListener);
}
@Test
@Ignore
public void printRaceData_SAW005906_20120805() throws Exception {
new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005906.20120805.replay"), new SwissTimingReplayPrintListener());
new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005906.20120805.replay"),
new SwissTimingReplayPrintListener());
}
@Test
@@ -8,6 +8,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Bundle-ActivationPolicy: lazy
Require-Bundle: com.sap.sailing.domain.common,
com.sap.sailing.domain,
org.json.simple;bundle-version="1.1.0"
org.json.simple;bundle-version="1.1.0",
com.sap.sailing.domain.swisstimingadapter,
com.googlecode.java-diff-utils;bundle-version="1.3.0"
Export-Package: com.sap.sailing.domain.swisstimingreplayadapter,
com.sap.sailing.domain.swisstimingreplayadapter.impl;x-friends:="com.sap.sailing.domain.swisstimingreplayadapter.test"
@@ -86,7 +86,7 @@ public interface SwissTimingReplayListener {
void competitorsCount(short competitorsCount);
void competitor(int hashValue, String nation, String sailNumber, String name, CompetitorStatus competitorStatus, BoatType boatType,
void competitor(int hashValue, String nation, String sailNumberOrTrackerID, String name, CompetitorStatus competitorStatus, BoatType boatType,
short cRank_Bracket, short cnPoints_x10_Bracket, short ctPoints_x10_Winner);
/**
@@ -1,22 +1,50 @@
package com.sap.sailing.domain.swisstimingreplayadapter.impl;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.sap.sailing.domain.base.BoatClass;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.ControlPoint;
import com.sap.sailing.domain.base.Course;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.SpeedWithBearing;
import com.sap.sailing.domain.base.Waypoint;
import com.sap.sailing.domain.base.impl.KnotSpeedWithBearingImpl;
import com.sap.sailing.domain.base.impl.MillisecondsTimePoint;
import com.sap.sailing.domain.common.Bearing;
import com.sap.sailing.domain.common.Position;
import com.sap.sailing.domain.common.TimePoint;
import com.sap.sailing.domain.common.WindSourceType;
import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
import com.sap.sailing.domain.common.impl.Util;
import com.sap.sailing.domain.common.impl.WindSourceWithAdditionalID;
import com.sap.sailing.domain.swisstimingadapter.DomainFactory;
import com.sap.sailing.domain.swisstimingreplayadapter.CompetitorStatus;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayListener;
import com.sap.sailing.domain.swisstimingreplayadapter.SwissTimingReplayParser;
import com.sap.sailing.domain.tracking.DynamicTrackedRace;
import com.sap.sailing.domain.tracking.GPSFixMoving;
import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sailing.domain.tracking.WindTrack;
import com.sap.sailing.domain.tracking.impl.EmptyWindStore;
import com.sap.sailing.domain.tracking.impl.GPSFixMovingImpl;
import com.sap.sailing.domain.tracking.impl.MarkPassingImpl;
import com.sap.sailing.domain.tracking.impl.WindImpl;
import difflib.PatchFailedException;
/**
* Turns the data received through the {@link SwissTimingReplayListener} callback interface into domain objects, creating
@@ -30,6 +58,10 @@ import com.sap.sailing.domain.tracking.TrackedRace;
*
*/
public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter {
private static final Logger logger = Logger.getLogger(SwissTimingReplayToDomainAdapter.class.getName());
private final DomainFactory domainFactory;
private final Map<String, RaceDefinition> racePerRaceID;
private final Map<String, DynamicTrackedRace> trackedRacePerRaceID;
@@ -49,11 +81,68 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter {
*/
private Position referenceLocation;
public SwissTimingReplayToDomainAdapter() {
private final Map<String, Set<Competitor>> competitorsPerRaceID;
private final Map<String, Map<String, Mark>> marksPerRaceIDPerMarkID;
private final Map<String, TimePoint> bestStartTimePerRaceID;
private final Map<String, TimePoint> raceTimePerRaceID;
/**
* When the first mark definition of a course sequence is received, this member holds <code>null</code> and is then
* initialized with a valid list. The list then accumulates the marks until the tracker count is received which marks
* the end of the course sequence and the beginning of the tracker messages. The course is then compared to the current
* course. If the race hasn't even been created, this can be done now. Otherwise, the new course definition is compared
* to the current race's course definition, and if necessary, a course change is performed. Then, this member is set
* to <code>null</code> again to show that the course has been consumed and updated to the race definition if necessary.
*/
private List<ControlPoint> currentCourseDefinition;
private final Regatta regatta;
private final TrackedRegattaRegistry trackedRegattaRegistry;
private final Map<Integer, Mark> markByHashValue;
private final Map<Integer, Competitor> competitorByHashValue;
/**
* If a wind speed and direction was transmitted with the last mark message, it is recorded for the mark. When the next
* mark position tracker message is received, a look up in this map is performed. If a wind speed/direction is found, it is
* added to the tracked race's {@link WindSourceType#EXPEDITION} wind track.
*/
private final Map<ControlPoint, SpeedWithBearing> windAtControlPoint;
/**
* Records the next mark for each competitor numerically
*/
private final Map<Competitor, Short> lastNextMark;
/**
* @param regatta
* the regatta to associate the race(s) received by the listener with, or <code>null</code> to force the
* use / creation of a default regatta per race
*/
public SwissTimingReplayToDomainAdapter(Regatta regatta, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry) {
this.regatta = null;
this.trackedRegattaRegistry = trackedRegattaRegistry;
racePerRaceID = new HashMap<>();
trackedRacePerRaceID = new HashMap<>();
bestStartTimePerRaceID = new HashMap<>();
raceTimePerRaceID = new HashMap<>();
competitorsPerRaceID = new HashMap<>();
marksPerRaceIDPerMarkID = new HashMap<>();
markByHashValue = new HashMap<>();
competitorByHashValue = new HashMap<>();
windAtControlPoint = new HashMap<>();
lastNextMark = new HashMap<>();
this.domainFactory = domainFactory;
}
private BoatClass getCurrentBoatClass() {
return domainFactory.getOrCreateBoatClassFromRaceID(currentRaceID);
}
@Override
public void referenceTimestamp(long referenceTimestampMillis) {
referenceTimePoint = new MillisecondsTimePoint(referenceTimestampMillis);
@@ -68,49 +157,161 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter {
public void raceID(String raceID) {
currentRaceID = raceID;
}
private boolean isValid(int threeByteValue) {
return threeByteValue == 2<<24 - 1;
}
@Override
public void frameMetaData(byte cid, int raceTime, int startTime, int estimatedStartTime, RaceStatus raceStatus,
short distanceToNextMark, Weather weather, short humidity, short temperature, String messageText,
byte cFlag, byte rFlag, byte duration, short nm) {
// TODO Auto-generated method stub
super.frameMetaData(cid, raceTime, startTime, estimatedStartTime, raceStatus, distanceToNextMark, weather, humidity,
temperature, messageText, cFlag, rFlag, duration, nm);
if (isValid(startTime)) {
bestStartTimePerRaceID.put(currentRaceID, getTimePoint(startTime));
} else if (isValid(estimatedStartTime)) {
bestStartTimePerRaceID.put(currentRaceID, getTimePoint(estimatedStartTime));
}
if (bestStartTimePerRaceID.containsKey(currentRaceID)) {
raceTimePerRaceID.put(currentRaceID, bestStartTimePerRaceID.get(currentRaceID).plus(1000*raceTime));
}
}
private TimePoint getTimePoint(int threeByteTimeSpecInSecondsSinceMidnight) {
assert isValid(threeByteTimeSpecInSecondsSinceMidnight);
return getMidnight(referenceTimePoint).plus(1000 * threeByteTimeSpecInSecondsSinceMidnight);
}
private TimePoint getMidnight(TimePoint referenceTimePoint) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(referenceTimePoint.asMillis());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return new MillisecondsTimePoint(cal.getTimeInMillis());
}
@Override
public void competitor(int hashValue, String nation, String sailNumber, String name,
public void competitor(int hashValue, String threeLetterIOCCode, String sailNumberOrTrackerID, String name,
CompetitorStatus competitorStatus, BoatType boatType, short cRank_Bracket, short cnPoints_x10_Bracket,
short ctPoints_x10_Winner) {
// TODO distinguish between competitors and non-competitors; all non-competitors are considered marks
// TODO Auto-generated method stub
super.competitor(hashValue, nation, sailNumber, name, competitorStatus, boatType, cRank_Bracket, cnPoints_x10_Bracket,
ctPoints_x10_Winner);
if (boatType == BoatType.Competitor) {
Competitor competitor = domainFactory.getOrCreateCompetitor(sailNumberOrTrackerID, threeLetterIOCCode, name,
getCurrentBoatClass());
Set<Competitor> competitorsOfCurrentRace = competitorsPerRaceID.get(currentRaceID);
if (competitorsOfCurrentRace == null) {
competitorsOfCurrentRace = new HashSet<>();
competitorsPerRaceID.put(currentRaceID, competitorsOfCurrentRace);
}
competitorsOfCurrentRace.add(competitor);
competitorByHashValue.put(hashValue, competitor);
} else {
// consider it a mark
Mark mark = domainFactory.getOrCreateMark(sailNumberOrTrackerID);
Map<String, Mark> marksOfCurrentRace = marksPerRaceIDPerMarkID.get(currentRaceID);
if (marksOfCurrentRace == null) {
marksOfCurrentRace = new HashMap<>();
marksPerRaceIDPerMarkID.put(currentRaceID, marksOfCurrentRace);
}
marksOfCurrentRace.put(sailNumberOrTrackerID, mark);
markByHashValue.put(hashValue, mark);
}
}
@Override
public void mark(MarkType markType, String name, byte index, String id1, String id2, short windSpeedInKnots,
short trueWindDirectionInDegrees) {
// TODO Auto-generated method stub
super.mark(markType, name, index, id1, id2, windSpeedInKnots, trueWindDirectionInDegrees);
final List<String> markNames = new ArrayList<>();
markNames.add(id1);
if (id2 != null) {
markNames.add(id2);
}
final ControlPoint controlPoint = domainFactory.getOrCreateControlPoint(markNames);
if (index == 0) {
currentCourseDefinition = new ArrayList<>();
}
while (currentCourseDefinition.size() < index+1) {
currentCourseDefinition.add(null); // pad course
}
currentCourseDefinition.set(index, controlPoint);
if (windSpeedInKnots != -1 && trueWindDirectionInDegrees != -1) {
SpeedWithBearing windSpeedWithBearing = new KnotSpeedWithBearingImpl(windSpeedInKnots,
new DegreeBearingImpl(trueWindDirectionInDegrees));
windAtControlPoint.put(controlPoint, windSpeedWithBearing);
} else {
windAtControlPoint.remove(controlPoint);
}
}
@Override
public void trackersCount(short trackersCount) {
try {
RaceDefinition race = racePerRaceID.get(currentRaceID);
if (race == null) {
createRace();
} else {
Course course = race.getCourse();
try {
course.update(currentCourseDefinition, domainFactory.getBaseDomainFactory());
} catch (PatchFailedException e) {
throw new RuntimeException(e);
}
}
} finally {
currentCourseDefinition = null;
}
}
private void createRace() {
final Regatta myRegatta = regatta != null ? regatta : domainFactory.getOrCreateRegatta(currentRaceID, trackedRegattaRegistry);
RaceDefinition race = domainFactory.createRaceDefinition(
myRegatta,
currentRaceID, competitorsPerRaceID.get(currentRaceID), currentCourseDefinition);
racePerRaceID.put(currentRaceID, race);
DynamicTrackedRace trackedRace = trackedRegattaRegistry.getOrCreateTrackedRegatta(myRegatta).
createTrackedRace(race, EmptyWindStore.INSTANCE, TrackedRace.DEFAULT_LIVE_DELAY_IN_MILLISECONDS,
WindTrack.DEFAULT_MILLISECONDS_OVER_WHICH_TO_AVERAGE_WIND,
/* time over which to average speed: */ race.getBoatClass().getApproximateManeuverDurationInMilliseconds(),
/* raceDefinitionSetToUpdate */ null);
trackedRacePerRaceID.put(currentRaceID, trackedRace);
}
@Override
public void trackers(int hashValue, int latitude, int longitude, short cog, short sog_Knots_x10, short average_sog,
short vmg_Knots_x10, CompetitorStatus competitorStatus, short rank, short distanceToLeader_meters,
short distanceToNextMark_meters, short nextMark, short pRank, short ptPoints, short pnPoints) {
// TODO Auto-generated method stub
super.trackers(hashValue, latitude, longitude, cog, sog_Knots_x10, average_sog, vmg_Knots_x10, competitorStatus, rank,
distanceToLeader_meters, distanceToNextMark_meters, nextMark, pRank, ptPoints, pnPoints);
DynamicTrackedRace trackedRace = trackedRacePerRaceID.get(currentRaceID);
Position position = new DegreePosition(referenceLocation.getLatDeg() + ((double) latitude) / 10000000.,
referenceLocation.getLngDeg() + ((double) longitude) / 10000000.);
TimePoint raceTimePoint = raceTimePerRaceID.get(currentRaceID);
Bearing bearing = new DegreeBearingImpl(cog);
SpeedWithBearing speed = new KnotSpeedWithBearingImpl(((double) sog_Knots_x10) / 10., bearing);
GPSFixMoving fix = new GPSFixMovingImpl(position, raceTimePoint, speed);
Mark mark = markByHashValue.get(hashValue);
if (mark != null) {
for (Map.Entry<ControlPoint, SpeedWithBearing> controlPointWithWind : windAtControlPoint.entrySet()) {
if (Util.contains(controlPointWithWind.getKey().getMarks(), mark)) {
trackedRace.recordWind(new WindImpl(position, raceTimePoint, controlPointWithWind.getValue()),
new WindSourceWithAdditionalID(WindSourceType.EXPEDITION, mark.getName()));
}
}
trackedRace.recordFix(mark, fix);
} else {
Competitor competitor = competitorByHashValue.get(hashValue);
if (competitor != null) {
trackedRace.recordFix(competitor, fix);
if (lastNextMark.get(competitor) != nextMark) {
List<MarkPassing> newMarkPassings = new ArrayList<>();
Util.addAll(trackedRace.getMarkPassings(competitor), newMarkPassings);
newMarkPassings.add(new MarkPassingImpl(raceTimePoint,
Util.get(trackedRace.getRace().getCourse().getWaypoints(), nextMark), competitor));
trackedRace.updateMarkPassings(competitor, newMarkPassings);
lastNextMark.put(competitor, nextMark);
}
} else {
logger.warning("Couldn't find hash value "+hashValue+" in either the mark or the competitor map");
}
}
}
@Override
public void ranking(int hashValue, short rank, short rankIndex, short racePoints,
CompetitorStatus competitorStatus, short finishRank, short finishRankIndex, int gap_seconds,
int raceTime_seconds) {
// TODO Auto-generated method stub
super.ranking(hashValue, rank, rankIndex, racePoints, competitorStatus, finishRank, finishRankIndex, gap_seconds,
raceTime_seconds);
}
}