/* 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.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import net.maritimecloud.mms.server.connection.client.Client; import net.maritimecloud.mms.server.connection.client.ClientManager; import net.maritimecloud.util.geometry.Area; import net.maritimecloud.util.geometry.Circle; import net.maritimecloud.util.geometry.PositionTime; import org.cakeframework.container.Container; import org.cakeframework.container.concurrent.Daemon; /** * An object that tracks positions. * * @author Kasper Nielsen */ public class PositionTracker { // Good description of why we use brute force // http://www.jandrewrogers.com/2015/03/02/geospatial-databases-are-hard/?h /** Magic constant. */ static final int THRESHOLD = 1; /** All targets at last update. Must be read via synchronized */ private ConcurrentHashMap<Client, PositionTime> latest = new ConcurrentHashMap<>(); /** All current subscriptions. */ final ConcurrentHashMap<PositionUpdatedHandler, Subscription> subscriptions = new ConcurrentHashMap<>(); private final ClientManager clientManager; public PositionTracker(ClientManager clientManager) { this.clientManager = requireNonNull(clientManager); } @Daemon public void run(Container c) { long start = System.currentTimeMillis(); while (!c.getState().isShutdown()) { try { doRun0(); long now = System.currentTimeMillis(); long sleep = 1000 - Math.min(1000, now - start); if (sleep > 0) { Thread.sleep(sleep); } start = now; } catch (Exception e) { e.printStackTrace(); } } } /** Should be scheduled to run every x second to update handlers. */ private void doRun0() { ConcurrentHashMap<Client, PositionTime> current = new ConcurrentHashMap<>(); final Map<Client, PositionTime> latest = this.latest; // We only want to process those that have been updated since last time final ConcurrentHashMap<Client, PositionTime> updates = new ConcurrentHashMap<>(); clientManager.forEach(pt -> { PositionTime p = latest.get(pt); PositionTime currentPt = pt.getLatestPositionAndTime(); if (p == null || !p.positionEquals(currentPt)) { updates.put(pt, currentPt); } current.put(pt, currentPt); }); // update each subscription with new positions subscriptions.forEachValue(THRESHOLD, s -> s.updateWith(updates)); // update latest with current latest this.latest = current; } /** * Invokes the callback for every tracked object within the specified area of interest. * * @param shape * the area of interest * @param block * the callback */ public void forEachWithinArea(Area shape, BiConsumer<Client, PositionTime> block) { requireNonNull(shape, "shape is null"); requireNonNull(block, "block is null"); clientManager.forEach(c -> { PositionTime pt = c.getLatestPositionAndTime(); if (shape.contains(pt)) { block.accept(c, pt); } }); } /** * Returns the number of subscriptions. * * @return the number of subscriptions */ public int getNumberOfSubscriptions() { return subscriptions.size(); } /** * Returns a map of all tracked objects and their latest position. * * @param shape * the area of interest * @return a map of all tracked objects within the area as keys and their latest position as the value */ public Map<Client, PositionTime> getTargetsWithin(Area shape) { final ConcurrentHashMap<Client, PositionTime> result = new ConcurrentHashMap<>(); forEachWithinArea(shape, (a, b) -> { if (shape.contains(b)) { result.put(a, b); } }); return result; } public boolean remove(Client t) { return latest.remove(t) != null; } /** * Subscribes to changes in the specified area. * * @param area * the area to monitor * @param handler * a subscription that can be used to cancel the subscription * @return a subscription object */ public Subscription subscribe(Area area, PositionUpdatedHandler handler) { return subscribe(area, handler, 100); } /** * Subscribes to changes in the specified area. * * @param area * the area to monitor * @param handler * a subscription that can be used to cancel the subscription * @param slack * is the precision in meters with which we want to report entering/exiting messages. We use it to avoid * situations where a boat sails on a boundary line and keeps changing from being inside to outside of it * @return a subscription object */ public Subscription subscribe(Area area, PositionUpdatedHandler handler, double slack) { Area exitShape = requireNonNull(area, "area is null"); if (slack < 0) { throw new IllegalArgumentException("Slack must be non-negative, was " + slack); } if (slack > 0) { if (area instanceof Circle) { Circle c = (Circle) area; exitShape = c.withRadius(c.getRadius() + slack); } else { throw new UnsupportedOperationException("Only circles allowed for now"); } } Subscription s = new Subscription(this, handler, area, exitShape); if (subscriptions.putIfAbsent(handler, s) != null) { throw new IllegalArgumentException("The specified handler has already been registered"); } return s; } } // // /** // * Returns the latest position time updated for the specified target. Or <code>null</code> if no position has ever // * been recorded for the target. // * // * // * @param target // * the target // * @return the latest position time updated // */ // public PositionTime getLatest(Client target) { // return latest.get(target); // } // // public PositionTime getLatestIfLaterThan(Client target, long time) { // PositionTime t = getLatest(target); // return t != null && time < t.getTime() ? t : null; // }