package au.gov.amsa.navigation; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.Scheduler; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; import rx.schedulers.Schedulers; import au.gov.amsa.risky.format.BinaryFixes; import au.gov.amsa.risky.format.Downsample; import au.gov.amsa.risky.format.Fix; import au.gov.amsa.risky.format.FixImpl; import au.gov.amsa.risky.format.Fixes; import au.gov.amsa.util.Files; import au.gov.amsa.util.identity.MmsiValidator2; public class BinaryFixesDriftDetectorMain { private static final String DRIFT_CANDIDATES_TXT = "target/drift-candidates.txt"; private static final Logger log = LoggerFactory.getLogger(BinaryFixesDriftDetectorMain.class); public static void main(String[] args) { new File(DRIFT_CANDIDATES_TXT).delete(); VesselPosition.validate = false; FixImpl.validate = false; List<File> files = Files.find(new File("/media/an/binary-fixes/2015"), Pattern.compile(".*\\.track")); log.info("files=" + files.size()); final AtomicLong num = new AtomicLong(); int count = Observable // list files .from(files) // exclude invalid mmsi .filter(onlyValidMmsis()) // share the load between processors .buffer(Math.max(1, files.size() / (Math.max(1, Runtime.getRuntime().availableProcessors() - 1)))) // search each list of files for drift detections .flatMap(detectDrift(num, Schedulers.computation())) // count .reduce(0, BinaryFixesDriftDetectorMain.<Integer> add()).toBlocking().single(); log.info("drift detections = " + count); } private static Func1<? super File, Boolean> onlyValidMmsis() { return file -> MmsiValidator2.INSTANCE.isValid(Long.parseLong(file.getName().substring(0, file.getName().indexOf('.')))); } @SuppressWarnings("unchecked") private static <T extends Number> Func2<T, T, T> add() { return (a, b) -> { if (a instanceof Integer) return (T) (Number) (a.intValue() + b.intValue()); else if (a instanceof Long) return (T) (Number) (a.longValue() + b.longValue()); else if (a instanceof Double) return (T) (Number) (a.doubleValue() + b.doubleValue()); else if (a instanceof Float) return (T) (Number) (a.floatValue() + b.floatValue()); else if (a instanceof Byte) return (T) (Number) (a.byteValue() + b.byteValue()); else if (a instanceof Short) return (T) (Number) (a.shortValue() + b.shortValue()); else throw new RuntimeException("not implemented"); }; } private static Func1<List<File>, Observable<Integer>> detectDrift(final AtomicLong num, final Scheduler scheduler) { return list -> { return Observable.from(list) // files to drift detections .concatMap( file -> { return BinaryFixes.from(file) // log count .doOnNext(logCount(num)) // .compose(Fixes.ignoreOutOfOrderFixes(false)) // detect drift .compose(DriftDetector.detectDrift()) // downsample to min 5 minutes between // reports but ensure that start of // drift is always included .compose( Downsample.<DriftCandidate> minTimeStep(5, TimeUnit.MINUTES, isStartOfDrift())) // onNext .doOnNext(ON_NEXT) // log on error .doOnError(e -> { log.error(file + ":" + e.getMessage(), e); }) // count .count(); }) // schedule .subscribeOn(scheduler); }; } private static Func1<DriftCandidate, Boolean> isStartOfDrift() { return c -> c.driftingSince() == c.fix().time(); } private static final Action1<DriftCandidate> ON_NEXT = new Action1<DriftCandidate>() { final char COMMA = ','; final Charset UTF8 = Charset.forName("UTF-8"); @Override public synchronized void call(DriftCandidate c) { try { FileOutputStream os = new FileOutputStream(DRIFT_CANDIDATES_TXT, true); Fix f = c.fix(); StringBuilder s = new StringBuilder(); s.append(f.mmsi()); s.append(COMMA); s.append(f.lat()); s.append(COMMA); s.append(f.lon()); s.append(COMMA); s.append(f.time()); s.append(COMMA); s.append(f.aisClass()); s.append(COMMA); s.append(f.courseOverGroundDegrees().get()); s.append(COMMA); s.append(f.headingDegrees().get()); s.append(COMMA); s.append(f.speedOverGroundKnots().get()); s.append(COMMA); s.append(f.navigationalStatus().isPresent() ? f.navigationalStatus().get() .toString() : ""); s.append(COMMA); s.append(c.driftingSince()); s.append('\n'); os.write(s.toString().getBytes(UTF8)); os.close(); } catch (IOException e) { throw new RuntimeException(e); } } }; private static Action1<Fix> logCount(final AtomicLong num) { return p -> { long n = num.incrementAndGet(); if (n % 1000000 == 0) { log.info((n / 1000000.0) + "m"); } }; } }