All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dk.tbsalling.ais.tracker.AISTracker Maven / Gradle / Ivy

/*
 * AISUtils
 * - a java-based library for processing of AIS messages received from digital
 * VHF radio traffic related to maritime navigation and safety in compliance with ITU 1371.
 *
 * (C) Copyright 2011- by S-Consult ApS, DK31327490, http://s-consult.dk, Denmark.
 *
 * Released under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
 * For details of this license see the nearby LICENCE-full file, visit http://creativecommons.org/licenses/by-nc-sa/3.0/
 * or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
 *
 * NOT FOR COMMERCIAL USE!
 * Contact [email protected] to obtain a commercially licensed version of this software.
 *
 */

package dk.tbsalling.ais.tracker;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import dk.tbsalling.ais.tracker.events.*;
import dk.tbsalling.aismessages.AISInputStreamReader;
import dk.tbsalling.aismessages.ais.messages.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.function.Supplier;

import static java.time.Instant.EPOCH;
import static java.time.Instant.now;
import static java.util.Objects.requireNonNull;

/**
 * An AisTracker receives AISMessages and based on these it maintains a collection of all known tracks,
 * including their position, speed, course, etc.
 *
 * If a certain track has not received any updates for a while it enters status 'stale' and will receive
 * no further updates. Instead a new track is created if more AISMessages are received from the same vessel
 * later on.
 */
@ThreadSafe
public class AISTracker implements TrackEventEmitter {

    private final static Logger LOG = LoggerFactory.getLogger(AISTracker.class);

    private final Predicate messageFilter;

    /**
     * Construct an AISTracker which processes all received AISMessages.
     */
    public AISTracker() {
        LOG.info("AisTracker created.");
        messageFilter = msg -> true;
        shutdown = false;
    }

    /*
     * Construct on AISTracker which processes only messages satisfying the messageFilter.
     */
    public AISTracker(Predicate messageFilter) {
        LOG.info("AisTracker created with custom predicate.");
        this.messageFilter = messageFilter;
        shutdown = false;
    }

    /*
     * Update the tracker from an input stream of NMEA armoured AIS messages.
     *
     * If an IOException is thrown, the state of the tracker is maintained, and tracking
     * is resumed when this method is called again with a new InputStream.
     */
    public void update(InputStream nmeaInputStream) throws IOException {
        new AISInputStreamReader(nmeaInputStream, aisMessage -> update(aisMessage)).run();
    }

    /**
     * Update the tracker with a new AIS message.
     *
     * If there is a reception timestamp in the meta data of the AIS message, then it will be used as the
     * message timestamp. If not, current system time will be used as the timestamp.
     *
     * @param aisMessage the AIS message.
     */
    public void update(AISMessage aisMessage) {
        if (threadSafeGet(() -> shutdown))
            throw new IllegalStateException("Tracker has been requested to shutdown.");

        requireNonNull(aisMessage);

        Metadata metadata = aisMessage.getMetadata();
        Instant messageTimestamp = metadata == null ? now(Clock.systemUTC()) : metadata.getReceived();

        if (messageFilter.test(aisMessage))
            updateAisTrack(aisMessage, messageTimestamp);
    }

    /**
     * Update the tracker with a new AIS message.
     *
     * @param aisMessage the AIS message.
     * @param messageTimestamp the time this AIS message was received.
     */
    public void update(AISMessage aisMessage, Instant messageTimestamp) {
        if (threadSafeGet(() -> shutdown))
            throw new IllegalStateException("Tracker has been requested to shutdown.");

        requireNonNull(aisMessage);
        requireNonNull(messageTimestamp);

        if (messageFilter.test(aisMessage))
            updateAisTrack(aisMessage, messageTimestamp);
    }

    /**
     * Check if a given vessel is currently tracked by the tracker.
     * @param mmsi The MMSI no.
     * @return true if the vessel is currently tracked, false if not.
     */
    public boolean isTracked(long mmsi) {
        return threadSafeGet(() -> tracks.containsKey(mmsi));
    }

    /**
     * Count the no. of tracks currently being tracked.
     * @return the no of tracks.
     */
    public int getNumberOfAisTracks() {
        return threadSafeGet(() -> tracks.size());
    }

