Java tutorial
/* Copyright (c) 2011 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dk.dma.ais.tracker; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import dk.dma.ais.binary.SixbitException; import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisMessage5; import dk.dma.ais.message.AisMessageException; import dk.dma.ais.message.AisPositionMessage; import dk.dma.ais.message.IVesselPositionMessage; import dk.dma.ais.message.NavigationalStatus; import dk.dma.ais.message.ShipTypeCargo; import dk.dma.ais.packet.AisPacket; import dk.dma.ais.packet.AisPacketReader; import dk.dma.ais.packet.AisPacketStream; import dk.dma.ais.packet.AisPacketStream.Subscription; import dk.dma.enav.model.geometry.BoundingBox; import dk.dma.enav.model.geometry.CoordinateSystem; import dk.dma.enav.model.geometry.Position; import dk.dma.enav.model.geometry.PositionTime; import java.util.function.Consumer; import net.jcip.annotations.Immutable; import net.jcip.annotations.NotThreadSafe; import org.apache.commons.lang.StringUtils; import javax.annotation.Nullable; import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.TreeMap; /** * This class can process a finite stream of AisPackets, and build a scenario * consisting of all received targets and a history of their movements. * * @author Thomas Borg Salling */ @NotThreadSafe public class ScenarioTracker implements Tracker { @Override public Subscription readFromStream(AisPacketStream stream) { return stream.subscribe(new Consumer<AisPacket>() { public void accept(AisPacket p) { update(p); } }); } public void readFromPacketReader(AisPacketReader packetReader) throws IOException { packetReader.forEachRemaining(new Consumer<AisPacket>() { @Override public void accept(AisPacket p) { update(p); } }); } /** * Get the Date of the first update in this scenario. * @return */ public Date scenarioBegin() { Date scenarioBegin = null; Set<Map.Entry<Integer, Target>> entries = targets.entrySet(); Iterator<Map.Entry<Integer, Target>> i = entries.iterator(); while (i.hasNext()) { Target target = i.next().getValue(); try { Date targetFirstUpdate = target.positionReports.firstKey(); if (scenarioBegin == null || targetFirstUpdate.before(scenarioBegin)) { scenarioBegin = targetFirstUpdate; } } catch (NoSuchElementException e) { } } return scenarioBegin; } /** * Get the Date of the last update in this scenario. * @return */ public Date scenarioEnd() { Date scenarioEnd = null; Set<Map.Entry<Integer, Target>> entries = targets.entrySet(); Iterator<Map.Entry<Integer, Target>> i = entries.iterator(); while (i.hasNext()) { Target target = i.next().getValue(); try { Date targetFirstUpdate = target.positionReports.lastKey(); if (scenarioEnd == null || targetFirstUpdate.after(scenarioEnd)) { scenarioEnd = targetFirstUpdate; } } catch (NoSuchElementException e) { } } return scenarioEnd; } /** * Get bounding box containing all movements in this scenario. * @return */ public BoundingBox boundingBox() { return boundingBox; } /** * Return all targets involved in this scenario. * @return */ public ImmutableSet<Target> getTargets() { return ImmutableSet.copyOf(targets.values()); } /** * Return all targets involved in this scenario and with a known location (ie. located inside of the bounding box). * @return */ public Set<Target> getTargetsHavingPositionUpdates() { return Sets.filter(getTargets(), new com.google.common.base.Predicate<Target>() { @Override public boolean apply(@Nullable Target target) { return target.hasPosition(); } }); } public void update(AisPacket p) { AisMessage message; try { message = p.getAisMessage(); int mmsi = message.getUserId(); Target target; if (!targets.containsKey(mmsi)) { target = new Target(); targets.put(mmsi, target); } else { target = targets.get(mmsi); } if (message instanceof IVesselPositionMessage) { updateBoundingBox((IVesselPositionMessage) message); } target.update(p); } catch (AisMessageException | SixbitException e) { // fail silently on unparsable packets //e.printStackTrace(); } } public void tagTarget(int mmsi, Object tag) { targets.get(mmsi).setTag(tag); } private final Map<Integer, Target> targets = new TreeMap<>(); private BoundingBox boundingBox; private void updateBoundingBox(IVesselPositionMessage positionMessage) { if (positionMessage.isPositionValid()) { Position position = positionMessage.getValidPosition(); if (position != null) { if (boundingBox == null) { boundingBox = BoundingBox.create(position, position, CoordinateSystem.CARTESIAN); } else { boundingBox = boundingBox .include(BoundingBox.create(position, position, CoordinateSystem.CARTESIAN)); } } } } private static String aisStringToJavaString(String aisString) { return aisString.replace('@', ' ').trim(); } @NotThreadSafe public final class Target implements Cloneable { public Target() { } public String getName() { return StringUtils.isBlank(name) ? getMmsi() : name; } public String getMmsi() { return String.valueOf(mmsi); } public int getImo() { return imo; } public String getDestination() { return destination == null ? "" : destination; } public ShipTypeCargo getShipTypeCargo() { return shipTypeCargo; } public String getCargoTypeAsString() { return shipTypeCargo == null ? null : shipTypeCargo.prettyCargo(); } public String getShipTypeAsString() { return shipTypeCargo == null ? null : shipTypeCargo.prettyType(); } public int getToBow() { return toBow; } public int getToStern() { return toStern; } public int getToPort() { return toPort; } public int getToStarboard() { return toStarboard; } private void setTag(Object tag) { tags.add(tag); } public boolean isTagged(Object tag) { return tags.contains(tag); } public boolean hasPosition() { return positionReports.size() > 0; } public Set<PositionReport> getPositionReports() { return ImmutableSet.copyOf(positionReports.values()); } public Date timeOfFirstPositionReport() { return positionReports.firstKey(); } public Date timeOfLastPositionReport() { return positionReports.lastKey(); } /** * Return position at at time atTime. If a real position report which is not older than 'maxAge' seconds compared to * atTime exists, then that real AIS-based position report will be returned. Otherwise a position report will * be inter- or extrapolated to provide an estimated position report. * * @param atTime The time at which to return a position report. * @param maxAge The max. no. of seconds to look back in history for a position report before estimating one. * @return An AIS-based or estimated position report. */ public PositionReport getPositionReportAt(Date atTime, int maxAge) { PositionReport positionReport = getPositionReportNear(atTime, maxAge); if (positionReport == null) { /* no position report at desired time - will estimate using interpolation or dead reckoning */ Map.Entry<Date, PositionReport> entry1 = positionReports.lowerEntry(atTime); if (entry1 != null) { PositionReport pr1 = entry1.getValue(); PositionReport pr2; Map.Entry<Date, PositionReport> higherEntry = positionReports.higherEntry(atTime); if (higherEntry != null) { pr2 = higherEntry.getValue(); positionReport = new PositionReport( PositionTime.createInterpolated(pr1.getPositionTime(), pr2.getPositionTime(), atTime.getTime()), pr1.getCog(), pr1.getSog(), pr1.getHeading(), pr1.getNavigationalStatus(), true); } else { positionReport = new PositionReport( PositionTime.createExtrapolated(pr1.getPositionTime(), pr1.getCog(), pr1.getSog(), atTime.getTime()), pr1.getCog(), pr1.getSog(), pr1.getHeading(), pr1.getNavigationalStatus(), true); } } } return positionReport; } /** * Get or estimate a position report. If a real position report exists somewhere in the range * (atTime - deltaSeconds; atTime] then return this report. Otherwise return null. * * @param atTime the time to which to look for a position report. * @param deltaSeconds the maximum number of seconds to go back to find a matching position report. * @return a matching position report or null. */ PositionReport getPositionReportNear(Date atTime, int deltaSeconds) { Map.Entry<Date, PositionReport> positionReportEntry = positionReports.floorEntry(atTime); if (positionReportEntry != null) { if (positionReportEntry.getKey().getTime() < atTime.getTime() - deltaSeconds * 1000) { positionReportEntry = null; } } return positionReportEntry == null ? null : positionReportEntry.getValue(); } private void update(AisPacket p) { AisMessage message = p.tryGetAisMessage(); checkOrSetMmsi(message); if (message instanceof AisPositionMessage) { AisPositionMessage positionMessage = (AisPositionMessage) message; if (positionMessage.isPositionValid()) { final float lat = (float) positionMessage.getPos().getLatitudeDouble(); final float lon = (float) positionMessage.getPos().getLongitudeDouble(); final int hdg = positionMessage.getTrueHeading(); final float cog = positionMessage.getCog() / 10.0f; final float sog = positionMessage.getSog() / 10.0f; final int nav = positionMessage.getNavStatus(); final long timestamp = p.getBestTimestamp(); positionReports.put(new Date(timestamp), new PositionReport(timestamp, lat, lon, cog, sog, hdg, NavigationalStatus.get(nav), false)); } } else if (message instanceof AisMessage5) { AisMessage5 message5 = (AisMessage5) message; name = aisStringToJavaString(message5.getName()); shipTypeCargo = new ShipTypeCargo(message5.getShipType()); destination = aisStringToJavaString(message5.getDest()); imo = (int) message5.getImo(); toBow = message5.getDimBow(); toStern = message5.getDimStern(); toPort = message5.getDimPort(); toStarboard = message5.getDimStarboard(); } } private void checkOrSetMmsi(AisMessage message) { final int msgMmsi = message.getUserId(); if (mmsi < 0) { mmsi = msgMmsi; } else { if (mmsi != msgMmsi) { throw new IllegalArgumentException( "Message from mmsi " + msgMmsi + " cannot update target with mmsi " + mmsi); } } } private String name, destination; private int mmsi = -1, imo = -1, toBow = -1, toStern = -1, toPort = -1, toStarboard = -1; private ShipTypeCargo shipTypeCargo; private final Set<Object> tags = new HashSet<>(); private final TreeMap<Date, PositionReport> positionReports = new TreeMap<>(); @Immutable public final class PositionReport { private PositionReport(PositionTime pt, float cog, float sog, int heading, NavigationalStatus navstat, boolean estimated) { this.positionTime = pt; this.cog = cog; this.sog = sog; this.heading = heading; this.navstat = navstat; this.estimated = estimated; } private PositionReport(long timestamp, float latitude, float longitude, float cog, float sog, int heading, NavigationalStatus navstat, boolean estimated) { this.positionTime = PositionTime.create(latitude, longitude, timestamp); this.cog = cog; this.sog = sog; this.heading = heading; this.navstat = navstat; this.estimated = estimated; } public PositionTime getPositionTime() { return positionTime; } public long getTimestamp() { return positionTime.getTime(); } public double getLatitude() { return positionTime.getLatitude(); } public double getLongitude() { return positionTime.getLongitude(); } public float getCog() { return cog; } public float getSog() { return sog; } public int getHeading() { return heading; } public NavigationalStatus getNavigationalStatus() { return navstat; } public boolean isEstimated() { return estimated; } @Override public String toString() { final StringBuffer sb = new StringBuffer("PositionReport{"); sb.append("positionTime=").append(positionTime); sb.append(", cog=").append(cog); sb.append(", sog=").append(sog); sb.append(", heading=").append(heading); sb.append(", navstat=").append(navstat); sb.append(", shipTypeCargo=").append(shipTypeCargo); sb.append(", estimated=").append(estimated); sb.append('}'); return sb.toString(); } private final PositionTime positionTime; private final float cog; private final float sog; private final int heading; private final NavigationalStatus navstat; /** true of position is inter- or extrapolated. false if position is received from AIS */ private final boolean estimated; } } }