package au.gov.amsa.navigation;
import static au.gov.amsa.navigation.DriftDetector.diff;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.mockito.Mockito;
import com.google.common.base.Optional;
import au.gov.amsa.navigation.DriftDetector.Options;
import au.gov.amsa.risky.format.Fix;
import au.gov.amsa.risky.format.NavigationalStatus;
import rx.Observable;
import rx.observers.TestSubscriber;
public class DriftDetectorTest {
private static final double PRECISION = 0.0000001;
private static final float DRIFT_SPEED_KNOTS = (float) ((DriftDetector.Options.DEFAULT_MIN_DRIFTING_SPEED_KNOTS
+ DriftDetector.Options.DEFAULT_MAX_DRIFTING_SPEED_KNOTS) * 0.5);
private static Options testOptions = new Options(45, 135, 0.25f, 20f,
TimeUnit.HOURS.toMillis(4), TimeUnit.MINUTES.toMillis(2));
@Test
public void testDiff() {
assertEquals(0, diff(0, 0), PRECISION);
}
@Test
public void testDiff2() {
assertEquals(10, diff(30, 20), PRECISION);
}
@Test
public void testDiff3() {
assertEquals(10, diff(20, 30), PRECISION);
}
@Test
public void testDiff4() {
assertEquals(20, diff(350, 10), PRECISION);
}
@Test
public void testDiff5() {
assertEquals(20, diff(10, 350), PRECISION);
}
@Test
public void testDrifted() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(110.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(Optional.of(DRIFT_SPEED_KNOTS));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertTrue(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testDriftedFalseIfNoCog() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.<Float> absent());
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(110.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(Optional.of(DRIFT_SPEED_KNOTS));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testDriftedFalseIfNoHeading() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.<Float> absent());
Mockito.when(fix.speedOverGroundKnots()).thenReturn(Optional.of(DRIFT_SPEED_KNOTS));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testDriftedFalseIfNoSpeed() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(110.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(Optional.<Float> absent());
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testNotDriftedBecauseSpeedTooHigh() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(110.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(
Optional.of(DriftDetector.Options.DEFAULT_MAX_DRIFTING_SPEED_KNOTS * 1.01f));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testNotDriftedBecauseSpeedTooLow() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(110.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(
Optional.of(DriftDetector.Options.DEFAULT_MIN_DRIFTING_SPEED_KNOTS * 0.99f));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testNotDriftedBecauseCogHeadingDiffTooLow() {
Fix fix = Mockito.mock(Fix.class);
Mockito.when(fix.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(fix.headingDegrees()).thenReturn(Optional.of(11.0f));
Mockito.when(fix.speedOverGroundKnots()).thenReturn(Optional.of(DRIFT_SPEED_KNOTS));
Mockito.when(fix.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
assertFalse(DriftDetector.isCandidate(Options.instance()).call(fix));
}
@Test
public void testEmpty() {
List<DriftCandidate> list = getCandidates(Observable.empty());
assertEquals(0, list.size());
}
@Test
public void testStartingWithNonDriftersDoesNothing() {
long t = 0;
// non-drifter
Fix f1 = createFix(0, DRIFT_SPEED_KNOTS, t);
// non-drifter
Fix f2 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// non-drifter
Fix f3 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3));
assertTrue(list.isEmpty());
}
@Test
public void testRule2TwoDrifters() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2));
assertEquals(2, list.size());
assertTrue(f1 == list.get(0).fix());
assertTrue(f2 == list.get(1).fix());
assertEquals(f1.time(), list.get(0).driftingSince());
assertEquals(f1.time(), list.get(1).driftingSince());
}
@Test
public void testRule2TwoDriftersBigTimeGap() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t += TimeUnit.DAYS.toMillis(1));
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2));
assertTrue(list.isEmpty());
}
@Test
public void testRule3ThreeDrifters() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f3 = createFix(92, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3));
assertEquals(3, list.size());
assertTrue(f1 == list.get(0).fix());
assertTrue(f2 == list.get(1).fix());
assertTrue(f3 == list.get(2).fix());
assertEquals(f1.time(), list.get(0).driftingSince());
assertEquals(f1.time(), list.get(1).driftingSince());
assertEquals(f1.time(), list.get(2).driftingSince());
}
@Test
public void testRule3FourDriftersBigTimeGapBetweenTwoAndThree() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f3 = createFix(92, DRIFT_SPEED_KNOTS, t += testOptions.expiryAgeMs() + 1);
// drifter
Fix f4 = createFix(93, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3, f4));
assertEquals(4, list.size());
assertTrue(f1 == list.get(0).fix());
assertTrue(f2 == list.get(1).fix());
assertTrue(f3 == list.get(2).fix());
assertTrue(f4 == list.get(3).fix());
assertEquals(f1.time(), list.get(0).driftingSince());
assertEquals(f1.time(), list.get(1).driftingSince());
assertEquals(f3.time(), list.get(2).driftingSince());
assertEquals(f3.time(), list.get(3).driftingSince());
}
@Test
public void testRule4DrifterThenTwoNonDrifters() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// non-drifter
Fix f2 = createFix(0, DRIFT_SPEED_KNOTS, t += 1);
// non-drifter
Fix f3 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3));
assertTrue(list.isEmpty());
}
@Test
public void testRule5TwoDriftersThenTwoNonDrifters() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
// non-drifter
Fix f3 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// non-drifter
Fix f4 = createFix(2, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3, f4));
assertEquals(2, list.size());
assertTrue(f1 == list.get(0).fix());
assertTrue(f2 == list.get(1).fix());
assertEquals(f1.time(), list.get(0).driftingSince());
assertEquals(f1.time(), list.get(1).driftingSince());
}
@Test
public void testRule6DrifterNonDrifterDrifter() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// non-drifter
Fix f2 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f3 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3));
assertEquals(2, list.size());
assertTrue(f1 == list.get(0).fix());
assertTrue(f3 == list.get(1).fix());
assertEquals(f1.time(), list.get(0).driftingSince());
assertEquals(f1.time(), list.get(1).driftingSince());
}
@Test
public void testRule6DrifterNonDrifterDrifterOverNonDriftingThreshold() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// non-drifter
Fix f2 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f3 = createFix(91, DRIFT_SPEED_KNOTS, t += testOptions.nonDriftingThresholdMs() + 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2, f3));
assertTrue(list.isEmpty());
}
@Test
public void testTwoDriftersButDifferentMmsi() {
long t = 0;
// drifter
Fix f1 = createFix(12344, 90, DRIFT_SPEED_KNOTS, t);
// drifter
Fix f2 = createFix(12345, 91, DRIFT_SPEED_KNOTS, t += 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2));
assertTrue(list.isEmpty());
}
@Test
public void testOutOfOrderFixes() {
long t = 0;
// drifter
Fix f1 = createFix(90, DRIFT_SPEED_KNOTS, t);
// drifter but before the one above
Fix f2 = createFix(91, DRIFT_SPEED_KNOTS, t -= 1);
List<DriftCandidate> list = getCandidates(Observable.just(f1, f2));
assertTrue(list.isEmpty());
}
@Test
public void testBackpressure() {
TestSubscriber<DriftCandidate> ts = new TestSubscriber<DriftCandidate>(1) {
@Override
public void onNext(DriftCandidate t) {
super.onNext(t);
request(1);
}
};
long t = 0;
// non-drifter
Fix f1 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// non-drifter
Fix f2 = createFix(1, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f3 = createFix(91, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f4 = createFix(92, DRIFT_SPEED_KNOTS, t += 1);
// drifter
Fix f5 = createFix(92, DRIFT_SPEED_KNOTS, t += 1);
Observable.just(f1, f2, f3, f4, f5).compose(DriftDetector.detectDrift(testOptions))
.subscribe(ts);
ts.awaitTerminalEvent();
ts.assertCompleted();
ts.assertNoErrors();
assertEquals(3, ts.getOnNextEvents().size());
}
private List<DriftCandidate> getCandidates(Observable<Fix> source) {
return source.compose(DriftDetector.detectDrift(testOptions)).toList().toBlocking()
.single();
}
private static Fix createFix(int mmsi, float courseHeadingDiff, float speedKnots, long time) {
Fix f = Mockito.mock(Fix.class);
Mockito.when(f.courseOverGroundDegrees()).thenReturn(Optional.of(10.0f));
Mockito.when(f.headingDegrees()).thenReturn(Optional.of(10.0f + courseHeadingDiff));
Mockito.when(f.speedOverGroundKnots()).thenReturn(Optional.of(speedKnots));
Mockito.when(f.navigationalStatus()).thenReturn(Optional.<NavigationalStatus> absent());
Mockito.when(f.mmsi()).thenReturn(mmsi);
Mockito.when(f.fix()).thenReturn(f);
Mockito.when(f.time()).thenReturn(time);
return f;
}
private static Fix createFix(float courseHeadingDiff, float speedKnots, long time) {
return createFix(123456789, courseHeadingDiff, speedKnots, time);
}
}