/* 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.abnormal.analyzer.analysis;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import dk.dma.ais.abnormal.analyzer.AppStatisticsService;
import dk.dma.ais.abnormal.event.db.EventRepository;
import dk.dma.ais.abnormal.event.db.domain.DriftEvent;
import dk.dma.ais.abnormal.event.db.domain.Event;
import dk.dma.ais.abnormal.event.db.domain.TrackingPoint;
import dk.dma.ais.abnormal.event.db.domain.builders.DriftEventBuilder;
import dk.dma.ais.tracker.eventEmittingTracker.EventEmittingTracker;
import dk.dma.ais.tracker.eventEmittingTracker.InterpolatedTrackingReport;
import dk.dma.ais.tracker.eventEmittingTracker.Track;
import dk.dma.ais.tracker.eventEmittingTracker.TrackingReport;
import dk.dma.ais.tracker.eventEmittingTracker.events.PositionChangedEvent;
import dk.dma.ais.tracker.eventEmittingTracker.events.TrackStaleEvent;
import dk.dma.enav.model.geometry.Position;
import org.apache.commons.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.util.List;
import java.util.TreeSet;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_COGHDG;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_DISTANCE;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_PERIOD;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_PREDICTIONTIME_MAX;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_SHIPLENGTH_MIN;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_SOG_MAX;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_DRIFT_SOG_MIN;
import static dk.dma.ais.abnormal.util.AisDataHelper.isCourseOverGroundAvailable;
import static dk.dma.ais.abnormal.util.AisDataHelper.isSpeedOverGroundAvailable;
import static dk.dma.ais.abnormal.util.AisDataHelper.isTrueHeadingAvailable;
import static dk.dma.ais.abnormal.util.AisDataHelper.nameOrMmsi;
import static dk.dma.ais.abnormal.util.TrackPredicates.isCargoVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isClassB;
import static dk.dma.ais.abnormal.util.TrackPredicates.isEngagedInTowing;
import static dk.dma.ais.abnormal.util.TrackPredicates.isPassengerVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isSpecialCraft;
import static dk.dma.ais.abnormal.util.TrackPredicates.isTankerVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isUnknownTypeOrSize;
import static dk.dma.ais.abnormal.util.TrackPredicates.isVeryLongVessel;
import static dk.dma.enav.util.compass.CompassUtils.absoluteDirectionalDifference;
/**
* This analysis detects vessels with indication for drift.
*
* The analysis is based on deviation between course over ground and heading. For vessels
* with low speed over ground: If a large part of reports received over an interval
* show a significant deviation between cog and hdg then an event is raised.
*
* This analysis is not based on previous observations (statistic data).
*
* @author Thomas Borg Salling <tbsalling@tbsalling.dk>
*/
public class DriftAnalysis extends Analysis {
private static final Logger LOG = LoggerFactory.getLogger(DriftAnalysis.class);
private final AppStatisticsService statisticsService;
/** Track must have sustained sog below this mark to a cause drift event */
final float SPEED_HIGH_MARK;
/** Track must have sustained sog above this mark to a cause drift event */
final float SPEED_LOW_MARK;
/** Minimum no. of degrees to consider heading/course deviation significant */
final float MIN_HDG_COG_DEVIATION_DEGREES;
/** Tracks must drift for this period of time before a drift event is raised */
final int OBSERVATION_PERIOD_MINUTES;
/** Tracks must drift for this distance before a drift event is raised */
final float OBSERVATION_DISTANCE_METERS;
/** Min. length of vessel (in meters) for analysis to be performed */
final int SHIP_LENGTH_MIN;
private TreeSet<Integer> tracksPossiblyDrifting = new TreeSet<>();
private int statCount = 0;
@Inject
public DriftAnalysis(Configuration configuration, AppStatisticsService statisticsService, EventEmittingTracker trackingService, EventRepository eventRepository) {
super(eventRepository, trackingService, null);
this.statisticsService = statisticsService;
setTrackPredictionTimeMax(configuration.getInteger(CONFKEY_ANALYSIS_DRIFT_PREDICTIONTIME_MAX, -1));
SPEED_HIGH_MARK = configuration.getFloat(CONFKEY_ANALYSIS_DRIFT_SOG_MAX, 5.0f);
SPEED_LOW_MARK = configuration.getFloat(CONFKEY_ANALYSIS_DRIFT_SOG_MIN, 1.0f);
MIN_HDG_COG_DEVIATION_DEGREES = configuration.getFloat(CONFKEY_ANALYSIS_DRIFT_COGHDG, 45f);
OBSERVATION_PERIOD_MINUTES = configuration.getInt(CONFKEY_ANALYSIS_DRIFT_PERIOD, 10);
OBSERVATION_DISTANCE_METERS = configuration.getFloat(CONFKEY_ANALYSIS_DRIFT_DISTANCE, 500f);
SHIP_LENGTH_MIN = configuration.getInt(CONFKEY_ANALYSIS_DRIFT_SHIPLENGTH_MIN, 50);
LOG.info(this.getClass().getSimpleName() + " created (" + this + ").");
}
@Override
public String toString() {
return "DriftAnalysis{" +
"SPEED_HIGH_MARK=" + SPEED_HIGH_MARK +
", SPEED_LOW_MARK=" + SPEED_LOW_MARK +
", MIN_HDG_COG_DEVIATION_DEGREES=" + MIN_HDG_COG_DEVIATION_DEGREES +
", OBSERVATION_PERIOD_MINUTES=" + OBSERVATION_PERIOD_MINUTES +
", OBSERVATION_DISTANCE_METERS=" + OBSERVATION_DISTANCE_METERS +
", SHIP_LENGTH_MIN=" + SHIP_LENGTH_MIN +
"} " + super.toString();
}
@AllowConcurrentEvents
@Subscribe
public void onSpeedOverGroundUpdated(PositionChangedEvent trackEvent) {
final Track track = trackEvent.getTrack();
Integer vesselLength = track.getVesselLength();
if (vesselLength != null && vesselLength < SHIP_LENGTH_MIN) {
statisticsService.incAnalysisStatistics(getAnalysisName(), "LOA < " + SHIP_LENGTH_MIN);
return;
}
if ( !isSpeedOverGroundAvailable(track.getSpeedOverGround())
|| !isCourseOverGroundAvailable(track.getCourseOverGround())
|| !isTrueHeadingAvailable(track.getTrueHeading())) {
return;
}
/* Do not perform analysis for vessels with these characteristics: */
if (isClassB.test(track)
|| isUnknownTypeOrSize.test(track)
|| isSpecialCraft.test(track)
|| isEngagedInTowing.test(track)
) {
return;
}
/* Skip analysis if track has been predicted forward for too long */
if (isLastAisTrackingReportTooOld(track, track.getTimeOfLastPositionReport())) {
LOG.debug("Skipping analysis: MMSI " + track.getMmsi() + " was predicted for too long.");
return;
}
/* Perform analysis only for very long vessels and some other vessels: */
if (isVeryLongVessel.test(track) || (isCargoVessel.test(track) || isTankerVessel.test(track) || isPassengerVessel.test(track))) {
performAnalysis(track);
updateApplicationStatistics();
}
}
private void updateApplicationStatistics() {
statisticsService.incAnalysisStatistics(getAnalysisName(), "Analyses performed");
if (statCount++ % 10000 == 0) {
statisticsService.setAnalysisStatistics(getAnalysisName(), "# observation list", tracksPossiblyDrifting.size());
}
}
@AllowConcurrentEvents
@Subscribe
public void onTrackStale(TrackStaleEvent trackEvent) {
final int mmsi = trackEvent.getTrack().getMmsi();
if (tracksPossiblyDrifting.contains(mmsi)) {
LOG.debug(nameOrMmsi(trackEvent.getTrack().getShipName(), mmsi) + " is now stale. Removed from observation list.");
tracksPossiblyDrifting.remove(mmsi);
// TODO lowerEventIfRaised();
}
}
private void performAnalysis(Track track) {
final int mmsi = track.getMmsi();
final float sog = track.getSpeedOverGround();
final float cog = track.getCourseOverGround();
final float hdg = track.getTrueHeading();
if (sog >= SPEED_LOW_MARK && sog <= SPEED_HIGH_MARK && isCourseHeadingDeviationIndicatingDrift(cog, hdg)) {
LOG.debug(nameOrMmsi(track.getShipName(), mmsi) + " exhibits possible drift. Added to observation list.");
tracksPossiblyDrifting.add(mmsi);
if (isSustainedDrift(track)) {
LOG.debug(nameOrMmsi(track.getShipName(), mmsi) + " exhibits sustained drift. Event raised or maintained.");
raiseOrMaintainAbnormalEvent(DriftEvent.class, track);
}
} else if (tracksPossiblyDrifting.contains(mmsi)) {
LOG.debug(nameOrMmsi(track.getShipName(), mmsi) + " appears not be drifting anymore. Removed from observation list.");
tracksPossiblyDrifting.remove(mmsi);
lowerExistingAbnormalEventIfExists(DriftEvent.class, track);
}
}
boolean isSustainedDrift(Track track) {
if (!isTrackedForLongEnough(track)) {
LOG.debug(nameOrMmsi(track.getShipName(), track.getMmsi()) + " not observed for long enough to consider sustained drift.");
return false;
}
return isDriftPeriodLongEnough(track) && isDriftDistanceLongEnough(track);
}
boolean isCourseHeadingDeviationIndicatingDrift(float cog, float hdg) {
return absoluteDirectionalDifference(cog, hdg) > MIN_HDG_COG_DEVIATION_DEGREES && absoluteDirectionalDifference(180f + cog, hdg) > MIN_HDG_COG_DEVIATION_DEGREES;
}
private boolean isDrifting(TrackingReport tr) {
return tr.getSpeedOverGround() >= SPEED_LOW_MARK && tr.getSpeedOverGround() <= SPEED_HIGH_MARK && isCourseHeadingDeviationIndicatingDrift(tr.getCourseOverGround(), tr.getTrueHeading());
}
private boolean isTrackedForLongEnough(Track track) {
return track.getNewestTrackingReport().getTimestamp() - track.getOldestTrackingReport().getTimestamp() > OBSERVATION_PERIOD_MINUTES*60*1000;
}
boolean isDriftPeriodLongEnough(Track track) {
final long t1 = track.getNewestTrackingReport().getTimestamp() - OBSERVATION_PERIOD_MINUTES*60*1000;
return track.getTrackingReports()
.stream()
.filter(tr -> tr.getTimestamp() >= t1)
.allMatch(tr -> isDrifting(tr));
}
/**
* This method isolates the latest sequence of tracking reports with drift
* and returns the distance drifted in this sequence.
*
* @return
*/
boolean isDriftDistanceLongEnough(Track track) {
TrackingReport driftStart, driftEnd;
// Find driftEnd
driftEnd = track.getNewestTrackingReport();
if (!isDrifting(driftEnd)) {
return false;
}
// Find drift start
driftStart = driftEnd;
List<TrackingReport> trackingReports = track.getTrackingReports();
final int n = trackingReports.size();
for (int i = n - 1; i >= 0; i--) {
TrackingReport trackingReport = trackingReports.get(i);
if (isDrifting(trackingReport)) {
driftStart = trackingReport;
} else {
break;
}
}
// Calc distance drifted
final double distanceDriftedInMeters = driftStart.getPosition().rhumbLineDistanceTo(driftEnd.getPosition());
return distanceDriftedInMeters > OBSERVATION_DISTANCE_METERS;
}
@Override
protected Event buildEvent(Track track, Track... otherTracks) {
if (otherTracks != null && otherTracks.length > 0) {
throw new IllegalArgumentException("otherTracks not supported.");
}
final Integer mmsi = track.getMmsi();
final Integer imo = track.getIMO();
final String callsign = track.getCallsign();
final String name = nameOrMmsi(track.getShipName(), mmsi);
final Integer shipType = track.getShipType();
final Integer shipDimensionToBow = track.getShipDimensionBow();
final Integer shipDimensionToStern = track.getShipDimensionStern();
final Integer shipDimensionToPort = track.getShipDimensionPort();
final Integer shipDimensionToStarboard = track.getShipDimensionStarboard();
final LocalDateTime positionTimestamp = track.getTimeOfLastPositionReportTyped();
final Position position = track.getPosition();
final Float cog = track.getCourseOverGround();
final Float sog = track.getSpeedOverGround();
final Float hdg = track.getTrueHeading();
final Boolean interpolated = track.getNewestTrackingReport() instanceof InterpolatedTrackingReport;
final TrackingPoint.EventCertainty certainty = TrackingPoint.EventCertainty.RAISED;
final String title = "Drift";
final String description = String.format("%s is drifting on position %s at %s", name, position, DATE_FORMAT.format(positionTimestamp));
LOG.info(description);
Event event =
DriftEventBuilder.DriftEvent()
.title(title)
.description(description)
.startTime(positionTimestamp)
.behaviour()
.isPrimary(true)
.vessel()
.mmsi(mmsi)
.imo(imo)
.callsign(callsign)
.type(shipType /* shipTypeCategory */)
.toBow(shipDimensionToBow)
.toStern(shipDimensionToStern)
.toPort(shipDimensionToPort)
.toStarboard(shipDimensionToStarboard)
.name(name)
.trackingPoint()
.timestamp(positionTimestamp)
.positionInterpolated(interpolated)
.eventCertainty(certainty)
.speedOverGround(sog)
.courseOverGround(cog)
.trueHeading(hdg)
.latitude(position.getLatitude())
.longitude(position.getLongitude())
.getEvent();
addPreviousTrackingPoints(event, track);
statisticsService.incAnalysisStatistics(this.getClass().getSimpleName(), "Events raised");
return event;
}
}