    /**
     * Lookup a tracked AisTrack with the given mmsi no.
     * @param mmsi the mmsi no. to lookup.
     * @return The tracked AisTrack or null if no such track is currently tracked.
     */
    public AISTrack getAisTrack(long mmsi) {
        return threadSafeGet(() -> tracks.get(mmsi));
    }

    /**
     * Extract an immutable copy of all tracks currently tracked.
     * @return An immutable set of all tracks currently tracked.
     */
    public Set getAisTracks() {
        return threadSafeGet(() -> ImmutableSet.copyOf(tracks.values()));
    }

    /* Return the value of the current wallclock. */
    public Instant getWallclock() {
        return threadSafeGet(() -> wallclock);
    }

    /* Return the instant when track history pruning was last performed. */
    public Instant getTimeOfLastPruning() {
        return threadSafeGet(() -> timeOfLastPruning);
    }

    public boolean isShutdown() {
        return threadSafeGet(() -> shutdown);
    }

    /** Shut down the tracker */
    public void shutdown() {
        LOG.info("AisTracker shutdown requested.");
        lock.lock();
        try {
            shutdown = true;
        } finally {
            lock.unlock();
        }
        try {
            taskExecutor.shutdown();
            boolean cleanShutdown = taskExecutor.awaitTermination(1, TimeUnit.MINUTES);
            LOG.debug("taskExecutor: shutdown:" + taskExecutor.isShutdown() + " terminated:" + taskExecutor.isTerminated());
            if (cleanShutdown == false)
                LOG.warn("AisTracker was shut down before all pending tasks were processed");
        } catch (InterruptedException e) {
            LOG.error("Tracker failed to shutdown cleanly", e);
        }
        try {
            eventBusExecutor.shutdown();
            boolean cleanShutdown = eventBusExecutor.awaitTermination(1, TimeUnit.MINUTES);
            LOG.debug("eventBusExecutor: shutdown:" + eventBusExecutor.isShutdown() + " terminated:" + eventBusExecutor.isTerminated());
            if (cleanShutdown == false)
                LOG.warn("AisTracker was shut down before all pending events were processed");
        } catch (InterruptedException e) {
            LOG.error("Tracker failed to shutdown cleanly", e);
        }
        LOG.info("AisTracker shutdown completed.");
    }

    private  T threadSafeGet(Supplier getter) {
        lock.lock();
        try {
            return getter.get();
        } finally {
            lock.unlock();
        }
    }

    private void updateAisTrack(final AISMessage aisMessage, final Instant messageTimestamp) {
        final long mmsi = aisMessage.getSourceMmsi().getMMSI();

        lock.lock();
        try {
            if (messageTimestamp.isBefore(wallclock))
                throw new IllegalArgumentException("Current time is " + wallclock + "; message timestamp is too old: " + messageTimestamp);

            setWallclock(messageTimestamp);

            if (aisMessage instanceof StaticDataReport) {
                if (isTracked(mmsi)) {
                    updateAisTrack(mmsi, (StaticDataReport) aisMessage, messageTimestamp);
                } else {
                    insertAisTrack(mmsi, (StaticDataReport) aisMessage, messageTimestamp);
                }
            } else if (aisMessage instanceof DynamicDataReport) {
                if (isTracked(mmsi)) {
                    updateAisTrack(mmsi, (DynamicDataReport) aisMessage, messageTimestamp);
                } else {
                    insertAisTrack(mmsi, (DynamicDataReport) aisMessage, messageTimestamp);
                }
            } else if (aisMessage instanceof AidToNavigationReport) {
                if (isTracked(mmsi)) {
                    updateAisTrack(mmsi, (AidToNavigationReport) aisMessage, messageTimestamp);
                } else {
                    insertAisTrack(mmsi, (AidToNavigationReport) aisMessage, messageTimestamp);
                }
            }
            if (isHistoryPruneNeeded()) {
                taskExecutor.execute(() -> processTrackHistory());
            }
            if (isStaleCheckNeeded()) {
                taskExecutor.execute(() -> processStaleTracks());
            }
        } finally {
            lock.unlock();
        }
    }

    private void insertAisTrack(final long mmsi, final StaticDataReport shipStaticDataReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        final AISTrack aisTrack = new AISTrack(shipStaticDataReport, msgTimestamp);
        tracks.put(mmsi, aisTrack);
        fireTrackCreated(aisTrack);
    }

