mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-17 11:19:15 +00:00
bug5728: started by adding examples of jumpy tracks and building a trivial "test case" around it
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
+131
@@ -0,0 +1,131 @@
|
||||
package com.sap.sailing.server.trackfiles.test;
|
||||
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.common.SpeedWithBearing;
|
||||
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
|
||||
import com.sap.sailing.domain.test.AbstractLeaderboardTest;
|
||||
import com.sap.sailing.domain.tracking.DynamicGPSFixTrack;
|
||||
import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl;
|
||||
import com.sap.sailing.server.trackfiles.RouteConverterGPSFixImporterFactory;
|
||||
import com.sap.sse.common.Util;
|
||||
|
||||
/**
|
||||
* See also bug 5728. We have seen tracks coming from phones where there seem to be two
|
||||
* "interleaved" sub-sequences of fixes: one where the time stamps are rounded to a full
|
||||
* second; and one where time stamps have a fractional seconds value. Each of these
|
||||
* sub-sequences seems to be consistent in itself, but at their boundaries the track
|
||||
* appears jittery and jumpy.<p>
|
||||
*
|
||||
* We surmise that a constant offset can be computed by which the full-second time stamps
|
||||
* would need to be adjusted in order to result in a consistent track that is smooth also
|
||||
* at the boundaries between the two sub-sequences.<p>
|
||||
*
|
||||
* This test starts with looking at two tracks that are known to show this issue. Jumpiness
|
||||
* can be measured by looking at the number and badness of inconsistencies between computed
|
||||
* and reported COG/SOG values between fixes, then "learning" the offset, adjusting all
|
||||
* integer-second fixes by the offset and computing number and badness of inconsistencies
|
||||
* again.
|
||||
*
|
||||
* @author Axel Uhl (d043530)
|
||||
*
|
||||
*/
|
||||
public class JumpyTrackSmootheningTest {
|
||||
private DynamicGPSFixTrack<Competitor, GPSFixMoving> track;
|
||||
|
||||
protected void readTrack(String filename) throws Exception {
|
||||
track = new DynamicGPSFixMovingTrackImpl<Competitor>(AbstractLeaderboardTest.createCompetitor(filename),
|
||||
/* millisecondsOverWhichToAverage */ 5000, /* losslessCompaction */ true);
|
||||
final InputStream fileInputStream = getClass().getClassLoader().getResourceAsStream(filename);
|
||||
final InputStream inputStream;
|
||||
if (filename.endsWith(".gz")) {
|
||||
inputStream = new GZIPInputStream(fileInputStream);
|
||||
} else {
|
||||
inputStream = fileInputStream;
|
||||
}
|
||||
RouteConverterGPSFixImporterFactory.INSTANCE.createRouteConverterGPSFixImporter().importFixes(inputStream,
|
||||
(fix, device)->track.add((GPSFixMoving) fix), /* inferSpeedAndBearing */ false, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* On {@link #track} looks at adjacent fixes and compares the COG/SOG values reported by those fixes
|
||||
* with the COG/SOG value inferred from their position and time delta.
|
||||
*/
|
||||
private int getInconsistenciesOnRawFixes() {
|
||||
int numberOfInconsistencies = 0;
|
||||
GPSFixMoving previous = null;
|
||||
track.lockForRead();
|
||||
try {
|
||||
for (final GPSFixMoving fix : track.getRawFixes()) {
|
||||
if (previous == null) {
|
||||
previous = fix;
|
||||
} else {
|
||||
final SpeedWithBearing inferred = previous.getSpeedAndBearingRequiredToReach(fix);
|
||||
final SpeedWithBearing reportedByPrevious = previous.getSpeed();
|
||||
final SpeedWithBearing reportedByFix = fix.getSpeed();
|
||||
if (tooDifferent(inferred, reportedByPrevious) || tooDifferent(inferred, reportedByFix)) {
|
||||
numberOfInconsistencies++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
return numberOfInconsistencies;
|
||||
}
|
||||
|
||||
private boolean tooDifferent(SpeedWithBearing inferred, SpeedWithBearing reported) {
|
||||
return
|
||||
// speed difference must be less than 20% of
|
||||
(Math.abs((inferred.getKnots()-reported.getKnots()) / inferred.getKnots()) > 0.2 ||
|
||||
// course difference must be less than 10deg
|
||||
Math.abs(inferred.getBearing().getDifferenceTo(reported.getBearing()).getDegrees()) > 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCZE2471() throws Exception {
|
||||
readTrack("CZE2471.gpx.gz");
|
||||
track.lockForRead();
|
||||
try {
|
||||
assertFalse(Util.isEmpty(track.getRawFixes()));
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
final int numberOfInconsistencies = getInconsistenciesOnRawFixes();
|
||||
assertTrue(numberOfInconsistencies > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCZE2956() throws Exception {
|
||||
readTrack("CZE2956.gpx.gz");
|
||||
track.lockForRead();
|
||||
try {
|
||||
assertFalse(Util.isEmpty(track.getRawFixes()));
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
final int numberOfInconsistencies = getInconsistenciesOnRawFixes();
|
||||
assertTrue(numberOfInconsistencies > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGallagherZelenka() throws Exception {
|
||||
readTrack("GallagherZelenka.gpx.gz");
|
||||
track.lockForRead();
|
||||
try {
|
||||
assertFalse(Util.isEmpty(track.getRawFixes()));
|
||||
} finally {
|
||||
track.unlockAfterRead();
|
||||
}
|
||||
final int numberOfInconsistencies = getInconsistenciesOnRawFixes();
|
||||
assertTrue(numberOfInconsistencies > 0);
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -148,14 +148,12 @@ public abstract class AbstractDataRetriever implements DataRetriever {
|
||||
boolean rawFixes, WaypointCreator<F> creator, Iterable<E> elements, NameReader<E> nameReader,
|
||||
TrackReaderRetriever<E, F> trackRetriever) throws FormatNotSupportedException, IOException {
|
||||
List<GpxRoute> routes = new ArrayList<>();
|
||||
final TimePoint start = race.getStartOfRace() != null ? race.getStartOfRace() : race.getStartOfTracking();
|
||||
final TimePoint end = race.getEndOfRace() != null ? race.getEndOfRace() : race.getEndOfTracking();
|
||||
for (E element : elements) {
|
||||
TimePoint start = race.getStartOfRace() != null ? race.getStartOfRace() : race.getStartOfTracking();
|
||||
TimePoint end = race.getEndOfRace() != null ? race.getEndOfRace() : race.getEndOfTracking();
|
||||
|
||||
String name = nameReader.getName(element);
|
||||
GpxRoute route = new GpxRoute(new Gpx11Format(), RouteCharacteristics.Track, name,
|
||||
Collections.<String> emptyList(), new ArrayList<GpxPosition>());
|
||||
|
||||
TrackReader<E, F> trackReader = trackRetriever.retrieveTrackReader(element);
|
||||
trackReader.getLocker().lock();
|
||||
try {
|
||||
@@ -179,7 +177,6 @@ public abstract class AbstractDataRetriever implements DataRetriever {
|
||||
} finally {
|
||||
trackReader.getLocker().unlock();
|
||||
}
|
||||
|
||||
if (!route.getPositions().isEmpty()) {
|
||||
routes.add(route);
|
||||
}
|
||||
|
||||
+3
-1
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
|
||||
import com.sap.sailing.domain.base.Boat;
|
||||
import com.sap.sailing.domain.base.Competitor;
|
||||
import com.sap.sailing.domain.base.Nationality;
|
||||
import com.sap.sailing.domain.common.trackfiles.TrackFilesFormat;
|
||||
import com.sap.sailing.domain.common.tracking.GPSFixMoving;
|
||||
import com.sap.sailing.domain.trackimport.FormatNotSupportedException;
|
||||
@@ -32,8 +33,9 @@ public class CompetitorDataRetriever extends AbstractDataRetriever {
|
||||
public String getName(Map.Entry<Competitor, Boat> competitorAndBoat) {
|
||||
final Competitor c = competitorAndBoat.getKey();
|
||||
final Boat b = competitorAndBoat.getValue();
|
||||
final Nationality nationality = c.getTeam().getNationality();
|
||||
return c.getName() + " - " + b.getSailID() + " - "
|
||||
+ c.getTeam().getNationality().getThreeLetterIOCAcronym();
|
||||
+ (nationality == null ? "" : nationality.getThreeLetterIOCAcronym());
|
||||
}
|
||||
}, retriever);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user