/* 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.inject.Inject;
import dk.dma.ais.abnormal.analyzer.AppStatisticsService;
import dk.dma.ais.abnormal.analyzer.services.SafetyZoneService;
import dk.dma.ais.abnormal.event.db.EventRepository;
import dk.dma.ais.abnormal.event.db.domain.CloseEncounterEvent;
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.CloseEncounterEventBuilder;
import dk.dma.ais.abnormal.util.Categorizer;
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.enav.model.geometry.CoordinateSystem;
import dk.dma.enav.model.geometry.Ellipse;
import dk.dma.enav.util.CoordinateConverter;
import net.jcip.annotations.NotThreadSafe;
import org.apache.commons.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.TreeSet;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_CLOSEENCOUNTER_PREDICTIONTIME_MAX;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_CLOSEENCOUNTER_RUN_PERIOD;
import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_CLOSEENCOUNTER_SOG_MIN;
import static dk.dma.ais.abnormal.util.AisDataHelper.nameOrMmsi;
import static dk.dma.ais.abnormal.util.TrackPredicates.isEngagedInFishing;
import static dk.dma.ais.abnormal.util.TrackPredicates.isEngagedInTowing;
import static dk.dma.ais.abnormal.util.TrackPredicates.isFishingVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isSlowVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isSmallVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isSpeedInvalid;
import static dk.dma.ais.abnormal.util.TrackPredicates.isSupportVessel;
import static dk.dma.ais.abnormal.util.TrackPredicates.isUndefinedVessel;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.stream.Collectors.toSet;
/**
* This analysis manages events where two vessels have a close encounter and therefore
* are in risk of collision.
*
* The analysis works by calculating an ellipse around each ship. The ellipse's size and orientation depends on the
* vessels loa, beam, speed, and course. The ellipse is translated forward from the vessel's center so that it covers
* a larger area in front of the vessel. If two ellipses intersect there is a risk of collision and this is registered
* as an abnormal event.
*
* This analysis is rather extensive, and we can therefore now allow to block the EventBus
* for the duration of a complete analysis. Instead the worked is spawned to a separate worker
* thread.
*
* @author Thomas Borg Salling <tbsalling@tbsalling.dk>
*/
@NotThreadSafe
public class CloseEncounterAnalysis extends PeriodicAnalysis {
private static final Logger LOG = LoggerFactory.getLogger(CloseEncounterAnalysis.class);
private final AppStatisticsService statisticsService;
private final SafetyZoneService safetyZoneService;
/** Minimum speed over ground to consider close encounter (in knots) */
private final float sogMin;
@Inject
public CloseEncounterAnalysis(Configuration configuration, AppStatisticsService statisticsService, EventEmittingTracker trackingService, EventRepository eventRepository, SafetyZoneService safetyZoneService) {
super(eventRepository, trackingService, null);
this.statisticsService = statisticsService;
this.safetyZoneService = safetyZoneService;
this.sogMin = configuration.getFloat(CONFKEY_ANALYSIS_CLOSEENCOUNTER_SOG_MIN, 5.0f);
setTrackPredictionTimeMax(configuration.getInteger(CONFKEY_ANALYSIS_CLOSEENCOUNTER_PREDICTIONTIME_MAX, -1));
setAnalysisPeriodMillis(configuration.getInt(CONFKEY_ANALYSIS_CLOSEENCOUNTER_RUN_PERIOD, 30000) * 1000);
LOG.info(this.getClass().getSimpleName() + " created (" + this + ").");
}
@Override
public String toString() {
return "CloseEncounterAnalysis{" +
"sogMin=" + sogMin +
"} " + super.toString();
}
protected void performAnalysis() {
LOG.debug("Starting " + getAnalysisName());
final long systemTimeMillisBeforeAnalysis = System.currentTimeMillis();
Collection<Track> tracks = getTrackingService().getTracks();
tracks.forEach(
t -> analyseCloseEncounters(tracks, t)
);
final long systemTimeMillisAfterAnalysis = System.currentTimeMillis();
statisticsService.incAnalysisStatistics(getAnalysisName(), "Analyses performed");
LOG.debug(getAnalysisName() + " of " + tracks.size() + " tracks completed in " + (systemTimeMillisAfterAnalysis - systemTimeMillisBeforeAnalysis) + " msecs.");
}
private void analyseCloseEncounters(Collection<Track> allTracks, Track track) {
clearTrackPairsAnalyzed();
if (
isSupportVessel.negate().test(track) &&
isEngagedInTowing.negate().test(track) &&
isSpeedInvalid.negate().test(track) &&
(track.getSpeedOverGround() == null || track.getSpeedOverGround() > sogMin)
) {
findNearByTracks(allTracks, track, 60000, 1852)
.stream()
.filter(isSupportVessel.negate())
.filter(isEngagedInTowing.negate())
.forEach(nearByTrack -> {
if (isTrackPairAnalyzed(track, nearByTrack)) {
return;
}
if (isSlowVessel.test(nearByTrack)) {
return;
}
if (nearByTrack.getSpeedOverGround() != null && nearByTrack.getSpeedOverGround() < sogMin) {
return;
}
if (isSmallVessel.test(track) && isSmallVessel.test(nearByTrack)) {
return;
}
if (isFishingVessel.test(track) && isFishingVessel.test(nearByTrack)) {
return;
}
if (isEngagedInFishing.test(track) && isEngagedInFishing.test(nearByTrack)) {
return;
}
if (isUndefinedVessel.test(track) && isUndefinedVessel.test(nearByTrack)) {
return;
}
analyseCloseEncounter(track, nearByTrack);
});
}
}
void analyseCloseEncounter(Track track1, Track track2) {
final long t = max(track1.getTimeOfLastPositionReport(), track2.getTimeOfLastPositionReport());
if (t > track1.getTimeOfLastPositionReport()) {
track1.predict(t);
}
if (t > track2.getTimeOfLastPositionReport()) {
track2.predict(t);
}
if (isLastAisTrackingReportTooOld(track1, t)) {
LOG.debug("Skipping analysis: MMSI " + track1.getMmsi() + " was predicted for too long.");
return;
}
if (isLastAisTrackingReportTooOld(track2, t)) {
LOG.debug("Skipping analysis: MMSI " + track2.getMmsi() + " was predicted for too long.");
return;
}
boolean allValuesPresent = false;
float track1Cog=Float.NaN, track1Sog=Float.NaN, track2Hdg=Float.NaN;
int track1Loa=-1, track1Beam=-1, track1Stern=-1, track1Starboard=-1, track2Loa=-1, track2Beam=-1, track2Stern=-1, track2Starboard=-1;
try {
track1Cog = track1.getCourseOverGround();
track1Sog = track1.getSpeedOverGround();
track1Loa = track1.getVesselLength();
track1Beam = track1.getVesselBeam();
track1Stern = track1.getShipDimensionStern();
track1Starboard = track1.getShipDimensionStarboard();
track2Hdg = track2.getTrueHeading();
track2Loa = track2.getVesselLength();
track2Beam = track2.getVesselBeam();
track2Stern = track2.getShipDimensionStern();
track2Starboard = track2.getShipDimensionStarboard();
allValuesPresent = true;
} catch(NullPointerException e) {
}
if (allValuesPresent && !Float.isNaN(track1Cog) && !Float.isNaN(track2Hdg)) {
Ellipse safetyEllipseTrack1 = safetyZoneService.safetyZone(track1.getPosition(), track1.getPosition(), track1Cog, track1Sog, track1Loa, track1Beam, track1Stern, track1Starboard);
Ellipse extentTrack2 = safetyZoneService.vesselExtent(track1.getPosition(), track2.getPosition(), track2Hdg, track2Loa, track2Beam, track2Stern, track2Starboard);
if (safetyEllipseTrack1 != null && extentTrack2 != null && safetyEllipseTrack1.intersects(extentTrack2)) {
track1.setProperty(Track.SAFETY_ZONE, safetyEllipseTrack1);
track2.setProperty(Track.EXTENT, extentTrack2);
raiseOrMaintainAbnormalEvent(CloseEncounterEvent.class, track1, track2);
} else {
lowerExistingAbnormalEventIfExists(CloseEncounterEvent.class, track1);
}
}
markTrackPairAnalyzed(track1, track2);
}
private Set<String> trackPairsAnalyzed;
void clearTrackPairsAnalyzed() {
trackPairsAnalyzed = new TreeSet<>();
}
void markTrackPairAnalyzed(Track track1, Track track2) {
String trackPairKey = calculateTrackPairKey(track1, track2);
trackPairsAnalyzed.add(trackPairKey);
}
static String calculateTrackPairKey(Track track1, Track track2) {
int mmsi1 = track1.getMmsi();
int mmsi2 = track2.getMmsi();
return min(mmsi1, mmsi2) + "-" + max(mmsi1, mmsi2);
}
boolean isTrackPairAnalyzed(Track track1, Track track2) {
String trackPairKey = calculateTrackPairKey(track1, track2);
return trackPairsAnalyzed.contains(trackPairKey);
}
/**
* In the set of candidateTracks: find the candidateTracks which are near to the nearToTrack - with 'near'
* defined as
*
* - last reported position timestamp within +/- 1 minute of nearToTrack's
* - last reported position within 1 nm of nearToTrack
*
* @param candidateTracks the set of candidate candidateTracks to search among.
* @param nearToTrack the nearToTrack to find other near-by candidateTracks for.
* @return the set of nearby candidateTracks
*/
Set<Track> findNearByTracks(Collection<Track> candidateTracks, Track nearToTrack, int maxTimestampDeviationMillis, int maxDistanceDeviationMeters) {
Set<Track> nearbyTracks = Collections.EMPTY_SET;
TrackingReport positionReport = nearToTrack.getNewestTrackingReport();
if (positionReport != null) {
final long timestamp = positionReport.getTimestamp();
nearbyTracks = candidateTracks.parallelStream().filter(candidateTrack ->
candidateTrack.getMmsi() != nearToTrack.getMmsi() &&
candidateTrack.getTimeOfLastPositionReport() > 0L &&
candidateTrack.getTimeOfLastPositionReport() > timestamp - maxTimestampDeviationMillis &&
candidateTrack.getTimeOfLastPositionReport() < timestamp + maxTimestampDeviationMillis &&
candidateTrack.getPosition().distanceTo(nearToTrack.getPosition(), CoordinateSystem.CARTESIAN) < maxDistanceDeviationMeters
).collect(toSet());
}
return nearbyTracks;
}
@Override
protected Event buildEvent(Track primaryTrack, Track... otherTracks) {
if (otherTracks == null) {
throw new IllegalArgumentException("otherTracks cannot be null.");
}
if (otherTracks.length != 1) {
throw new IllegalArgumentException("otherTracks.length must be exactly 1, not " + otherTracks.length + ".");
}
final Track secondaryTrack = otherTracks[0];
String primaryShipName = nameOrMmsi(primaryTrack.getShipName(), primaryTrack.getMmsi());
String secondaryShipName = nameOrMmsi(secondaryTrack.getShipName(), secondaryTrack.getMmsi());
String primaryShipType = "unknown type";
Integer primaryShipTypeBoxed = primaryTrack.getShipType();
short primaryShipTypeCategory = Categorizer.mapShipTypeToCategory(primaryShipTypeBoxed);
if (primaryShipTypeBoxed != null) {
primaryShipType = Categorizer.mapShipTypeCategoryToString(primaryShipTypeCategory);
}
short primaryShipLengthCategory = Categorizer.mapShipLengthToCategory(primaryTrack.getVesselLength());
String secondaryShipType = "?";
Integer secondaryShipTypeBoxed = secondaryTrack.getShipType();
short secondaryShipTypeCategory = Categorizer.mapShipTypeToCategory(secondaryShipTypeBoxed);
if (secondaryShipTypeBoxed != null) {
secondaryShipType = Categorizer.mapShipTypeCategoryToString(secondaryShipTypeCategory);
}
short secondaryShipLengthCategory = Categorizer.mapShipLengthToCategory(secondaryTrack.getVesselLength());
StringBuffer title = new StringBuffer();
title.append("Close encounter");
StringBuffer description = new StringBuffer();
description.append("Close encounter between ");
description.append(primaryShipName);
description.append(" (" + primaryShipType + ") and ");
description.append(secondaryShipName);
description.append(" (" + secondaryShipType + ") on ");
description.append(DATE_FORMAT.format(primaryTrack.getTimeOfLastPositionReportTyped()));
description.append(".");
Ellipse primaryTrackSafetyEllipse = (Ellipse) primaryTrack.getProperty(Track.SAFETY_ZONE);
Ellipse secondaryTrackExtent = (Ellipse) secondaryTrack.getProperty(Track.EXTENT);
CoordinateConverter CoordinateConverter = new CoordinateConverter(primaryTrackSafetyEllipse.getGeodeticReference().getLongitude(), primaryTrackSafetyEllipse.getGeodeticReference().getLatitude());
double primaryTrackLatitude = CoordinateConverter.y2Lat(primaryTrackSafetyEllipse.getX(), primaryTrackSafetyEllipse.getY());
double primaryTrackLongitude = CoordinateConverter.x2Lon(primaryTrackSafetyEllipse.getX(), primaryTrackSafetyEllipse.getY());
double secondaryTrackLatitude = CoordinateConverter.y2Lat(secondaryTrackExtent.getX(), secondaryTrackExtent.getY());
double secondaryTrackLongitude = CoordinateConverter.x2Lon(secondaryTrackExtent.getX(), secondaryTrackExtent.getY());
statisticsService.incAnalysisStatistics(getAnalysisName(), "Events raised");
LOG.info(description.toString());
Event event =
CloseEncounterEventBuilder.CloseEncounterEvent()
.safetyZoneOfPrimaryVessel()
.targetTimestamp(new Date(primaryTrack.getTimeOfLastPositionReport()))
.centerLatitude(primaryTrackLatitude)
.centerLongitude(primaryTrackLongitude)
.majorAxisHeading(primaryTrackSafetyEllipse.getMajorAxisGeodeticHeading())
.majorSemiAxisLength(primaryTrackSafetyEllipse.getAlpha())
.minorSemiAxisLength(primaryTrackSafetyEllipse.getBeta())
.extentOfSecondaryVessel()
.targetTimestamp(new Date(secondaryTrack.getTimeOfLastPositionReport()))
.centerLatitude(secondaryTrackLatitude)
.centerLongitude(secondaryTrackLongitude)
.majorAxisHeading(secondaryTrackExtent.getMajorAxisGeodeticHeading())
.majorSemiAxisLength(secondaryTrackExtent.getAlpha())
.minorSemiAxisLength(secondaryTrackExtent.getBeta())
.title(title.toString())
.description(description.toString())
.state(Event.State.ONGOING)
.startTime(primaryTrack.getTimeOfLastPositionReportTyped())
.behaviour()
.isPrimary(true)
.vessel()
.mmsi(primaryTrack.getMmsi())
.imo(primaryTrack.getIMO())
.callsign(primaryTrack.getCallsign())
.type(primaryShipTypeBoxed /* primaryShipTypeCategory */)
.toBow(primaryTrack.getShipDimensionBow())
.toStern(primaryTrack.getShipDimensionStern())
.toPort(primaryTrack.getShipDimensionPort())
.toStarboard(primaryTrack.getShipDimensionStarboard())
.name(primaryTrack.getShipName())
.trackingPoint()
.timestamp(primaryTrack.getTimeOfLastPositionReportTyped())
.positionInterpolated(primaryTrack.getNewestTrackingReport() instanceof InterpolatedTrackingReport)
.eventCertainty(TrackingPoint.EventCertainty.RAISED)
.speedOverGround(primaryTrack.getSpeedOverGround())
.courseOverGround(primaryTrack.getCourseOverGround())
.trueHeading(primaryTrack.getTrueHeading())
.latitude(primaryTrack.getPosition().getLatitude())
.longitude(primaryTrack.getPosition().getLongitude())
.behaviour()
.isPrimary(false)
.vessel()
.mmsi(secondaryTrack.getMmsi())
.imo(secondaryTrack.getIMO())
.callsign(secondaryTrack.getCallsign())
.type(secondaryShipTypeBoxed /* secondaryShipTypeCategory */)
.toBow(secondaryTrack.getShipDimensionBow())
.toStern(secondaryTrack.getShipDimensionStern())
.toPort(secondaryTrack.getShipDimensionPort())
.toStarboard(secondaryTrack.getShipDimensionStarboard())
.name(secondaryTrack.getShipName())
.trackingPoint()
.timestamp(secondaryTrack.getTimeOfLastPositionReportTyped())
.positionInterpolated(secondaryTrack.getNewestTrackingReport() instanceof InterpolatedTrackingReport)
.eventCertainty(TrackingPoint.EventCertainty.RAISED)
.speedOverGround(secondaryTrack.getSpeedOverGround())
.courseOverGround(secondaryTrack.getCourseOverGround())
.trueHeading(secondaryTrack.getTrueHeading())
.latitude(secondaryTrack.getPosition().getLatitude())
.longitude(secondaryTrack.getPosition().getLongitude())
.getEvent();
addPreviousTrackingPoints(event, primaryTrack);
addPreviousTrackingPoints(event, secondaryTrack);
return event;
}
}