    private void insertAisTrack(final long mmsi, final DynamicDataReport basicShipDynamicDataReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        final AISTrack aisTrack = new AISTrack(basicShipDynamicDataReport, msgTimestamp);
        tracks.put(mmsi, aisTrack);
        fireTrackCreated(aisTrack);
    }

    private void insertAisTrack(final long mmsi, final AidToNavigationReport aidToNavigationReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        final AISTrack aisTrack = new AISTrack(aidToNavigationReport, msgTimestamp);
        tracks.put(mmsi, aisTrack);
        fireTrackCreated(aisTrack);
    }

    private void updateAisTrack(final long mmsi, final StaticDataReport shipStaticDataReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        AISTrack oldTrack = tracks.get(mmsi);
        if (msgTimestamp.isBefore(oldTrack.getTimeOfLastUpdate()))
            throw new IllegalArgumentException("Cannot update track with an older message: " + msgTimestamp + " is before previous update " + oldTrack.getTimeOfStaticUpdate());

        AISTrack newTrack = new AISTrack(shipStaticDataReport, oldTrack.getDynamicDataReport(), msgTimestamp, oldTrack.getTimeOfDynamicUpdate());
        tracks.put(mmsi, newTrack);
        fireTrackUpdated(newTrack);
    }

    private void updateAisTrack(final long mmsi, final DynamicDataReport basicShipDynamicDataReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        AISTrack oldTrack = tracks.get(mmsi);
        if (msgTimestamp.isBefore(oldTrack.getTimeOfLastUpdate()))
            throw new IllegalArgumentException("Cannot update track with an older message: " + msgTimestamp + " is before previous update " + oldTrack.getTimeOfDynamicUpdate());

        AISTrack newTrack = new AISTrack(oldTrack.getStaticDataReport(), basicShipDynamicDataReport, oldTrack.getTimeOfStaticUpdate(), msgTimestamp);
        tracks.put(mmsi, newTrack);
        fireTrackUpdated(newTrack);
        fireTrackDynamicsUpdated(newTrack);
    }

    private void updateAisTrack(final long mmsi, final AidToNavigationReport aidToNavigationReport, final Instant msgTimestamp) {
        /* Assumes lock is locked */
        AISTrack oldTrack = tracks.get(mmsi);
        if (msgTimestamp.isBefore(oldTrack.getTimeOfLastUpdate()))
            throw new IllegalArgumentException("Cannot update track with an older message: " + msgTimestamp + " is before previous update " + oldTrack.getTimeOfDynamicUpdate());

        AISTrack newTrack = new AISTrack(aidToNavigationReport, msgTimestamp);
        tracks.put(mmsi, newTrack);
        fireTrackUpdated(newTrack);
        fireTrackDynamicsUpdated(newTrack);
    }

    //
    // Core data fields of the tracker
    //

    private ReentrantLock lock = new ReentrantLock();

    /** */
    @GuardedBy("lock")
    private boolean shutdown = false;

    @GuardedBy("lock")
    private Map tracks = new HashMap<>();

