/* Copyright (c) 2011 Danish Maritime Authority * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library. If not, see <http://www.gnu.org/licenses/>. */ package dk.dma.ais.tracker.eventEmittingTracker; import com.google.common.collect.ImmutableList; import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisMessage5; import dk.dma.ais.message.AisStaticCommon; import dk.dma.ais.message.IVesselPositionMessage; import dk.dma.ais.packet.AisPacket; import dk.dma.ais.tracker.Target; import dk.dma.commons.util.DateTimeUtil; import dk.dma.enav.model.geometry.CoordinateSystem; import dk.dma.enav.model.geometry.Position; import net.jcip.annotations.GuardedBy; import net.jcip.annotations.ThreadSafe; import java.time.LocalDateTime; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.TreeSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import java.util.function.Supplier; import static java.util.Comparator.comparingLong; /** * The Track class contains the consolidated information known about a given target - likely as the result * of several received AIS messages. * * Track supports dynamic storage of track properties via the getProperty()/setProperty() methods, but * also contains some business logic and knowledge of what it stores; e.g. via the getMmsi(), and get/setPosition() * methods. * * Information which is available directly in AisPackets is stored and retrieved from these. Other information, * such as cell-id's, event booking-keeping, etc. is set dynamically as custom properties. * */ @ThreadSafe public final class Track extends Target implements Cloneable { public static final String CELL_ID = "cellId"; public static final String SAFETY_ZONE = "safetyZone"; public static final String EXTENT = "extent"; private Lock trackLock = new ReentrantLock(); private static final Comparator<TrackingReport> byTimestamp = comparingLong(TrackingReport::getTimestamp); private static final Supplier<TreeSet<TrackingReport>> treeSetSupplier = () -> new TreeSet<>(byTimestamp); private boolean positionReportPurgeEnable = true; public final static int MAX_AGE_POSITION_REPORTS_MINUTES = 20; @GuardedBy("trackLock") private TreeSet<TrackingReport> trackingReports = treeSetSupplier.get(); @GuardedBy("trackLock") private final Map<String, Object> properties = new HashMap<>(3); /** The last received AIS packet of type 5 */ @GuardedBy("trackLock") private AisPacket lastStaticReport; /** Cached value of ship type from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer shipType; /** Cached value of ship name from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private String shipName; /** Cached value of ship callsign from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private String callsign; /** Cached value of IMO no. lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer imo; /** Cached value of dimension A from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer shipDimensionBow; /** Cached value of dimension B from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer shipDimensionStern; /** Cached value of dimension C from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer shipDimensionPort; /** Cached value of dimension D from lastStaticReport - for faster reads */ @GuardedBy("trackLock") private Integer shipDimensionStarboard; @GuardedBy("trackLock") private long timeOfLastUpdate = 0L; @GuardedBy("trackLock") private long timeOfLastPositionReport = 0L; /** * Create a new track with the given MMSI no. * @param mmsi the MMSI no. assigned to this track. */ public Track(int mmsi) { super(mmsi); } /** * Deep copy/clone a track */ @Override public Track clone() throws CloneNotSupportedException { Track clone = (Track) super.clone(); // Do not share locks clone.trackLock = new ReentrantLock(); // Deep copy collection TreeSet<TrackingReport> trackingReportsClone = treeSetSupplier.get(); clone.trackingReports.forEach(tr -> trackingReportsClone.add(tr)); clone.trackingReports = trackingReportsClone; return clone; } @Override public String toString() { final StringBuilder sb = new StringBuilder("Track{"); sb.append("positionReportPurgeEnable=").append(positionReportPurgeEnable); sb.append(", trackingReports=").append(trackingReports); sb.append(", properties=").append(properties); sb.append(", lastStaticReport=").append(lastStaticReport); sb.append(", shipType=").append(shipType); sb.append(", shipName='").append(shipName).append('\''); sb.append(", callsign='").append(callsign).append('\''); sb.append(", imo=").append(imo); sb.append(", shipDimensionBow=").append(shipDimensionBow); sb.append(", shipDimensionStern=").append(shipDimensionStern); sb.append(", shipDimensionPort=").append(shipDimensionPort); sb.append(", shipDimensionStarboard=").append(shipDimensionStarboard); sb.append(", timeOfLastUpdate=").append(timeOfLastUpdate); sb.append(", timeOfLastPositionReport=").append(timeOfLastPositionReport); sb.append(", mmsi=").append(getMmsi()); sb.append(", timeOfLastAisTrackingReport=").append(getTimeOfLastAisTrackingReport()); sb.append(", oldestTrackingReport=").append(getOldestTrackingReport()); sb.append(", newestTrackingReport=").append(getNewestTrackingReport()); sb.append(", IMO=").append(getIMO()); sb.append(", vesselLength=").append(getVesselLength()); sb.append(", vesselBeam=").append(getVesselBeam()); sb.append(", position=").append(getPosition()); sb.append(", speedOverGround=").append(getSpeedOverGround()); sb.append(", courseOverGround=").append(getCourseOverGround()); sb.append(", trueHeading=").append(getTrueHeading()); sb.append('}'); return sb.toString(); } @Override public int hashCode() { int result = (positionReportPurgeEnable ? 1 : 0); result = 31*result + (trackingReports != null ? trackingReports.hashCode() : 0); result = 31*result + (properties != null ? properties.hashCode() : 0); result = 31*result + (lastStaticReport != null ? lastStaticReport.hashCode() : 0); result = 31*result + (shipType != null ? shipType.hashCode() : 0); result = 31*result + (shipName != null ? shipName.hashCode() : 0); result = 31*result + (callsign != null ? callsign.hashCode() : 0); result = 31*result + (imo != null ? imo.hashCode() : 0); result = 31*result + (shipDimensionBow != null ? shipDimensionBow.hashCode() : 0); result = 31*result + (shipDimensionStern != null ? shipDimensionStern.hashCode() : 0); result = 31*result + (shipDimensionPort != null ? shipDimensionPort.hashCode() : 0); result = 31*result + (shipDimensionStarboard != null ? shipDimensionStarboard.hashCode() : 0); result = 31*result + (int) (timeOfLastUpdate ^ (timeOfLastUpdate >>> 32)); result = 31*result + (int) (timeOfLastPositionReport ^ (timeOfLastPositionReport >>> 32)); return result; } /* * Update this track with a new AisPacket. The MMSI no. inside the packet must match this Track's MMSI no. * and in order to maintain low insertion-cost only packets newer than the previously received are accepted. * * This update will be treated as an update received from a real AIS source and the packet will be stored for * a period of time to support replay. * * @param p */ public void update(AisPacket p) { checkAisPacket(p); AisMessage msg = p.tryGetAisMessage(); if (msg instanceof AisStaticCommon) { addStaticReport(p); } if (msg instanceof IVesselPositionMessage) { addTrackingReport(new AisTrackingReport(p)); } } /** * Update this track with a new AisPacket. The MMSI no. inside the packet must match this Track's MMSI no. * and in order to maintain low insertion-cost only packets newer than the previously received are accepted. * * This update is treated as an interpolated (artifical, non-real) update. * * @param m */ public void update(long timestamp, AisMessage m) { if (m instanceof IVesselPositionMessage) { IVesselPositionMessage pm = (IVesselPositionMessage) m; update(timestamp, pm.getValidPosition(), (float) (pm.getCog() / 10.0), (float) (pm.getSog() / 10.0), pm.getTrueHeading()); } } /** * Update this track with interpolated or predicted information (as opposed to information * received from an AIS receiver or basestation). */ public void update(long timestamp, Position position, float cog, float sog, float hdg) { InterpolatedTrackingReport trackingReport = new InterpolatedTrackingReport(timestamp, position, cog, sog, hdg); addTrackingReport(trackingReport); } /** * Get a custom property previously set on this track. * @param propertyName * @return */ public Object getProperty(String propertyName) { return threadSafeGetStaticData(() -> properties.get(propertyName)); } /** * Attach/set a custom property on this track. * * @param propertyName * @param propertyValue */ public void setProperty(String propertyName, Object propertyValue) { try { trackLock.lock(); properties.put(propertyName, propertyValue); } finally { trackLock.unlock(); } } /** * Remove a custom property previously set on this track. * * @param propertyName */ public void removeProperty(String propertyName) { try { trackLock.lock(); properties.remove(propertyName); } finally { trackLock.unlock(); } } /** Get the timestamp of the last update of any type. */ public long getTimeOfLastUpdate() { return threadSafeGetStaticData(() -> timeOfLastUpdate); } /** Get the timestamp of the last update of any type. */ public LocalDateTime getTimeOfLastUpdateTyped() { return DateTimeUtil.MILLIS_TO_LOCALDATETIME_UTC.apply(getTimeOfLastUpdate()); } /** Get the timestamp of the last position report. */ public long getTimeOfLastPositionReport() { return threadSafeGetStaticData(() -> timeOfLastPositionReport); } /** Get the timestamp of the last position report. */ public LocalDateTime getTimeOfLastPositionReportTyped() { return DateTimeUtil.MILLIS_TO_LOCALDATETIME_UTC.apply(getTimeOfLastPositionReport()); } /** Get time timestamp of the last non-predicted position report */ public long getTimeOfLastAisTrackingReport() { try { trackLock.lock(); return trackingReports .stream() .filter(tr -> tr instanceof AisTrackingReport) .max(Comparator.comparing(tr -> tr.getTimestamp())) .get() .getTimestamp(); } catch(NoSuchElementException e) { return -1; } finally { trackLock.unlock(); } } /** Get time timestamp of the last non-predicted position report */ public LocalDateTime getTimeOfLastAisTrackingReportTyped() { return DateTimeUtil.MILLIS_TO_LOCALDATETIME_UTC.apply(getTimeOfLastAisTrackingReport()); } /** Return the last received static report (if any) */ public AisPacket getLastStaticReport() { return threadSafeGetStaticData(() -> lastStaticReport); } /** * Enable or disable purging of old position reports from the track. * @param positionReportPurgeEnable */ public void setPositionReportPurgeEnable(boolean positionReportPurgeEnable) { this.positionReportPurgeEnable = positionReportPurgeEnable; } /** Update the track with a new static report */ private void addStaticReport(AisPacket p) { AisStaticCommon msg = (AisStaticCommon) p.tryGetAisMessage(); try { trackLock.lock(); lastStaticReport = p; timeOfLastUpdate = p.getBestTimestamp(); callsign = msg.getCallsign(); shipType = msg.getShipType(); shipName = msg.getName(); shipDimensionBow = msg.getDimBow(); shipDimensionStern = msg.getDimStern(); shipDimensionPort = msg.getDimPort(); shipDimensionStarboard = msg.getDimStarboard(); if (msg instanceof AisMessage5) { imo = (int) ((AisMessage5) msg).getImo(); } } finally { trackLock.unlock(); } } /** Update the track with a new trackingReport */ private void addTrackingReport(TrackingReport trackingReport) { try { trackLock.lock(); trackingReports.add(trackingReport); timeOfLastUpdate = trackingReport.getTimestamp(); timeOfLastPositionReport = trackingReports.last().getTimestamp(); } finally { trackLock.unlock(); } purgeTrackingReports(MAX_AGE_POSITION_REPORTS_MINUTES); } /** Get the oldest reported position report kept. */ public TrackingReport getOldestTrackingReport() { TrackingReport oldestTrackingReport = null; try { trackLock.lock(); oldestTrackingReport = trackingReports.first(); } catch(NoSuchElementException e) { } finally { trackLock.unlock(); } return oldestTrackingReport; } /** Get the most recently reported position report. */ public TrackingReport getNewestTrackingReport() { TrackingReport mostRecentTrackingReport = null; try { trackLock.lock(); mostRecentTrackingReport = trackingReports.last(); } catch(NoSuchElementException e) { } finally { trackLock.unlock(); } return mostRecentTrackingReport; } /** Get the the position report at time t. */ public TrackingReport getTrackingReportAt(long t) { TrackingReport trackingReportAtT = null; try { trackLock.lock(); Iterator<TrackingReport> iterator = trackingReports.iterator(); while(iterator.hasNext()) { trackingReportAtT = iterator.next(); if (trackingReportAtT.getTimestamp() == t) { break; } trackingReportAtT = null; } } finally { trackLock.unlock(); } return trackingReportAtT; } /** * Get a trail of historic position reports. The oldest will be a maximum of MAX_AGE_POSITION_REPORTS_MINUTES * minutes older than the newest (if position report purging is enabled; otherwise there is no limit). * @return */ public List<TrackingReport> getTrackingReports() { purgeTrackingReports(MAX_AGE_POSITION_REPORTS_MINUTES); ImmutableList<TrackingReport> trackingReports1 = null; try { trackLock.lock(); trackingReports1 = ImmutableList.copyOf(trackingReports); } finally { trackLock.unlock(); } return trackingReports1; } /** * Purge position reports older than the given amount of minutes, compared to the most * recent stored position report. * @return */ private void purgeTrackingReports(int maxAgeMinutes) { if (positionReportPurgeEnable) { TrackingReport newestTrackingReport = getNewestTrackingReport(); if (newestTrackingReport != null) { long now = newestTrackingReport.getTimestamp(); long oldestKept = now - maxAgeMinutes * 60 * 1000; trackLock.lock(); try { trackingReports.removeIf(tr -> tr.getTimestamp() < oldestKept); } finally { trackLock.unlock(); } } } } /** * Predict this track's position forward to time atTime. * @param atTime timestamp in milliseconds since the Epoch */ public void predict(long atTime) { long now = getTimeOfLastPositionReport(); if (atTime > now) { TrackingReport trackingReport = getNewestTrackingReport(); if (trackingReport != null) { Position currentPosition = trackingReport.getPosition(); float cog = trackingReport.getCourseOverGround(); float sog = trackingReport.getSpeedOverGround(); float hdg = trackingReport.getTrueHeading(); long deltaMillis = atTime - now; float deltaSeconds = deltaMillis / 1000; float deltaMinutes = deltaSeconds / 60; float deltaHours = deltaMinutes / 60; float distanceNauticalMiles = sog * deltaHours; float distanceMeters = distanceNauticalMiles * 1852; Position predictedPosition = CoordinateSystem.CARTESIAN.pointOnBearing(currentPosition, distanceMeters, cog); addTrackingReport(new InterpolatedTrackingReport(atTime, predictedPosition, cog, sog, hdg /* TODO should be PredictedTrackingReport */)); } else { throw new IllegalStateException("No enough data to predict future position."); } } else { throw new IllegalArgumentException("atTime (" + atTime + ") should be ahead of track's last position update (" + now + ")."); } } private void checkAisPacket(AisPacket p) { AisMessage msg = p.tryGetAisMessage(); if (msg == null) { throw new IllegalArgumentException("Cannot extract AisMessage from AisPacket: " + p.toString()); } if (msg.getUserId() != getMmsi()) { throw new IllegalArgumentException("This track only accepts updates for MMSI " + getMmsi() + ", not " + msg.getUserId() + "."); } } /** Convenience method to return track's last reported ship type or null */ public Integer getShipType() { return threadSafeGetStaticData(() -> shipType); } /** Convenience method to return track's last reported ship name or null */ public String getShipName() { return threadSafeGetStaticData(() -> shipName); } /** Convenience method to return track's last reported callsign or null */ public String getCallsign() { return threadSafeGetStaticData(() -> callsign); } /** Convenience method to return track's last reported IMO no. or null */ public Integer getIMO() { return threadSafeGetStaticData(() -> imo); } /** Convenience method to return track's last reported vessel length or null */ public Integer getVesselLength() { return shipDimensionBow == null || shipDimensionStern == null ? null : shipDimensionBow + shipDimensionStern; } /** Convenience method to return track's last reported vessel beam or null */ public Integer getVesselBeam() { return shipDimensionPort == null || shipDimensionStarboard == null ? null : shipDimensionPort + shipDimensionStarboard; } public Integer getShipDimensionBow() { return threadSafeGetStaticData(() -> shipDimensionBow); } public Integer getShipDimensionStern() { return threadSafeGetStaticData(() -> shipDimensionStern); } public Integer getShipDimensionPort() { return threadSafeGetStaticData(() -> shipDimensionPort); } public Integer getShipDimensionStarboard() { return threadSafeGetStaticData(() -> shipDimensionStarboard); } /** Convenience method to return track's last reported position or null */ public Position getPosition() { return nullAndThreadSafeGetFromNewestTrackingReport(tp -> tp.getPosition()); } /** Convenience method to return track's last reported SOG or null */ public Float getSpeedOverGround() { return nullAndThreadSafeGetFromNewestTrackingReport(tp -> tp.getSpeedOverGround()); } /** Convenience method to return track's last reported COG or null */ public Float getCourseOverGround() { return nullAndThreadSafeGetFromNewestTrackingReport(tp -> tp.getCourseOverGround()); } /** Convenience method to return track's last reported heading or null */ public Float getTrueHeading() { return nullAndThreadSafeGetFromNewestTrackingReport(tp -> tp.getTrueHeading()); } private <T> T threadSafeGetStaticData(Supplier<T> getter) { T value = null; try { trackLock.lock(); value = getter.get(); } finally { trackLock.unlock(); } return value; } private <T> T nullAndThreadSafeGetFromNewestTrackingReport(Function<? super TrackingReport, T> getter) { T value = null; try { trackLock.lock(); TrackingReport tp = getNewestTrackingReport(); if (tp != null) { try { value = getter.apply(tp); } catch (NullPointerException e) { } } } finally { trackLock.unlock(); } return value; } }