/* 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.message.AisMessage;
import dk.dma.ais.message.AisTargetType;
import dk.dma.ais.packet.AisPacket;
import dk.dma.ais.packet.AisPacketSource;
import dk.dma.ais.tracker.Tracker;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
/**
* A simple tracker that keeps track of targets.
* <p>
* There are no automatically cleanup instead users must regularly cleanup targets by calling
* {@link #removeAll(Predicate)}
*
* @author Kasper Nielsen
* @author Jens Tuxen
*/
public class TargetTracker implements Tracker {
/** All targets that we are currently monitoring. */
final ConcurrentHashMap<Integer, MmsiTarget> targets = new ConcurrentHashMap<>();
/**
* Returns the number of targets that is being tracked. This is usually a lot faster than invoking
* <tt>stream(predicate).count()</tt>
*
* @return the number of targets that is being tracked
*/
public int count(Predicate<? super AisPacketSource> predicate) {
LongAdder la = new LongAdder();
targets.values().stream().forEach(t -> {
for (TargetInfo i : t.values()) {
if (predicate.test(i.getPacketSource())) {
la.increment();
return;
}
}
});
return la.intValue();
}
/**
* Returns the total number of reports for all targets. Each target might have multiple reports.
*
* @return the total number of reports for all targets
*/
public int countNumberOfReports() {
LongAdder la = new LongAdder();
targets.values().forEach(t -> {
la.add(t.size());
});
return la.intValue();
}
/**
* Returns the latest target info for the specified MMSI number.
*
* @param mmsi
* the MMSI number
* @return the latest target info for the specified MMSI number
*/
public TargetInfo get(int mmsi) {
return get(mmsi, e -> true);
}
public TargetInfo get(int mmsi, Predicate<? super AisPacketSource> sourcePredicate) {
MmsiTarget target = targets.get(mmsi);
return target == null ? null : target.getLatest(sourcePredicate);
}
/**
* Returns a set of all packet sources for a given MMSI number.
*
* @param mmsi
* the MMSI number
* @return a set of all packet sources for a given MMSI number
*/
public Set<AisPacketSource> getPacketSourcesForMMSI(int mmsi) {
MmsiTarget t = targets.get(mmsi);
return t == null ? Collections.emptySet() : new HashSet<>(t.keySet());
}
/**
* Removes all targets that are accepted by the specified predicate. Is typically used to remove targets based on
* time stamps.
*
* @param predicate
* the predicate that selects which items to remove
*/
public void removeAll(Predicate<? super TargetInfo> predicate) {
requireNonNull(predicate);
targets.values().stream().forEach(t -> {
for (TargetInfo i : t.values()) {
if (predicate.test(i)) {
t.remove(i.getPacketSource(), i);
}
}
// race with update mechanism is handled in #tryUpdate
if (t.isEmpty()) {
targets.remove(t.mmsi, t);
}
});
}
/**
* Removes all targets that are accepted by the specified predicate. Is
* typically used to remove targets based on time stamps.
*
* @param predicate
* the predicate that selects which items to remove
*/
public void removeAll(BiPredicate<? super AisPacketSource, ? super TargetInfo> predicate) {
requireNonNull(predicate);
targets.forEachValue(10, t -> {
for (Map.Entry<AisPacketSource, TargetInfo> e : t.entrySet()) {
if (predicate.test(e.getKey(), e.getValue())) {
t.remove(e.getKey(), e.getValue());
}
}
// if there are no more targets just remove it
// tryUpdate contains functionality to make sure we do not have
// any consistency issues.
if (t.isEmpty()) {
targets.remove(t.mmsi, t);
}
});
}
/**
* Returns the number of targets that is being tracked.
*
* @return the number of targets that is being tracked
* @see #count(Predicate)
*/
public int size() {
return targets.size();
}
/**
* Creates a parallel stream of all targets.
*
* @return a stream of targets
*/
public Stream<TargetInfo> stream() {
return stream(s->true, t->true);
}
/**
* Creates a parallel stream of targets with the specified source predicate.
*
* @param sourcePredicate the predicate on AIS packet source
* @return a stream of targets
*/
public Stream<TargetInfo> stream(Predicate<? super AisPacketSource> sourcePredicate) {
return stream(sourcePredicate, t->true);
}
/**
* Creates a sequential stream of targets with the specified source predicate and matching the given targetPredicate.
* @param sourcePredicate
* @param targetPredicate
* @return a stream of targets
*/
public Stream<TargetInfo> stream(Predicate<? super AisPacketSource> sourcePredicate, Predicate<? super TargetInfo> targetPredicate) {
requireNonNull(targetPredicate, "targetPredicate is null");
requireNonNull(sourcePredicate, "sourcePredicate is null");
return targets.values().parallelStream().map(t -> t.getLatest(sourcePredicate)).filter(e -> e != null).filter(targetPredicate);
}
/**
* Creates a sequential stream of all targets.
*
* @return a stream of targets
*/
public Stream<TargetInfo> streamSequential() {
return streamSequential(src -> true);
}
/**
* Creates a sequential stream of targets with the specified source predicate.
*
* @param sourcePredicate
* the predicate on AIS packet source
* @return a stream of targets
*/
public Stream<TargetInfo> streamSequential(Predicate<? super AisPacketSource> sourcePredicate) {
requireNonNull(sourcePredicate, "sourcePredicate is null");
return streamSequential(sourcePredicate, target -> true);
}
/**
* Creates a sequential stream of targets with the specified source predicate and matching the given targetPredicate.
* @param sourcePredicate
* @param targetPredicate
* @return a stream of targets
*/
public Stream<TargetInfo> streamSequential(Predicate<? super AisPacketSource> sourcePredicate, Predicate<? super TargetInfo> targetPredicate) {
requireNonNull(targetPredicate, "targetPredicate is null");
requireNonNull(sourcePredicate, "sourcePredicate is null");
return targets.values().stream().map(t -> t.getLatest(sourcePredicate)).filter(e -> e != null).filter(targetPredicate);
}
/**
* A little helper method that makes sure we do not get lost updates when updating a target. While the MMSI target
* is being cleaned.
*
* @param mmsi
* the MMSI number
* @param c
* a consumer that can use the MMSI target
*/
private void tryUpdate(int mmsi, Consumer<MmsiTarget> c) {
for (;;) {
// Lets first get the target. Or create a new target if it does not
// currently exist
MmsiTarget t = targets.computeIfAbsent(mmsi, i -> new MmsiTarget(mmsi));
c.accept(t);
// We can get some very rare races with the cleanup method. So we
// just need to check that the mmsi target we
// just updated/created is the same now
if (t == targets.get(mmsi)) {
return;
}
}
}
/**
* Updates the tracker with the specified packet
*
* @param packet
* the packet to update the trigger with
*/
public void update(AisPacket packet) {
AisMessage message = packet.tryGetAisMessage();
Date date = packet.getTimestamp();
// We only want to handle messages containing targets data
// #1-#3, #4, #5, #18, #21, #24 and a valid timestamp
if (message != null && date != null) {
// find the target type
AisTargetType targetType = message.getTargetType();
// only update if there is a target type
if (targetType != null) {
tryUpdate(
message.getUserId(),
t -> t.compute(
AisPacketSource.create(packet),
(source, existing) -> TargetInfo.updateTarget(existing, packet, targetType,
date.getTime(), source, t.msg24Part0)));
}
}
}
/**
* Used by the backup routine to restore data.
*
* @param packetSource
* the source
* @param targetInfo
* the target info
*/
void update(AisPacketSource packetSource, TargetInfo targetInfo) {
tryUpdate(targetInfo.getMmsi(),
t -> t.merge(packetSource, targetInfo, (ex, newOne) -> ex == null ? newOne : ex.merge(newOne)));
}
/**
* A single ship containing multiple reports for different combinations of sources.
*/
@SuppressWarnings("serial")
static class MmsiTarget extends ConcurrentHashMap<AisPacketSource, TargetInfo> {
/** The MMSI number */
final int mmsi;
/** A cache of AIS messages 24 part 0. */
final ConcurrentHashMap<AisPacketSource, byte[]> msg24Part0 = new ConcurrentHashMap<>();
MmsiTarget(int mmsi) {
this.mmsi = mmsi;
}
/**
* Returns the newest position and static data.
*
* @param predicate
* a predicate for filtering on the sources
* @return the newest position and static data
*/
TargetInfo getLatest(Predicate<? super AisPacketSource> predicate) {
// This method is fairly optimized to avoid creating excessive objects.
TargetInfo bestStatic = null;
TargetInfo bestPosition = null;
for (TargetInfo i : values()) {
if (predicate.test(i.getPacketSource())) {
if (i.hasStaticInfo()
&& (bestStatic == null || i.getStaticTimestamp() > bestStatic.getStaticTimestamp())) {
bestStatic = i;
}
if (i.hasPositionInfo()
&& (bestPosition == null || i.getPositionTimestamp() > bestPosition.getPositionTimestamp())) {
bestPosition = i;
}
}
}
if (bestStatic == null || bestStatic == bestPosition) {
return bestPosition;
} else if (bestPosition == null) {
return bestStatic;
} else { // we need to merge two different targets
return bestPosition.mergeWithStaticFrom(bestStatic);
}
}
}
}