    /** To inject special executors for unit testing */
    void setTaskExecutor(ExecutorService taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    /** Asynchroneous executor service to take care of pruning */
    private ExecutorService taskExecutor = Executors.newSingleThreadExecutor();

    //
    // Fields and methods related to the wallclock
    //

    /** Time of last update - perceived by the tracker as current time; or time as seen on the wallclock. */
    @GuardedBy("lock")
    private Instant wallclock = EPOCH;

    private void setWallclock(Instant wallclock) {
        lock.lock();
        try {
            this.wallclock = wallclock;
            fireWallclockChanged(this.wallclock);
        } finally {
            lock.unlock();
        }
    }

    //
    // Fields and methods related to pruning
    //

    /** Run through all tracks and prune historic items which have expired */
    private void processTrackHistory() {
        lock.lock();
        try {
            Map prunedTracks = Maps.newTreeMap();
            tracks.forEach((mmsi, track) -> {
                if (TRACK_NEEDS_PRUNING.test(track)) {
                    prunedTracks.put(track.getMmsi(), new AISTrack(track, INSTANT_IMPLIES_PRUNING));
                }
            });
            prunedTracks.forEach((mmsi, track) -> tracks.put(mmsi, prunedTracks.get(mmsi)));
            timeOfLastPruning = wallclock;
        } finally {
            lock.unlock();
        }
    }

    private boolean isHistoryPruneNeeded() {
        /* Assumes lock is locked */
        return timeOfLastPruning.isBefore(wallclock.minus(PRUNE_CHECK_PERIOD));
    }

    /** Time on the wall clock between track history pruning jobs */
    private final static Duration PRUNE_CHECK_PERIOD = Duration.ofMinutes(5);

    /** The instant in time when the last pruning job ran */
    @GuardedBy("lock")
    private Instant timeOfLastPruning = EPOCH;

    /** Max duration to keep dynamic history of each track */
    private final static Duration DYNAMIC_DATA_HISTORY_MAX_AGE = Duration.ofHours(6);

    /** Predicate for instants which imply that pruning is required */
    private final Predicate INSTANT_IMPLIES_PRUNING  = instant -> instant.isBefore(wallclock.minus(DYNAMIC_DATA_HISTORY_MAX_AGE));

    /** Predicate for tracks which need pruning of their dynamic history */
    private final Predicate TRACK_NEEDS_PRUNING = aisTrack -> !aisTrack.getDynamicDataHistory().isEmpty() && INSTANT_IMPLIES_PRUNING.test(aisTrack.getDynamicDataHistory().firstKey());

    //
    // Fields and methods related to track stale check
    //

    /** Run through all tracks and note which ones are stale */
    private void processStaleTracks() {
        lock.lock();
        try {
            Map staleTracks = Maps.newTreeMap();
            tracks.forEach((mmsi, track) -> {
                if (track.getTimeOfLastUpdate().isBefore(wallclock.minus(STALE_PERIOD))) {
                    staleTracks.put(mmsi, track);
                }
            });
            staleTracks.forEach((mmsi, track) -> { tracks.remove(mmsi); fireTrackDeleted(track); });
            timeOfLastStaleCheck = wallclock;
        } finally {
            lock.unlock();
        }
    }

    private boolean isStaleCheckNeeded() {
        lock.lock();
        try {
            return timeOfLastStaleCheck.isBefore(wallclock.minus(STALE_CHECK_PERIOD));
        } finally {
            lock.unlock();
        }
    }

    void setStaleCheckPeriod(Duration staleCheckPeriod) {
        lock.lock();
        try {
            STALE_CHECK_PERIOD = staleCheckPeriod;
        } finally {
            lock.unlock();
        }
    }

    void setStalePeriod(Duration stalePeriod) {
        lock.lock();
        try {
            STALE_PERIOD = stalePeriod;
        } finally {
            lock.unlock();
        }
    }

    /** Every this duration on the wallclock tracks are checked to be stale */
    private Duration STALE_CHECK_PERIOD = Duration.ofMinutes(1);

    /** Tracks not updated within this duration are considered stale. */
    private Duration STALE_PERIOD = Duration.ofMinutes(30);

    /** The instant in time when the last pruning job ran */
    @GuardedBy("lock")
    private Instant timeOfLastStaleCheck = EPOCH;

    //
    // Fields and methods related to event firing
    // The event bus is Guava Eventbus - see more: http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/eventbus/EventBus.html
    //

    private final ExecutorService eventBusExecutor = Executors.newCachedThreadPool();
    private final EventBus eventBus = new AsyncEventBus(eventBusExecutor);

    @Override
    public void registerSubscriber(Object subscriber) {
        eventBus.register(subscriber);
        LOG.info("Subscribed to tracker events: " + subscriber);
    }

    private void fireTrackCreated(AISTrack track) {
        eventBus.post(new AisTrackCreatedEvent(track));
    }

    private void fireTrackUpdated(AISTrack track) {
        eventBus.post(new AisTrackUpdatedEvent(track));
    }

    private void fireTrackDynamicsUpdated(AISTrack track) {
        eventBus.post(new AisTrackDynamicsUpdatedEvent(track));
    }

    private void fireTrackDeleted(AISTrack track) {
        eventBus.post(new AisTrackDeletedEvent(track));
    }

    private void fireWallclockChanged(Instant wallclock) {
        eventBus.post(new WallclockChangedEvent(getWallclock()));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy