dk.dma.vessel.track.model.VesselTarget.java Source code

Java tutorial

Introduction

Here is the source code for dk.dma.vessel.track.model.VesselTarget.java

Source

/* 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.vessel.track.model;

import dk.dma.ais.data.AisTargetDimensions;
import dk.dma.ais.message.AisMessage;
import dk.dma.ais.message.AisMessage5;
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.message.NavigationalStatus;
import dk.dma.ais.packet.AisPacket;
import dk.dma.ais.packet.AisPacketTags;
import dk.dma.ais.packet.AisPacketTags.SourceType;
import dk.dma.enav.model.Country;
import dk.dma.enav.model.geometry.Position;
import org.apache.commons.lang.StringUtils;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;
import java.util.Objects;

/**
 * Vessel target entity
 */
@Entity
@SuppressWarnings("unused")
public class VesselTarget implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * The number of days (plus one, actually) to track max-speed for.
     */
    private static final int MAX_SPEED_DAYS = 30;

    /**
     * The minimum distance between two past track positions.
     */
    public static final int PAST_TRACK_MIN_DIST = 100; // Meters

    public enum State {
        NONE, NEW, UPDATED
    }

    @Transient
    State changed = State.NONE;

    @Id
    int mmsi;

    @NotNull
    AisTargetType targetType;

    @NotNull
    SourceType sourceType;

    @Column(length = 2)
    String country;

    @NotNull
    @Temporal(TemporalType.TIMESTAMP)
    Date lastReport;

    // **** Position Data - Instantiate with invalid data
    @Temporal(TemporalType.TIMESTAMP)
    Date lastPosReport;
    Float lat;
    Float lon;
    Float cog;
    Float sog;
    Short heading;
    Short rot;
    NavigationalStatus navStatus;

    // **** Static Data
    @Temporal(TemporalType.TIMESTAMP)
    Date lastStaticReport;
    Short length;
    Short width;
    String name;
    String callsign;
    Long imoNo;
    String destination;
    Float draught;
    Date eta;
    Integer vesselType;

    // *** Max-speed data
    @Column(columnDefinition = "BINARY(" + (MAX_SPEED_DAYS * 2) + ")")
    byte[] maxSpeed;

    // *** Past track reference
    @OneToOne(cascade = CascadeType.ALL)
    PastTrackPos lastPastTrackPos;

    @Transient
    PastTrackPos newPastTrackPos;

    /**
     * Constructor
     */
    public VesselTarget() {
    }

    /**
     * Constructor
     * Used when creating new vessel targets
     */
    public VesselTarget(int mmsi) {
        this.mmsi = mmsi;
        changed = State.NEW;
    }

    /**
     * Merges the information of the AIS packet wiht this vessel target
     * @param packet the AIS packet
     * @param message the AIS message
     */
    public synchronized void merge(AisPacket packet, AisMessage message) {

        Objects.requireNonNull(packet);
        Objects.requireNonNull(message);

        boolean updated = false;

        // Sanity check
        if (message.getUserId() != mmsi) {
            throw new IllegalArgumentException("Cannot merge with MMSI " + message.getUserId());
        }

        // Update target type
        if (message.getTargetType() != targetType) {
            targetType = message.getTargetType();
            updated = true;
        }

        // Update country
        Country c = Country.getCountryForMmsi(message.getUserId());
        if (c != null && StringUtils.isNotBlank(c.getTwoLetter()) && !c.getTwoLetter().equals(country)) {
            country = c.getTwoLetter();
            updated = true;
        }

        // Position Data
        if (message instanceof IVesselPositionMessage) {
            updated |= updateVesselPositionMessage((IVesselPositionMessage) message, packet.getTimestamp());
        }

        // Static Data
        if (message instanceof AisStaticCommon) {
            updated |= updateVesselStaticMessage((AisStaticCommon) message, packet.getTimestamp());
        }

        // Only update the lastReport time stamp if any fields have been updated
        if (updated) {
            lastReport = packet.getTimestamp();
            AisPacketTags tags = packet.getTags();
            sourceType = (tags.getSourceType() == null) ? SourceType.TERRESTRIAL : tags.getSourceType();
            if (changed != State.NEW) {
                changed = State.UPDATED;
            }
        }

    }

    /**
     * Update the positional fields from the AIS message
     * @param posMessage the AIS message
     * @param date the date of the message
     * @return if any fields were updated
     */
    private boolean updateVesselPositionMessage(IVesselPositionMessage posMessage, Date date) {

        // Check that this is a newer position update
        if (lastPosReport != null && lastPosReport.getTime() >= date.getTime()) {
            return false;
        }

        boolean updated = false;

        // Update sog
        Float sog = posMessage.getSog() / 10.0f;
        if (posMessage.isSogValid() && !compare(sog, this.sog)) {
            this.sog = sog;
            updateMaxSpeedToday((short) Math.round(sog));
            updated = true;
        }

        // Update cog
        Float cog = posMessage.getCog() / 10.0f;
        if (posMessage.isCogValid() && !compare(cog, this.cog)) {
            this.cog = cog;
            updated = true;
        }

        // Update heading
        Short heading = (short) posMessage.getTrueHeading();
        if (posMessage.isHeadingValid() && !compare(heading, this.heading)) {
            this.heading = heading;
            updated = true;
        }

        if (posMessage.isPositionValid()) {
            Position pos = posMessage.getPos().getGeoLocation();

            // Update latitude
            if (!compare(pos.getLatitude(), this.lat)) {
                this.lat = (float) pos.getLatitude();
                updated = true;
            }

            // Update longitude
            if (!compare(pos.getLongitude(), this.lon)) {
                this.lon = (float) pos.getLongitude();
                updated = true;
            }
        }

        if (posMessage instanceof AisPositionMessage) {
            AisPositionMessage classAposMessage = (AisPositionMessage) posMessage;

            // Update rot
            Short rot = (short) classAposMessage.getRot();
            if (classAposMessage.isRotValid() && !compare(rot, this.rot)) {
                this.rot = rot;
                updated = true;
            }

            // Update nav status
            NavigationalStatus navStatus = NavigationalStatus.get(classAposMessage.getNavStatus());
            if (navStatus != this.navStatus) {
                this.navStatus = navStatus;
                updated = true;
            }
        }

        // Only update lasPosReport if any positional field has been updated
        if (updated) {
            lastPosReport = date;
        }

        // Check if we need to update past track
        if (updated && checkValidPos()) {
            updatePastTrack();
        }

        return updated;
    }

    /**
     * Update the static information fields from the AIS message
     * @param message the AIS message
     * @param date the date of the message
     * @return if any fields were updated
     */
    private boolean updateVesselStaticMessage(AisStaticCommon message, Date date) {

        // Check that this is a newer static update
        if (lastStaticReport != null && lastStaticReport.getTime() >= date.getTime()) {
            return false;
        }

        boolean updated = false;

        // Update the name
        String name = AisMessage.trimText(message.getName());
        if (StringUtils.isNotBlank(name) && !name.equals(this.name)) {
            this.name = name;
            updated = true;
        }

        // Update the call-sign
        String callsign = AisMessage.trimText(message.getCallsign());
        if (StringUtils.isNotBlank(callsign) && !callsign.equals(this.callsign)) {
            this.callsign = callsign;
            updated = true;
        }

        // Update the vessel type
        Integer vesselType = message.getShipType();
        if (!vesselType.equals(this.vesselType)) {
            this.vesselType = vesselType;
            updated = true;
        }

        if (message instanceof AisMessage5) {
            AisMessage5 msg5 = (AisMessage5) message;
            AisTargetDimensions dim = new AisTargetDimensions(msg5);

            // Update length
            Short length = (short) (dim.getDimBow() + dim.getDimStern());
            if (!length.equals(this.length)) {
                this.length = length;
                updated = true;
            }

            // Update width
            Short width = (short) (dim.getDimPort() + dim.getDimStarboard());
            if (!width.equals(this.width)) {
                this.width = width;
                updated = true;
            }

            // Update destination
            String destination = StringUtils.defaultIfBlank(AisMessage.trimText(msg5.getDest()), null);
            if (destination != null && !destination.equals(this.destination)) {
                this.destination = destination;
                updated = true;
            }

            // Update draught
            Float draught = msg5.getDraught() / 10.0f;
            if (msg5.getDraught() > 0 && !compare(draught, this.draught)) {
                this.draught = draught;
                updated = true;
            }

            // Update ETA
            Date eta = msg5.getEtaDate();
            if (eta != null && !eta.equals(this.eta)) {
                this.eta = eta;
                updated = true;
            }

            // Update IMO
            Long imo = msg5.getImo();
            if (msg5.getImo() > 0 && !imo.equals(this.imoNo)) {
                this.imoNo = imo;
                updated = true;
            }
        }

        // Only update lastStaticReport if any static field has been updated
        if (updated) {
            lastStaticReport = date;
        }

        return updated;
    }

    /**
     * Check if we need to insert a new past track entity.
     * This happens if the distance to the last past track position
     * exceeds a threshold.
     * <p>
     * Please also note that there will be at most 1 new past track per minute
     * which is the time between the persistence process.
     */
    private boolean updatePastTrack() {
        if (lastPastTrackPos != null && newPastTrackPos == null
                && computePastTrackDist(lastPastTrackPos) > PAST_TRACK_MIN_DIST) {
            // The distance to the last registered past track position exceeds the threshold
            newPastTrackPos = new PastTrackPos(this);
            return true;

        } else if (lastPastTrackPos == null && newPastTrackPos == null) {
            // We need to keep track of the first registered position
            newPastTrackPos = new PastTrackPos(this);
            return true;
        }
        return false;
    }

    /**
     * Computes the distance between the current vessel position and the given past track position
     * @param pos the past track position
     * @return the distance between the current vessel position and the given past track position
     */
    public double computePastTrackDist(PastTrackPos pos) {
        return Position.create(lat, lon).rhumbLineDistanceTo(Position.create(pos.getLat(), pos.getLon()));
    }

    /**
     * Utility method that compares two numbers
     * @param f1 the first number
     * @param f2 the first number
     * @return if the numbers are (almost) identical
     */
    private boolean compare(Number f1, Number f2) {
        if (f1 == null && f2 == null) {
            return true;
        } else if (f1 == null || f2 == null) {
            return false;
        }
        return Math.abs(f1.doubleValue() - f2.doubleValue()) < 0.001;
    }

    public synchronized void flagChanged(State changed) {
        this.changed = changed;
    }

    /**
     * Returns the changed state of the entity
     * @return the changed state of the entity
     */
    public State changed() {
        return changed;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return mmsi;
    }

    /**
     * Checks if the target defines a valid position
     * @return if the target defines a valid position
     */
    public boolean checkValidPos() {
        return lat != null && lon != null && cog != null && sog != null && lastPosReport != null;
    }

    // ****** Max Speed functions ******* //

    /**
     * Reads the max-speed for the given day index
     * @param day the day index
     * @return the max-speed for the given day index
     */
    public short readMaxSpeed(long day) {
        if (maxSpeed == null) {
            return 0;
        }
        int index = (int) (day % MAX_SPEED_DAYS) * 2;
        return (short) ((maxSpeed[index] << 8) | maxSpeed[index + 1]);
    }

    /**
     * Writes the max-speed for the given day index
     * @param day the day index
     * @param speed the speed to write
     */
    public void writeMaxSpeed(long day, short speed) {
        if (speed == 0) {
            return;
        }
        if (maxSpeed == null) {
            maxSpeed = new byte[MAX_SPEED_DAYS * 2];
        }
        int index = (int) (day % MAX_SPEED_DAYS) * 2;
        maxSpeed[index] = (byte) ((speed >> 8) & 0xff);
        maxSpeed[index + 1] = (byte) (speed & 0xff);
    }

    /**
     * Updates the max-speed for today.
     * This should be used with care, as it involves two operations:
     * <ul>
     *     <li>Update the speed for today if it is larger that the existing value</li>
     *     <li>Reset the max-speed of tomorrow</li>
     * </ul>
     *
     * @param speed the speed to write
     */
    public void updateMaxSpeedToday(short speed) {
        long day = LocalDate.now().toEpochDay();
        short oldSpeed = readMaxSpeed(day);
        if (speed > oldSpeed) {
            writeMaxSpeed(day, speed);
        }
        writeMaxSpeed(day + 1, (short) 0);
    }

    /**
     * Computes the max speed over the recorded period
     * @return the max speed over the recorded period
     */
    public short computeMaxSpeed() {
        if (maxSpeed == null) {
            return 0;
        }

        int speed = 0;
        for (int index = 0; index < MAX_SPEED_DAYS; index++) {
            speed = Math.max(speed, (maxSpeed[index * 2] << 8) | maxSpeed[index * 2 + 1]);
        }
        return (short) speed;
    }

    @Override
    public String toString() {
        return "VesselTarget{" + "changed=" + changed + ", mmsi=" + mmsi + ", lastReport=" + lastReport + ", lat="
                + lat + ", lon=" + lon + ", lastPastTrackPos=" + lastPastTrackPos + ", newPastTrackPos="
                + newPastTrackPos + '}';
    }

    // ****** Getters and setters ******* //

    public int getMmsi() {
        return mmsi;
    }

    public AisTargetType getTargetType() {
        return targetType;
    }

    public SourceType getSourceType() {
        return sourceType;
    }

    public String getCountry() {
        return country;
    }

    public Date getLastReport() {
        return lastReport;
    }

    public Date getLastPosReport() {
        return lastPosReport;
    }

    public Float getLat() {
        return lat;
    }

    public Float getLon() {
        return lon;
    }

    public Float getCog() {
        return cog;
    }

    public Float getSog() {
        return sog;
    }

    public Short getHeading() {
        return heading;
    }

    public Short getRot() {
        return rot;
    }

    public NavigationalStatus getNavStatus() {
        return navStatus;
    }

    public Date getLastStaticReport() {
        return lastStaticReport;
    }

    public Short getLength() {
        return length;
    }

    public Short getWidth() {
        return width;
    }

    public String getName() {
        return name;
    }

    public String getCallsign() {
        return callsign;
    }

    public Long getImoNo() {
        return imoNo;
    }

    public String getDestination() {
        return destination;
    }

    public Float getDraught() {
        return draught;
    }

    public Date getEta() {
        return eta;
    }

    public Integer getVesselType() {
        return vesselType;
    }

    public byte[] getMaxSpeed() {
        return maxSpeed;
    }

    public PastTrackPos getLastPastTrackPos() {
        return lastPastTrackPos;
    }

    public void setLastPastTrackPos(PastTrackPos lastPastTrackPos) {
        this.lastPastTrackPos = lastPastTrackPos;
    }

    public PastTrackPos getNewPastTrackPos() {
        return newPastTrackPos;
    }

    public void setNewPastTrackPos(PastTrackPos newPastTrackPos) {
        this.newPastTrackPos = newPastTrackPos;
    }
}