/* 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 net.maritimecloud.mms.server.tracker; import static java.util.Objects.requireNonNull; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import net.maritimecloud.mms.server.connection.client.Client; import net.maritimecloud.util.geometry.Area; import net.maritimecloud.util.geometry.Position; import net.maritimecloud.util.geometry.PositionTime; /** * A subscription is created for each {@link PositionUpdatedHandler}. It is to use unsubscribe ({@link #cancel()}). * * @author Kasper Nielsen */ public class Subscription { /** The handler that should be called whenever objects are entering/exiting. */ private final PositionUpdatedHandler handler; /** The shape we look at to see if we are entering the area of interest. */ private final Area shapeEntering; /** The shape we look at to see if we are exiting the area of interest. */ private final Area shapeExiting; /** A map of currently tracked objects for this subscription. */ private final ConcurrentHashMap<Client, PositionTime> trackedObjects = new ConcurrentHashMap<>(); /** The tracker that this subscription is registered with. */ private final PositionTracker tracker; Subscription(PositionTracker tracker, PositionUpdatedHandler handler, Area shape, Area exitShape) { this.tracker = requireNonNull(tracker); this.shapeEntering = requireNonNull(shape); this.shapeExiting = requireNonNull(exitShape); this.handler = requireNonNull(handler); } /** Cancels the subscription and free up any resources. */ public synchronized void cancel() { if (tracker.subscriptions.remove(handler, this)) { trackedObjects.clear(); } } /** * Performs the given consumer for each tracked object in parallel. * * @param consumer * the consumer */ public void forEachTrackedObject(Consumer<Client> consumer) { requireNonNull(consumer, "consumer is null"); trackedObjects.forEachKey(PositionTracker.THRESHOLD, t -> consumer.accept(t)); } /** * Executes the given consumer for each tracked object in parallel. * * @param consumer * the consumer */ public void forEachTrackedObject(BiConsumer<Client, Position> consumer) { requireNonNull(consumer, "consumer is null"); trackedObjects.forEach(PositionTracker.THRESHOLD, (t, pt) -> { // pt.time is from the first time we encountered the position. // We might have gotten messages with the position but different timestamps // To avoid confusion we do not export the timestamp out consumer.accept(t, Position.create(pt.getLatitude(), pt.getLongitude())); }); } /** * Returns the number of tracked objects. * * @return the number of tracked objects */ public int getNumberOfTrackedObjects() { return trackedObjects.size(); } /** * Returns a map of tracked objects with their current position. * * @return a map of tracked objects with their current position */ public Map<Client, Position> getTrackedObjects() { // We could return trackedObjects directly. But we do not want to return PositionTime objects // because the time is not the time from the latest report. But the first time with the current position. // Which would be easily to mistake for users. HashMap<Client, Position> result = new HashMap<>(); for (Map.Entry<Client, PositionTime> e : trackedObjects.entrySet()) { result.put(e.getKey(), Position.create(e.getValue().getLatitude(), e.getValue().getLongitude())); } return result; } /** * Called regular by the position tracked with updated positions. If any of updated objects are within the area of * interest. This class must notify the installed handler. * * @param updates * the position that have been updated since this method was last invoked */ synchronized void updateWith(ConcurrentHashMap<Client, PositionTime> updates) { for (Map.Entry<Client, PositionTime> e : updates.entrySet()) { Client t = e.getKey(); PositionTime pt = e.getValue(); PositionTime current = trackedObjects.get(t); boolean positionChanged = current == null || pt == null || !current.positionEquals(pt); if (current == null) {// not tracked if (shapeEntering.contains(pt)) { trackedObjects.put(t, pt); handler.entering(t, pt, PositionUpdatedHandler.EnterReason.ENTERED_AREA); } } else if (!shapeExiting.contains(pt)) { handler.exiting(t, PositionUpdatedHandler.LeaveReason.LEFT_AREA); trackedObjects.remove(t); } else { if (positionChanged) { handler.updated(t, current, pt); } trackedObjects.put(t, pt); } } } }