/* 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.targetTracker; import dk.dma.ais.data.AisTarget; import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisMessage24; import dk.dma.ais.message.AisPositionMessage; import dk.dma.ais.message.AisStaticCommon; import dk.dma.ais.message.AisTargetType; import dk.dma.ais.message.IVesselPositionMessage; import dk.dma.ais.packet.AisPacket; import dk.dma.ais.packet.AisPacketSource; import dk.dma.ais.tracker.Target; import dk.dma.enav.model.Country; import dk.dma.enav.model.geometry.Position; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import static java.util.Objects.requireNonNull; /** * Immutable information about a target. Whenever we receive a new message from the target. We create a new TargetInfo * instance via {@link #updateTarget(TargetInfo, AisPacket, AisTargetType, long, AisPacketSource, Map)}. * * @author Kasper Nielsen */ public class TargetInfo extends Target { /** serialVersionUID. */ private static final long serialVersionUID = 2L; /** The target type of the info, is never null. */ final AisTargetType targetType; /** The latest position and time stamp that was received Packet that was received. */ final long positionTimestamp; final byte[] positionPacket; final Position position; final float cog; final int heading; final byte navStatus; final float sog; //Do not want serialization of mutable complex object transient volatile AisTarget aisTarget; //further caching transient volatile AisPacket positionAisPacket; transient volatile AisPacket staticAisPacket1; transient volatile AisPacket staticAisPacket2; // The latest static info final long staticTimestamp; final byte[] staticData1; final byte[] staticData2; final int staticShipType; final AisPacketSource packetSource; /** * @return the packetSource */ public AisPacketSource getPacketSource() { return packetSource; } private TargetInfo(AisPacketSource packetSource, int mmsi, AisTargetType targetType, long positionTimestamp, Position p, int heading, float cog, float sog, byte navStatus, byte[] positionPacket, long staticTimestamp, byte[] staticData1, byte[] staticData2, int staticShipType) { super(mmsi); this.packetSource=packetSource; this.targetType = requireNonNull(targetType); // Position data this.positionTimestamp = positionTimestamp; this.position = p; this.heading = heading; this.cog = cog; this.sog = sog; this.navStatus = navStatus; this.positionPacket = positionPacket; // Static Data this.staticTimestamp = staticTimestamp; this.staticData1 = staticData1; this.staticData2 = staticData2; this.staticShipType = staticShipType; // Caching for AisPackets //this.staticAisPacket1 = getStaticAisPacket1(); //this.staticAisPacket2 = getStaticAisPacket2(); //this.positionAisPacket = getPositionPacket(); // Caching for getAisTarget() //this.aisTarget = getAisTarget(); } /** * Returns the target type of the target. * @return */ public AisTargetType getTargetType() { return targetType; } public long getPositionTimestamp() { return positionTimestamp; } public Position getPosition() { return position; } public float getCog() { return cog; } public int getHeading() { return heading; } public byte getNavStatus() { return navStatus; } public float getSog() { return sog; } public long getStaticTimestamp() { return staticTimestamp; } public int getStaticShipType() { return staticShipType; } public AisTarget getAisTarget() { AisTarget t = aisTarget; return t == null ?aisTarget = TargetInfoToAisTarget.generateAisTarget(this) : t; } /** * Returns the country of the vessel based on its MMSI number. * * @return the country of the vessel based on its MMSI number */ public Country getCountry() { return Country.getCountryForMmsi(getMmsi()); } /** * Returns the latest received position packet. Or <code>null</code> if no position has been received from the * vessel. * * @return the latest received position packet */ public AisPacket getPositionPacket() { byte[] positionPacket = this.positionPacket; if (positionPacket != null) { AisPacket positionAisPacket = this.positionAisPacket; return positionAisPacket == null ? positionAisPacket = AisPacket.fromByteArray(positionPacket): positionAisPacket; } return null; } /** * Returns the number of static packets we have received. Either return 0 if we have received 0 packets. 1 if we * have received a single packet. Or 2 we if have received two packets (AisMessage type 24). * * @return the number of static packets we have received */ public int getStaticCount() { return staticData1 == null ? 0 : staticData2 == null ? 1 : 2; } /** * Returns any static packets we have received from the target. */ public AisPacket[] getStaticPackets() { if (staticData1 == null) { return new AisPacket[0]; } else if (staticData2 == null) { return new AisPacket[] { getStaticAisPacket1() }; } else { return new AisPacket[] { getStaticAisPacket1(), getStaticAisPacket2() }; } } public AisPacket[] getPackets() { if (!hasPositionInfo()) { return getStaticPackets(); } Collection<AisPacket> packets = new ArrayList<>(); for (AisPacket p: getStaticPackets()) { packets.add(p); } packets.add(getPositionPacket()); return packets.toArray(new AisPacket[packets.size()]); } public AisPacket getStaticAisPacket1() { byte[] staticData1 = this.staticData1; if (staticData1 != null) { AisPacket staticAisPacket1 = this.staticAisPacket1; return staticAisPacket1 == null ? staticAisPacket1 = AisPacket.fromByteArray(staticData1): staticAisPacket1; } return null; } public AisPacket getStaticAisPacket2() { byte[] staticData2 = this.staticData2; if (staticData2 != null) { AisPacket staticAisPacket2 = this.staticAisPacket2; return staticAisPacket2 == null ? staticAisPacket2 = AisPacket.fromByteArray(staticData2): staticAisPacket2; } return null; } /** * Returns true if we have positional information. * * @return true if we have positional information */ public boolean hasPositionInfo() { return positionPacket != null; } /** * Returns true if we have static information. * * @return true if we have static information */ public boolean hasStaticInfo() { return staticData1 != null; } TargetInfo merge(TargetInfo other) { if (positionTimestamp >= other.positionTimestamp && staticTimestamp >= other.staticTimestamp) { return this; } else if (other.positionTimestamp >= positionTimestamp && other.staticTimestamp >= staticTimestamp) { return other; } return positionTimestamp >= other.positionTimestamp ? mergeWithStaticFrom(other) : other .mergeWithStaticFrom(this); } /** * Copies the static information from the specified target into a new target. Maintaining the position information * of the current target. * * @param other * @return the new target */ TargetInfo mergeWithStaticFrom(TargetInfo other) { //assert packetSource == other.packetSource return new TargetInfo(packetSource, getMmsi(), targetType, positionTimestamp, position, heading, cog, sog, navStatus, positionPacket, other.staticTimestamp, other.staticData1, other.staticData2, other.staticShipType); } /** * We have received a new AIS message from a target. * * @param existing * non-null if an existing target with the same MMSI number already exists * @param packet * the packet that was received * @param targetType * the type of target * @param timestamp * the timestamp of the packet * @param source * the source of the packet * @param msg24Part0 * a map of cached message type 24 static part 0 messages * @return a new target info */ static TargetInfo updateTarget(TargetInfo existing, AisPacket packet, AisTargetType targetType, long timestamp, AisPacketSource source, Map<AisPacketSource, byte[]> msg24Part0) { AisMessage message = packet.tryGetAisMessage();// is non-null int mmsi = message.getUserId(); // ATON and BS targets are easy to handle because they do not contain much other than a position if (targetType == AisTargetType.ATON || targetType == AisTargetType.BS) { // make sure it has a never timestamp if (existing != null && timestamp <= existing.positionTimestamp) { return existing; } return new TargetInfo(source, mmsi, targetType, timestamp, message.getValidPosition(), -1, -1, -1, (byte) -1, packet.toByteArray(), -1, null, null, -1); } TargetInfo result = updateTargetWithPosition(existing, packet, message, mmsi, targetType, timestamp, source); return updateTargetWithStatic(packet, message, mmsi, targetType, timestamp, source, result, msg24Part0); } static TargetInfo updateTargetWithPosition(TargetInfo existing, AisPacket packet, AisMessage message, int mmsi, AisTargetType targetType, long timestamp, AisPacketSource source) { if (message instanceof IVesselPositionMessage) { // only update if never timestamp if (existing == null || timestamp >= existing.positionTimestamp) { // check if another targetType in which case we drop the existing one // we need to have this check, after having checked the timestamp if (existing != null && existing.targetType != targetType) { existing = null; } IVesselPositionMessage ivm = (IVesselPositionMessage) message; Position p = message.getValidPosition(); int heading = ivm.getTrueHeading(); float cog = ivm.getCog(); float sog = ivm.getSog(); byte navStatus = message instanceof AisPositionMessage ? (byte) ((AisPositionMessage) message) .getNavStatus() : (byte) -1; if (existing == null) { return new TargetInfo(source, mmsi, targetType, timestamp, p, heading, cog, sog, navStatus, packet.toByteArray(), -1, null, null, -1); } else { return new TargetInfo(source, mmsi, targetType, timestamp, p, heading, cog, sog, navStatus, packet.toByteArray(), existing.staticTimestamp, existing.staticData1, existing.staticData2, existing.staticShipType); } } } return existing; } static TargetInfo updateTargetWithStatic(AisPacket packet, AisMessage message, int mmsi, AisTargetType targetType, long timestamp, AisPacketSource source, TargetInfo existing, Map<AisPacketSource, byte[]> msg24Part0) { if (message instanceof AisStaticCommon) { // only update if never timestamp if (existing == null || timestamp >= existing.staticTimestamp) { // check if another targetType in which case we drop the existing one // we need to have this check, after having checked the timestamp if (existing != null && existing.targetType != targetType) { existing = null; } AisStaticCommon c = (AisStaticCommon) message; byte[] static0; byte[] static1 = null; if (c instanceof AisMessage24) { // AisMessage24 is split into 2 parts, if we get a part 0. // Save in a hash table, where we keep it until we receive part 1 if (((AisMessage24) c).getPartNumber() == 0) { msg24Part0.put(source, packet.toByteArray()); // we know that existing have not been updated by updateTargetWithPosition because // AisMessage24 only contains static information, so existing=original return existing; // the target is updated when we receive part 1 } else { static0 = msg24Part0.remove(source); if (static0 == null) { return existing;// We do not have the first part:( } static1 = packet.toByteArray(); } } else { static0 = packet.toByteArray(); } if (existing == null) { return new TargetInfo(source, mmsi, targetType, -1, null, -1, -1, -1, (byte) -1, null, timestamp, static0, static1, c.getShipType()); } else { return new TargetInfo(source, mmsi, existing.targetType, existing.positionTimestamp, existing.position, existing.heading, existing.cog, existing.sog, existing.navStatus, existing.positionPacket, timestamp, static0, static1, c.getShipType()); } } } return existing; } }