/* 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.analyzer.behaviour.BehaviourManager; import dk.dma.ais.abnormal.analyzer.behaviour.EventCertainty; import dk.dma.ais.abnormal.analyzer.behaviour.events.AbnormalEventLower; import dk.dma.ais.abnormal.analyzer.behaviour.events.AbnormalEventMaintain; import dk.dma.ais.abnormal.analyzer.behaviour.events.AbnormalEventRaise; import dk.dma.ais.abnormal.event.db.EventRepository; import dk.dma.ais.abnormal.event.db.domain.CourseOverGroundEvent; import dk.dma.ais.abnormal.event.db.domain.Event; import dk.dma.ais.abnormal.event.db.domain.TrackingPoint; import dk.dma.ais.abnormal.stat.db.StatisticDataRepository; import dk.dma.ais.abnormal.stat.db.data.CourseOverGroundStatisticData; import dk.dma.ais.abnormal.stat.db.data.StatisticData; 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.events.CellChangedEvent; 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 static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_COG_CELL_SHIPCOUNT_MIN; import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_COG_PD; import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_COG_PREDICTIONTIME_MAX; import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_COG_SHIPLENGTH_MIN; import static dk.dma.ais.abnormal.analyzer.config.Configuration.CONFKEY_ANALYSIS_COG_USE_AGGREGATED_STATS; import static dk.dma.ais.abnormal.event.db.domain.builders.CourseOverGroundEventBuilder.CourseOverGroundEvent; import static dk.dma.ais.abnormal.util.AisDataHelper.nameOrMmsi; 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.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.isSpecialCraft; import static dk.dma.ais.abnormal.util.TrackPredicates.isUnknownTypeOrSize; /** * This analysis manages events where a vessel has an "abnormal" course over ground * relative to the previous observations for vessels in the same grid cell. Statistics * for previous observations are stored in the StatisticDataRepository. * * @author Thomas Borg Salling <tbsalling@tbsalling.dk> */ public class CourseOverGroundAnalysis extends StatisticBasedAnalysis { private static final Logger LOG = LoggerFactory.getLogger(CourseOverGroundAnalysis.class); private final AppStatisticsService statisticsService; private final int TOTAL_SHIP_COUNT_THRESHOLD; private final float PD; private final int SHIP_LENGTH_MIN; private final boolean USE_AGGREGATED_STATS; @Inject public CourseOverGroundAnalysis(Configuration configuration, AppStatisticsService statisticsService, StatisticDataRepository statisticsRepository, EventEmittingTracker trackingService, EventRepository eventRepository, BehaviourManager behaviourManager) { super(eventRepository, statisticsRepository, trackingService, behaviourManager); this.statisticsService = statisticsService; setTrackPredictionTimeMax(configuration.getInteger(CONFKEY_ANALYSIS_COG_PREDICTIONTIME_MAX, -1)); TOTAL_SHIP_COUNT_THRESHOLD = configuration.getInt(CONFKEY_ANALYSIS_COG_CELL_SHIPCOUNT_MIN, 1000); PD = configuration.getFloat(CONFKEY_ANALYSIS_COG_PD, 0.001f); SHIP_LENGTH_MIN = configuration.getInt(CONFKEY_ANALYSIS_COG_SHIPLENGTH_MIN, 50); USE_AGGREGATED_STATS = configuration.getBoolean(CONFKEY_ANALYSIS_COG_USE_AGGREGATED_STATS, false); LOG.info(getAnalysisName() + " created (" + this + ")."); } @Override public String toString() { return "CourseOverGroundAnalysis{" + "TOTAL_SHIP_COUNT_THRESHOLD=" + TOTAL_SHIP_COUNT_THRESHOLD + ", PD=" + PD + ", SHIP_LENGTH_MIN=" + SHIP_LENGTH_MIN + ", USE_AGGREGATED_STATS=" + USE_AGGREGATED_STATS + "} " + super.toString(); } @AllowConcurrentEvents @Subscribe public void onCellIdChanged(CellChangedEvent trackEvent) { statisticsService.incAnalysisStatistics(getAnalysisName(), "Events received"); Track track = trackEvent.getTrack(); if (isClassB.test(track) || isUnknownTypeOrSize.test(track) || isFishingVessel.test(track) || isSlowVessel.test(track) || isSmallVessel.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; } Long cellId = (Long) track.getProperty(Track.CELL_ID); Integer shipType = track.getShipType(); Integer shipLength = track.getVesselLength(); Float courseOverGround = track.getCourseOverGround(); if (cellId == null) { statisticsService.incAnalysisStatistics(getAnalysisName(), "Unknown cell id"); return; } if (shipType == null) { statisticsService.incAnalysisStatistics(getAnalysisName(), "Unknown ship type"); return; } if (shipLength == null) { statisticsService.incAnalysisStatistics(getAnalysisName(), "Unknown ship length"); return; } if (courseOverGround == null) { statisticsService.incAnalysisStatistics(getAnalysisName(), "Unknown course over ground"); return; } if (shipLength < SHIP_LENGTH_MIN) { statisticsService.incAnalysisStatistics(getAnalysisName(), "LOA < " + SHIP_LENGTH_MIN); return; } int shipTypeKey = Categorizer.mapShipTypeToCategory(shipType) - 1; int shipLengthKey = Categorizer.mapShipLengthToCategory(shipLength) - 1; int courseOverGroundKey = Categorizer.mapCourseOverGroundToCategory(courseOverGround) - 1; if (isAbnormalCourseOverGround(cellId, shipTypeKey, shipLengthKey, courseOverGroundKey)) { getBehaviourManager().abnormalBehaviourDetected(CourseOverGroundEvent.class, track); } else { getBehaviourManager().normalBehaviourDetected(CourseOverGroundEvent.class, track); } } @AllowConcurrentEvents @Subscribe public void onTrackStale(TrackStaleEvent trackEvent) { getBehaviourManager().trackStaleDetected(CourseOverGroundEvent.class, trackEvent.getTrack()); lowerExistingAbnormalEventIfExists(CourseOverGroundEvent.class, trackEvent.getTrack()); } @Subscribe public void onAbnormalEventRaise(AbnormalEventRaise behaviourEvent) { LOG.debug("onAbnormalEventRaise " + behaviourEvent.getTrack().getMmsi()); if (behaviourEvent.getEventClass().equals(CourseOverGroundEvent.class)) { raiseOrMaintainAbnormalEvent(CourseOverGroundEvent.class, behaviourEvent.getTrack()); } } @Subscribe public void onAbnormalEventMaintain(AbnormalEventMaintain behaviourEvent) { LOG.debug("onAbnormalEventMaintain " + behaviourEvent.getTrack().getMmsi()); if (behaviourEvent.getEventClass().equals(CourseOverGroundEvent.class)) { raiseOrMaintainAbnormalEvent(CourseOverGroundEvent.class, behaviourEvent.getTrack()); } } @Subscribe public void onAbnormalEventLower(AbnormalEventLower behaviourEvent) { LOG.debug("onAbnormalEventLower " + behaviourEvent.getTrack().getMmsi()); if (behaviourEvent.getEventClass().equals(CourseOverGroundEvent.class)) { lowerExistingAbnormalEventIfExists(CourseOverGroundEvent.class, behaviourEvent.getTrack()); } } /** * If the probability p(d)<PD and total count>TOTAL_SHIP_COUNT_THRESHOLD then abnormal. p(d)=sum(count)/count for all sog_intervals for * that shiptype and size. * * @param cellId * @param shipTypeKey * @param shipSizeKey * @param courseOverGroundKey * @return true if the presence of size/type with this cog in this cell is abnormal. False otherwise. */ boolean isAbnormalCourseOverGround(Long cellId, int shipTypeKey, int shipSizeKey, int courseOverGroundKey) { float pd = 1.0f; StatisticData courseOverGroundStatisticData = getStatisticDataRepository().getStatisticData("CourseOverGroundStatistic", cellId); if (courseOverGroundStatisticData instanceof CourseOverGroundStatisticData) { Integer totalCount = ((CourseOverGroundStatisticData) courseOverGroundStatisticData).getSumFor(CourseOverGroundStatisticData.STAT_SHIP_COUNT); if (totalCount > TOTAL_SHIP_COUNT_THRESHOLD) { int shipCount = calculateShipCount((CourseOverGroundStatisticData) courseOverGroundStatisticData, shipTypeKey, shipSizeKey, courseOverGroundKey); pd = (float) shipCount / (float) totalCount; LOG.debug("cellId=" + cellId + ", shipType=" + shipTypeKey + ", shipSize=" + shipSizeKey + ", cog=" + courseOverGroundKey + ", shipCount=" + shipCount + ", totalCount=" + totalCount + ", pd=" + pd); } else { LOG.debug("totalCount of " + totalCount + " is not enough statistical data for cell " + cellId); } } LOG.debug("pd = " + pd); boolean isAbnormalCourseOverGround = pd < PD; if (isAbnormalCourseOverGround) { LOG.debug("Abnormal event detected."); } else { LOG.debug("Normal or inconclusive event detected."); } statisticsService.incAnalysisStatistics(getAnalysisName(), "Analyses performed"); return isAbnormalCourseOverGround; } private int calculateShipCount(CourseOverGroundStatisticData courseOverGroundStatisticData, int shipTypeKey, int shipSizeKey, int courseOverGroundKey) { if (USE_AGGREGATED_STATS) { return courseOverGroundStatisticData.aggregateSumOverKey1(shipSizeKey, courseOverGroundKey, CourseOverGroundStatisticData.STAT_SHIP_COUNT); } else { Integer value = courseOverGroundStatisticData.getValue(shipTypeKey, shipSizeKey, courseOverGroundKey, CourseOverGroundStatisticData.STAT_SHIP_COUNT); return value == null ? 0 : value; } } @Override protected Event buildEvent(Track track, Track... otherTracks) { if (otherTracks != null && otherTracks.length > 0) { throw new IllegalArgumentException("otherTracks not supported."); } Integer mmsi = track.getMmsi(); Integer imo = track.getIMO(); String callsign = track.getCallsign(); String name = nameOrMmsi(track.getShipName(), mmsi); Integer shipType = track.getShipType(); Integer shipLength = track.getVesselLength(); Integer shipDimensionToBow = track.getShipDimensionBow(); Integer shipDimensionToStern = track.getShipDimensionStern(); Integer shipDimensionToPort = track.getShipDimensionPort(); Integer shipDimensionToStarboard = track.getShipDimensionStarboard(); LocalDateTime positionTimestamp = track.getTimeOfLastPositionReportTyped(); Position position = track.getPosition(); Float cog = track.getCourseOverGround(); Float sog = track.getSpeedOverGround(); Float hdg = track.getTrueHeading(); Boolean interpolated = track.getNewestTrackingReport() instanceof InterpolatedTrackingReport; TrackingPoint.EventCertainty certainty = TrackingPoint.EventCertainty.UNDEFINED; EventCertainty eventCertainty = getBehaviourManager().getEventCertaintyAtCurrentPosition(CourseOverGroundEvent.class, track); if (eventCertainty != null) { certainty = TrackingPoint.EventCertainty.create(eventCertainty.getCertainty()); } short shipTypeCategory = Categorizer.mapShipTypeToCategory(shipType); short shipLengthCategory = Categorizer.mapShipLengthToCategory(shipLength); short courseOverGroundCategory = Categorizer.mapCourseOverGroundToCategory(cog); short speedOverGroundCategory = Categorizer.mapSpeedOverGroundToCategory(sog); String shipTypeAsString = Categorizer.mapShipTypeCategoryToString(shipTypeCategory); String shipLengthAsString = Categorizer.mapShipSizeCategoryToString(shipLengthCategory); String courseOverGroundAsString = Categorizer.mapCourseOverGroundCategoryToString(courseOverGroundCategory); String speedOverGroundAsString = Categorizer.mapSpeedOverGroundCategoryToString(speedOverGroundCategory); String title = "Abnormal course over ground"; String description = String.format("Abnormal course over ground of " + name + " (" + shipTypeAsString + ") on position " + position + " at " + DATE_FORMAT.format(positionTimestamp) + ": cog:%.0f(%s) sog:%.1f(%s) type:%d(%s) size:%d(%s).", cog, courseOverGroundAsString, sog, speedOverGroundAsString, shipType, shipTypeAsString, shipLength, shipLengthAsString); LOG.info(description); Event event = CourseOverGroundEvent() .shipType(shipTypeCategory) .shipLength(shipLengthCategory) .courseOverGround(courseOverGroundCategory) .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(getAnalysisName(), "Events raised"); return event; } }