track = getTrackedRace().getTrack(mark);
+ track.lockForRead();
+ try {
+ number += Util.size(track.getRawFixes());
+ } finally {
+ track.unlockAfterRead();
+ }
+ }
+ return number;
+ }
+
}
\ No newline at end of file
diff --git a/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs
index 5912fe30df5..c111da40d73 100755
--- a/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs
+++ b/java/com.sap.sailing.domain.common/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
eclipse.preferences.version=1
encoding//src/com/sap/sailing/domain/common/AbstractBearing.java=UTF-8
+encoding//src/com/sap/sailing/domain/common/confidence/impl/ConfidenceBasedAveragerImpl.java=utf-8
encoding//src/com/sap/sailing/domain/common/impl/AbstractSpeedWithAbstractBearingImpl.java=UTF-8
encoding//src/com/sap/sailing/domain/common/tracking/impl/CompactGPSFixMovingImpl.java=UTF-8
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Bearing.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Bearing.java
index 4508b693b6b..82e8e5660ec 100755
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Bearing.java
+++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/Bearing.java
@@ -2,7 +2,13 @@ package com.sap.sailing.domain.common;
import java.io.Serializable;
+import com.sap.sailing.domain.common.impl.DegreeBearingImpl;
+
public interface Bearing extends Serializable {
+ Bearing NORTH = new DegreeBearingImpl(0);
+ Bearing EAST = new DegreeBearingImpl(90);
+ Bearing SOUTH = new DegreeBearingImpl(180);
+ Bearing WEST = new DegreeBearingImpl(270);
double getDegrees();
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/RaceCompetitorIdsAsStringWithMD5Hash.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/RaceCompetitorIdsAsStringWithMD5Hash.java
new file mode 100644
index 00000000000..d4073411507
--- /dev/null
+++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/RaceCompetitorIdsAsStringWithMD5Hash.java
@@ -0,0 +1,105 @@
+package com.sap.sailing.domain.common;
+
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import com.sap.sse.common.Util;
+
+/**
+ * Manages the set of competitors participating in a specific race. This is not trivial for a {@link RaceBoardPanel}
+ * because it is provided with the {@link CompetitorSelectionProvider} that is also used by the {@link LeaderboardPanel}
+ * and therefore has all competitors of the entire regatta, not just those of the race that the race board is showing.
+ *
+ *
+ * The back-end knows the race-specific set of competitors and can tell the client. However, we'd like to keep the
+ * network traffic required to keep this information up to date at a minimum. The key idea is to use an MD5 hash across
+ * the ordered race's competitor IDs and send that with each {@link GetRaceMapDataAction} request. The server can then
+ * compare this hash with the current race's competitor set and send an update if and only if there is a difference
+ * detected. This will, for the most part, reduce the communication to an initial request by the client as long as the
+ * race's competitors are not known, and the additional MD5 hash being sent with each request.
+ *
+ *
+ * Objects of this type start out with an "unknown" race competitor set. When the
+ * {@link #getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID() MD5 hash} for the currently
+ * known race competitors is asked, null will initially be returned. When the client has received the race
+ * competitors the client is expected to call {@link #setIdsAsStringsOfCompetitorsInRace(Iterable)}. If the argument to
+ * this call was not null,
+ * {@link #getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID()} will from then on return
+ * the MD5 hash of the competitor IDs in order.
+ *
+ * @author Axel Uhl (D043530)
+ *
+ */
+public class RaceCompetitorIdsAsStringWithMD5Hash implements Serializable {
+ private static final long serialVersionUID = 6881670366184539259L;
+
+ /**
+ * The {@link #competitorSelection} manages the competitors for the entire leaderboard which may be a
+ * superset of the competitors participating in the race shown by this map. For certain operations,
+ * however, it is useful to know exactly the competitors participating in the race shown by this map.
+ * For example, boat positions should only be requested from the server for those competitors, not all
+ * in the regatta, particularly when racing in split fleets.
+ *
+ * To keep this set consistent, the client sends a secure hash of this set to the server where the hash
+ * will be compared with the hash across the set of competitor IDs as string.
+ *
+ * If this field is null, nothing is known yet about the competitor-to-race assignment.
+ */
+ private Set idsAsStringOfCompetitorsParticipatingInRace;
+
+ /**
+ * The MD5 hash of the {@link #idsAsStringOfCompetitorsParticipatingInRace} where the competitors are first ordered
+ * alphanumerically by their ID as String using {@link String#compareTo(String)}, then concatenating these strings
+ * and converting to a byte[] using a UTF-8 encoding. May be null if it hasn't been computed before
+ * or if {@link #idsAsStringOfCompetitorsParticipatingInRace} is still null.
+ */
+ private byte[] md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID;
+
+ /**
+ * Starts out with an unknown set of race competitors. The {@link #getIdsOfCompetitorsParticipatingInRaceAsStrings()}
+ * and the {@link #getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID()} will return
+ * null until a call to {@link #setIdsAsStringsOfCompetitorsInRace(Iterable)} with a non-
+ * null argument has been performed.
+ */
+ public RaceCompetitorIdsAsStringWithMD5Hash() {
+ }
+
+ /**
+ * Immediately calls {@link #setIdsAsStringsOfCompetitorsInRace(Set)} with the argument provided. If that
+ * was null then this has the same effect as calling {@link #RaceCompetitorIdsAsStringWithMD5Hash()}.
+ */
+ public RaceCompetitorIdsAsStringWithMD5Hash(Set idsAsStringOfCompetitorsParticipatingInRace)
+ throws UnsupportedEncodingException, NoSuchAlgorithmException {
+ this.setIdsAsStringsOfCompetitorsInRace(idsAsStringOfCompetitorsParticipatingInRace);
+ }
+
+ public void setIdsAsStringsOfCompetitorsInRace(Set idsAsStringsOfCompetitorsInRace) throws UnsupportedEncodingException, NoSuchAlgorithmException {
+ this.idsAsStringOfCompetitorsParticipatingInRace = idsAsStringsOfCompetitorsInRace;
+ md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID = computeMD5(idsAsStringsOfCompetitorsInRace);
+ }
+
+ public byte[] getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID() {
+ return md5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID;
+ }
+
+ public Set getIdsOfCompetitorsParticipatingInRaceAsStrings() {
+ return idsAsStringOfCompetitorsParticipatingInRace;
+ }
+
+ private byte[] computeMD5(Iterable idsAsStringsOfCompetitorsInRace) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ List l = new ArrayList<>();
+ Util.addAll(idsAsStringsOfCompetitorsInRace, l);
+ Collections.sort(l);
+ final MessageDigest md5 = MessageDigest.getInstance("MD5");
+ for (String c : l) {
+ md5.update(c.getBytes("UTF-8"));
+ }
+ return md5.digest();
+ }
+}
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/QuadTree.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/QuadTree.java
index 51f42e5f100..798f3ffbe55 100755
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/QuadTree.java
+++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/QuadTree.java
@@ -1,164 +1,90 @@
-// **********************************************************************
-//
-//
-//
-// BBN Technologies
-// 10 Moulton Street
-// Cambridge, MA 02138
-// (617) 873-8000
-//
-// Copyright (C) BBNT Solutions LLC. All rights reserved.
-//
-//
-// **********************************************************************
-//
-// $Source$
-// $RCSfile$
-// $Revision: 184 $
-// $Date: 2008-09-16 16:17:21 +0200 (Tue, 16 Sep 2008) $
-// $Author: axel.uhl $
-//
-// **********************************************************************
-
package com.sap.sailing.domain.common.quadtree;
import java.io.Serializable;
-import java.util.Collection;
-import java.util.Vector;
+import com.sap.sailing.domain.common.Bounds;
import com.sap.sailing.domain.common.Position;
+import com.sap.sailing.domain.common.impl.BoundsImpl;
import com.sap.sailing.domain.common.impl.DegreePosition;
-import com.sap.sailing.domain.common.quadtree.impl.Bounds;
-import com.sap.sailing.domain.common.quadtree.impl.QuadTreeNode;
+import com.sap.sailing.domain.common.quadtree.impl.Node;
+import com.sap.sailing.domain.common.quadtree.impl.Node.GetResult;
/**
- * The QuadTree lets you organize objects in a grid, that redefines itself and focuses more gridding when more objects
- * appear in a certain area.
- *
- *
- * Note that this class is not thread safe. If multiple threads can access the same instance concurrently, callers have
- * to ensure proper synchronization. Concurrent reads are permissible while any write should block all other operations.
+ * A spatial data structure that provides efficient (O(log n)) access to nearest neighbors and
+ * to objects in a certain radius around another object. The location of objects is provided as
+ * {@link Position}.
*
* @param
* type of object stored by coordinates
* @author Axel Uhl (D043530)
*/
public class QuadTree implements Serializable {
+ private static final long serialVersionUID = -5500716775749946017L;
- static final long serialVersionUID = -7707825592455579873L;
-
- private QuadTreeNode top;
+ private final Node root;
+
+ private final static int DEFAULT_MAX_NODE_ITEMS = 20;
public QuadTree() {
- this(new Bounds(new DegreePosition(-90.0, -180.0), new DegreePosition(90.0, 180.0)), 20, QuadTreeNode.NO_MIN_SIZE);
+ this(new BoundsImpl(new DegreePosition(-90.0, -180.0), new DegreePosition(90.0, 180.0)), DEFAULT_MAX_NODE_ITEMS);
}
public QuadTree(Position southWest, Position northEast, int maxItems) {
- this(new Bounds(southWest, northEast), maxItems, QuadTreeNode.NO_MIN_SIZE);
+ this(new BoundsImpl(southWest, northEast), maxItems);
}
- public QuadTree(Bounds bounds, int maxItems, double minSize) {
- top = new QuadTreeNode(bounds, maxItems, minSize);
+ private QuadTree(Bounds bounds, int maxItems) {
+ root = new Node(bounds, maxItems);
}
- /**
- * Add a object into the tree at a location.
- *
- * @param lat up-down location in QuadTree Grid (latitude, y)
- * @param lon left-right location in QuadTree Grid (longitude, x)
- * @return true if the insertion worked.
- * @throws RuntimeException in case the leaf's lat/lng lies outside of the node's bounds.
- * This would typically be caused by the point being outside the whole quad tree's bounds.
- */
- public void put(Position point, T obj) {
- getTop().put(point, obj);
+ public T put(Position point, T obj) {
+ return root.put(point, obj);
}
- /**
- * Remove a object out of the tree at a location.
- *
- * @param lat up-down location in QuadTree Grid (latitude, y)
- * @param lon left-right location in QuadTree Grid (longitude, x)
- * @return the object removed, null if the object not found.
- */
public T remove(Position point, T obj) {
- return getTop().remove(point, obj);
+ return root.remove(point);
}
- public void replace(Position point, T newObj) {
- getTop().replace(point, newObj);
- }
-
- /** Clear the tree. */
+ /**
+ * Remove all elements from this tree.
+ */
public void clear() {
- getTop().clear();
+ root.clear();
}
/**
- * Get an object closest to a lat/lon.
- *
- * @param lat up-down location in QuadTree Grid (latitude, y)
- * @param lon left-right location in QuadTree Grid (longitude, x)
- * @return the object that was found.
+ * Get the value nearest to point. If the tree is empty, null is returned. Distance
+ * is calculated using the method {@link #getLatLngDistance(Position, Position)} which is an approximation only,
+ * based on Euklidian geometry with the latitude/longitude values.
*/
public T get(Position point) {
- return getTop().get(point);
+ final GetResult result = root.get(point);
+ return result == null ? null : result.getValue();
}
/**
- * Get an object closest to a lat/lon, within a maximum distance.
+ * Get the value closest to point, within a maximum distance, where distance
+ * is computed by the rules of {@link #getLatLngDistance(Position, Position)}. If no key
+ * is found within that distance, null is returned.
*
- * @param lat up-down location in QuadTree Grid (latitude, y)
- * @param lon left-right location in QuadTree Grid (longitude, x)
- * @param withinDistance maximum get distance. The distance is given
- * as the square root of the sum of
- * the squares of the latitude and longitude differences, respectively.
- * It therefore does not correspond to any distance in meters or
- * any euclidian distance at all. However, it should be good enough
- * (at least outside the polar regions, and in particular for smaller
- * regions), and in particular to find minimum distances.
- * @return the object that was found, null if nothing is within
- * the maximum distance.
+ * @param withinDistance
+ * maximum get distance. The distance is given as the square root of the sum of the squares of the
+ * latitude and longitude differences, respectively. It therefore does not correspond to any distance in
+ * meters or any euclidian distance at all. However, it should be good enough (at least outside the polar
+ * regions, and in particular for smaller regions), and in particular to find minimum distances.
+ * See {@link #getLatLngDistance(Position, Position)}.
+ * @return the object that was found, null if nothing is within the maximum distance.
*/
public T get(Position point, double withinDistance) {
- return getTop().get(point, withinDistance);
+ final GetResult result = root.get(point, withinDistance);
+ return result == null ? null : result.getValue();
}
/**
- * Get all the objects within a bounding box.
- *
- * @return Vector of objects.
+ * Get all values withing the bounds
*/
- public Collection get(Bounds rect) {
- return get(rect, new Vector());
- }
-
- /**
- * Get all the objects within a bounding box, and return the
- * objects within a given Vector.
- *
- * @param vector a vector to add objects to.
- * @return Vector of objects.
- */
- private Collection get(Bounds rect, Collection vector) {
-
- if (vector == null) {
- vector = new Vector();
- }
- // crossing the dateline, right?? Or at least containing the
- // entire earth. Might be trouble for VERY LARGE scales. The
- // last check is for micro-errors that happen to lon points
- // where there might be a smudge overlap for very small
- // scales.
- if (rect.getSouthWest().getLngDeg() > rect.getNorthEast().getLngDeg() || (Math.abs(rect.getSouthWest().getLngDeg() - rect.getNorthEast().getLngDeg()) < .001)) {
- return getTop().get(new Bounds(rect.getSouthWest(), new DegreePosition(rect.getNorthEast().getLatDeg(), 180)),
- getTop().get(new Bounds(new DegreePosition(rect.getSouthWest().getLatDeg(), -180), rect.getNorthEast()), vector));
- } else
- return getTop().get(rect, vector);
- }
-
- protected QuadTreeNode getTop() {
- return top;
+ public Iterable get(Bounds bounds) {
+ return root.get(bounds);
}
/**
@@ -166,8 +92,10 @@ public class QuadTree implements Serializable {
* and doing the "sqrt thing"
*/
public static double getLatLngDistance(Position a, Position b) {
- double distance = Math.sqrt((a.getLatDeg() - b.getLatDeg()) * (a.getLatDeg() - b.getLatDeg()) + (a.getLngDeg() - b.getLngDeg())
- * (a.getLngDeg() - b.getLngDeg()));
+ final double dy = a.getLatDeg() - b.getLatDeg();
+ final double dx = (a.getLngDeg() - b.getLngDeg());
+ final double dxWrapped = Math.abs(dx) <= 180 ? dx : 360.-Math.abs(dx);
+ double distance = Math.sqrt(dy * dy + dxWrapped * dxWrapped);
return distance;
}
}
\ No newline at end of file
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Bounds.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Bounds.java
deleted file mode 100644
index 9599f7e5f69..00000000000
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Bounds.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.sap.sailing.domain.common.quadtree.impl;
-
-import com.sap.sailing.domain.common.Position;
-
-public class Bounds {
- private final Position southWest;
- private final Position northEast;
-
- public Bounds(Position southWest, Position northEast) {
- this.southWest = southWest;
- this.northEast = northEast;
- }
-
- public Position getSouthWest() {
- return southWest;
- }
-
- public Position getNorthEast() {
- return northEast;
- }
-
- public boolean contains(Position p) {
- return p.getLatDeg() >= getSouthWest().getLatDeg() && p.getLatDeg() <= getNorthEast().getLatDeg() &&
- (getNorthEast().getLngDeg() >= getSouthWest().getLngDeg() && p.getLngDeg() >= getSouthWest().getLngDeg() && p.getLngDeg() <= getNorthEast().getLngDeg() ||
- // cross date line bounds
- getNorthEast().getLngDeg() < getSouthWest().getLngDeg() &&
- (p.getLngDeg() <= 180 && p.getLngDeg() >= getSouthWest().getLngDeg() ||
- p.getLngDeg() >= -180 && p.getLngDeg() <= getNorthEast().getLngDeg()));
- }
-
- public boolean containsBounds(Bounds rect) {
- return this.contains(rect.getSouthWest()) && this.contains(rect.getNorthEast()) ||
- rect.contains(this.getSouthWest()) && rect.contains(this.getNorthEast());
- }
-
-}
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/MutableDistance.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/MutableDistance.java
deleted file mode 100644
index 33abdb50680..00000000000
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/MutableDistance.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// **********************************************************************
-//
-//
-//
-// BBN Technologies
-// 10 Moulton Street
-// Cambridge, MA 02138
-// (617) 873-8000
-//
-// Copyright (C) BBNT Solutions LLC. All rights reserved.
-//
-//
-// **********************************************************************
-//
-// $Source$
-// $RCSfile$
-// $Revision: 15 $
-// $Date: 2008-04-20 02:18:58 +0200 (Sun, 20 Apr 2008) $
-// $Author: uhl $
-//
-// **********************************************************************
-
-package com.sap.sailing.domain.common.quadtree.impl;
-
-/**
- * A *really* simple class used as a changable double.
- */
-public class MutableDistance {
- public double value = 0;
-
- public MutableDistance(double distance) {
- value = distance;
- }
-}
\ No newline at end of file
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Node.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Node.java
new file mode 100644
index 00000000000..fb0d5aa6026
--- /dev/null
+++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/Node.java
@@ -0,0 +1,423 @@
+package com.sap.sailing.domain.common.quadtree.impl;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.sap.sailing.domain.common.Bounds;
+import com.sap.sailing.domain.common.Position;
+import com.sap.sailing.domain.common.impl.BoundsImpl;
+import com.sap.sailing.domain.common.impl.DegreePosition;
+import com.sap.sailing.domain.common.quadtree.QuadTree;
+import com.sap.sse.common.Util;
+
+/**
+ * A node in a {@link QuadTree}. There may be internal nodes that have no elements in them but have exactly
+ * four child nodes, or leaf nodes that have no children but contain item elements.
+ *
+ * @author Axel Uhl (D043530)
+ *
+ * @param
+ */
+public class Node {
+ // TODO remove when done
+ public static int visitedItems;
+ public static int visitedNodes;
+ public static int skippedNodes;
+
+ /**
+ * Either an array of exactly four child nodes which are then all non-null, or null,
+ * meaning that this node is a child node, having no children but potentially having items. A node that has children
+ * has no items. Quadrants are numbered as usual in geometry. See {@link #NE}, {@link #NW}, {@link #SW} and
+ * {@link #SE}.
+ */
+ private Node[] children;
+
+ private Map items;
+
+ private final Bounds bounds;
+
+ /**
+ * The maximum number of items to hold in {@link #items}. If {@link #put(Position, Object) adding} an item to this node
+ * would increase the item collection's size beyond this number, the node is {@link #split() split} into four new leaf
+ * nodes, and its items are distributed across those new leaf nodes. This number must be a positive integer.
+ */
+ private final int maxItems;
+
+ /**
+ * The quadrant index in {@link #children} for the north-east subtree
+ */
+ private final int NE = 0;
+
+ /**
+ * The quadrant index in {@link #children} for the north-west subtree
+ */
+ private final int NW = 1;
+
+ /**
+ * The quadrant index in {@link #children} for the south-west subtree
+ */
+ private final int SW = 2;
+
+ /**
+ * The quadrant index in {@link #children} for the south-east subtree
+ */
+ private final int SE = 3;
+
+ /**
+ * Creates a new node with the bounds as specified. The node starts out empty, as a leaf node that has
+ * an empty set of items.
+ *
+ * @param bounds
+ * must not have a north latitude less than the south latitude, or an {@link IllegalArgumentException}
+ * will result
+ */
+ public Node(Bounds bounds, int maxItems) {
+ if (bounds.getNorthEast().getLatDeg() < bounds.getSouthEast().getLatDeg()) {
+ throw new IllegalArgumentException("North border of bounds "+bounds+" is further south than its south border");
+ }
+ if (maxItems <= 0) {
+ throw new IllegalArgumentException("Maximum number of items must be positive but was "+maxItems);
+ }
+ this.maxItems = maxItems;
+ this.bounds = bounds;
+ items = new HashMap<>(maxItems);
+ }
+
+ private void split() {
+ assert children == null;
+ assert items != null;
+ createChildren();
+ distributeItemsToChildren();
+ assert children != null;
+ assert items == null;
+ }
+
+ private void distributeItemsToChildren() {
+ assert items != null;
+ assert children != null;
+ final Map myItems = items;
+ items = null;
+ for (final Entry item : myItems.entrySet()) {
+ getChild(item.getKey()).put(item.getKey(), item.getValue());
+ }
+ assert items == null;
+ }
+
+ private Node getChild(Position key) {
+ assert children != null;
+ assert items == null;
+ assert bounds.contains(key);
+ for (final Node child : children) {
+ if (child.bounds.contains(key)) {
+ return child;
+ }
+ }
+ throw new RuntimeException("Internal error: position "+key+" is within node bounds "+bounds+" but no child contains it");
+ }
+
+ /**
+ * Adds the value to this node and ensures that the node still meets the requirements regarding size.
+ * If necessary, the node is split with its items distributed across the new children. If a value already existed at
+ * position key, it is replaced.
+ *
+ * @param key
+ * the position at which to insert value. Must be contained in this node's {@link #bounds}.
+ * If it is not, an {@link IllegalArgumentException} will be thrown. Must not be null.
+ * @return the value previously at position key or null if there was no value at that
+ * position.
+ */
+ public T put(Position key, T value) {
+ if (value == null) {
+ throw new NullPointerException("Cannot insert null values into this node");
+ }
+ if (key == null) {
+ throw new NullPointerException("null keys not allowed");
+ }
+ if (!bounds.contains(key)) {
+ throw new IllegalArgumentException("key "+key+" must be within this node's bounds "+bounds);
+ }
+ final T result;
+ if (items != null) {
+ result = items.put(key, value);
+ if (result == null) {
+ // the size of this node has increased by one; check size constraint
+ if (items.size() > maxItems) {
+ split();
+ }
+ }
+ } else {
+ result = getChild(key).put(key, value);
+ }
+ return result;
+ }
+
+ /**
+ * Removes the element at position key from this node or any child nodes if such an element exists
+ *
+ * @return the value removed, or null if no element existed at position key in this node
+ * or any of its children
+ */
+ public T remove(Position key) {
+ final T result;
+ if (key != null) {
+ if (items != null) {
+ result = items.remove(key);
+ } else {
+ result = getChild(key).remove(key);
+ }
+ } else {
+ result = null;
+ }
+ return result;
+ }
+
+ private void createChildren() {
+ assert children == null;
+ @SuppressWarnings("unchecked")
+ final Node[] newChildren = (Node[]) new Node>[4];
+ children = newChildren;
+ final Position middleWest = new DegreePosition((bounds.getSouthWest().getLatDeg() + bounds.getNorthEast().getLatDeg())/2.,
+ bounds.getSouthWest().getLngDeg());
+ final Position southCenter = new DegreePosition(bounds.getSouthWest().getLatDeg(),
+ (bounds.getNorthEast().getLngDeg() + bounds.getSouthWest().getLngDeg()) / 2. -
+ // adjust for date line crossing if necessary
+ (bounds.getNorthEast().getLngDeg() >= bounds.getSouthWest().getLngDeg() ? 0. : 360.));
+ final Position middleCenter = new DegreePosition(middleWest.getLatDeg(), southCenter.getLngDeg());
+ final Position middleEast = new DegreePosition(middleWest.getLatDeg(), bounds.getNorthEast().getLngDeg());
+ final Position northCenter = new DegreePosition(bounds.getNorthEast().getLatDeg(), southCenter.getLngDeg());
+ children[NE] = new Node(new BoundsImpl(middleCenter, bounds.getNorthEast()), maxItems);
+ children[NW] = new Node(new BoundsImpl(middleWest, northCenter), maxItems);
+ children[SW] = new Node(new BoundsImpl(bounds.getSouthWest(), middleCenter), maxItems);
+ children[SE] = new Node(new BoundsImpl(southCenter, middleEast), maxItems);
+ assert children != null;
+ assert children.length == 4;
+ assert children[NE] != null && children[NW] != null && children[SW] != null && children[SE] != null;
+ }
+
+ /**
+ * Remove all elements from this node by either removing all its items locally or by removing all children
+ * and converting this back into a leaf node.
+ */
+ public void clear() {
+ if (items != null) {
+ items.clear();
+ } else {
+ items = new HashMap<>();
+ children = null;
+ }
+ }
+
+ public static class GetResult {
+ private final Position position;
+ private final T value;
+ private final double distance;
+ public GetResult(Position position, T value, double distance) {
+ super();
+ this.position = position;
+ this.value = value;
+ this.distance = distance;
+ }
+ public Position getPosition() {
+ return position;
+ }
+ public T getValue() {
+ return value;
+ }
+ public double getDistance() {
+ return distance;
+ }
+ @Override
+ public String toString() {
+ return "GetResult [position=" + position + ", value=" + value + ", distance=" + distance + "]";
+ }
+ }
+
+ /**
+ * Get the value nearest to point. If the node is empty, null is returned. Distance is
+ * calculated using the method {@link QuadTree#getLatLngDistance(Position, Position)} which is an approximation
+ * only, based on Euklidian geometry with the latitude/longitude values.
+ *
+ *
+ * If this is a leaf node, the nearest key by the definition above is used to determine the corresponding value and
+ * return it. Otherwise, the children are traversed. For the first child, the nearest key is determined recursively.
+ * Other children only need to be traversed if their bounds are closer to point than the key found so
+ * far.
+ *
+ * @param withinDistance
+ * a key is only considered if it has less than this {@link QuadTree#getLatLngDistance(Position, Position)
+ * distance} to point
+ */
+ public GetResult get(Position point, double withinDistance) {
+ assert (items == null) != (children == null);
+ visitedNodes++;
+ GetResult result = null;
+ double minDistance = withinDistance;
+ if (items != null) {
+ for (final Entry item : items.entrySet()) {
+ double itemDistanceToPoint = QuadTree.getLatLngDistance(point, item.getKey());
+ visitedItems++;
+ if (itemDistanceToPoint < minDistance) {
+ result = new GetResult(item.getKey(), item.getValue(), itemDistanceToPoint);
+ minDistance = itemDistanceToPoint;
+ }
+ }
+ } else {
+ // Traverse children in order of ascending distance, making it more likely to find good, near
+ // candidates early, hence enabling skipping more children, investigating fewer items. However,
+ // doing the full java.util.Collections / Arrays sort thing with Comparators and such is way too
+ // expensive as measurements have shown, and the overhead for just the four elements is too high.
+ // It is much more efficient to traverse the small children array multiple times and in each pass
+ // remembering the next minimal child distance so that during the next pass those children can be
+ // inspected. This process repeats until all children have been handled, taking at most four passes.
+ final double[] childDistanceToPoint = new double[4];
+ double minChildDistanceToPoint = Double.MAX_VALUE; // minimal distance of those children not yet done
+ for (int i=0; i<4; i++) {
+ childDistanceToPoint[i] = children[i].getDistance(point);
+ if (childDistanceToPoint[i] < minChildDistanceToPoint) {
+ minChildDistanceToPoint = childDistanceToPoint[i];
+ }
+ }
+ final boolean done[] = new boolean[4]; // have we handled that child already (skipped or searched)?
+ int doneCount = 0;
+ while (doneCount < 4) {
+ double nextMinChildDistanceToPoint = Double.MAX_VALUE;
+ for (int i=0; doneCount<4 && i<4; i++) {
+ if (!done[i]) {
+ if (childDistanceToPoint[i] == minChildDistanceToPoint) {
+ final Node child = children[i];
+ // If we have a possible result already, only investigate the child node if it is nearer to point
+ // than the result candidate and not an empty leaf node (which can be checked more quickly than
+ // performing the rather expensive distance calculation)
+ if (!child.isEmptyLeaf() && childDistanceToPoint[i] < minDistance) {
+ assert minDistance <= withinDistance;
+ final GetResult childResult = child.get(point, minDistance);
+ if (childResult != null) {
+ if (childResult.getDistance() < minDistance) {
+ result = childResult;
+ minDistance = childResult.getDistance();
+ }
+ }
+ } else {
+ skippedNodes++;
+ }
+ done[i] = true;
+ doneCount++;
+ } else {
+ if (childDistanceToPoint[i] < nextMinChildDistanceToPoint) {
+ nextMinChildDistanceToPoint = childDistanceToPoint[i]; // the distance to handle in the next pass
+ }
+ }
+ }
+ }
+ minChildDistanceToPoint = nextMinChildDistanceToPoint;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Get the value nearest to point. If the node is empty, null is returned. Distance is
+ * calculated using the method {@link QuadTree#getLatLngDistance(Position, Position)} which is an approximation
+ * only, based on Euklidian geometry with the latitude/longitude values.
+ *
+ *
+ * If this is a leaf node, the nearest key by the definition above is used to determine the corresponding value and
+ * return it. Otherwise, the children are traversed. For the first child, the nearest key is determined recursively.
+ * Other children only need to be traversed if their bounds are closer to point than the key found so
+ * far.
+ */
+ public GetResult get(Position point) {
+ return get(point, Double.MAX_VALUE);
+ }
+
+ /**
+ * To avoid expensive distance checks, a shortcut can be to check whether this node is an empty
+ * leaf node. This is the case if {@link #items} is not null (indicating a leaf node)
+ * but empty.
+ */
+ private boolean isEmptyLeaf() {
+ return items != null && items.isEmpty();
+ }
+
+ /**
+ * Computes a "distance" of this node's {@link #bounds} to the point based on the distance definition
+ * provided by {@link QuadTree#getLatLngDistance(Position, Position)}. If point is
+ * {@link Bounds#contains(Position) contained} in this node's {@link #bounds}, the distance returned is
+ * 0. Otherwise, the distance to the nearest border of this node's {@link #bounds} is determined.
+ */
+ protected double getDistance(Position point) {
+ final double result;
+ if (bounds.contains(point)) {
+ result = 0;
+ } else {
+ // Find out the point on the border defined by bounds that is nearest to point.
+ final double latDegNearestOnBorder =
+ point.getLatDeg() > bounds.getNorthEast().getLatDeg() ? bounds.getNorthEast().getLatDeg() :
+ point.getLatDeg() < bounds.getSouthWest().getLatDeg() ? bounds.getSouthWest().getLatDeg() : point.getLatDeg();
+ // Now construct a Bounds object of zero latitude span at point's latitude and with the longitude span
+ // copied from this Node's bounds. If these bounds contain point, use point's longitude as it is spanned by
+ // this node's bounds. Otherwise, extend the bounds to contain point, then check whether the west or east border
+ // was changed to contain point; point was closer to the border that was changed to include point
+ final Bounds boundsAtPointLat = new BoundsImpl(new DegreePosition(point.getLatDeg(), bounds.getSouthWest().getLngDeg()),
+ new DegreePosition(point.getLatDeg(), bounds.getNorthEast().getLngDeg()));
+ final double lngDegNearestOnBorder;
+ if (boundsAtPointLat.contains(point)) {
+ lngDegNearestOnBorder = point.getLngDeg();
+ } else {
+ final Bounds boundsAtPointLatExtendedToIncludePoint = boundsAtPointLat.extend(point);
+ if (boundsAtPointLatExtendedToIncludePoint.getSouthWest().getLngDeg() == bounds.getSouthWest().getLngDeg()) {
+ lngDegNearestOnBorder = bounds.getNorthEast().getLngDeg();
+ } else {
+ lngDegNearestOnBorder = bounds.getSouthWest().getLngDeg();
+ }
+ }
+ result = QuadTree.getLatLngDistance(point, new DegreePosition(latDegNearestOnBorder, lngDegNearestOnBorder));
+ }
+ return result;
+ }
+
+ /**
+ * Fetches all values within the bounds specified. Always returns a non-null iterable
+ * which may, however, be empty.
+ *
+ * @param bounds
+ * It is permissible for bounds to not even intersect with this node's {@link #bounds}.
+ * Then, however, the result will be empty.
+ */
+ public Iterable get(Bounds bounds) {
+ final Iterable result;
+ if (this.bounds.intersects(bounds)) {
+ Set preResult = new HashSet<>();
+ if (items != null) {
+ if (bounds.contains(this.bounds)) {
+ // all items must be contained, so no per-item check is required
+ preResult.addAll(items.values());
+ } else {
+ for (final Entry item : items.entrySet()) {
+ if (bounds.contains(item.getKey())) {
+ preResult.add(item.getValue());
+ }
+ }
+ }
+ } else {
+ for (final Node child : children) {
+ Util.addAll(child.get(bounds), preResult);
+ }
+ }
+ result = preResult;
+ } else {
+ result = Collections.emptySet();
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return ""+bounds+": "+(items==null?0:items.size())+" items in node, "+
+ (children==null?0:children.length)+" child nodes";
+ }
+}
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeLeaf.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeLeaf.java
deleted file mode 100644
index 1ea4950e23f..00000000000
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeLeaf.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// **********************************************************************
-//
-//
-//
-// BBN Technologies
-// 10 Moulton Street
-// Cambridge, MA 02138
-// (617) 873-8000
-//
-// Copyright (C) BBNT Solutions LLC. All rights reserved.
-//
-//
-// **********************************************************************
-//
-// $Source:
-// /cvs/distapps/openmap/src/openmap/com/bbn/openmap/util/quadtree/QuadTreeLeaf.java,v
-// $
-// $RCSfile$
-// $Revision: 45 $
-// $Date: 2008-06-08 22:04:43 +0200 (Sun, 08 Jun 2008) $
-// $Author: uhl $
-//
-// **********************************************************************
-
-package com.sap.sailing.domain.common.quadtree.impl;
-
-import java.io.Serializable;
-
-import com.sap.sailing.domain.common.Position;
-
-public class QuadTreeLeaf implements Serializable {
-
- static final long serialVersionUID = 7885745536157252519L;
-
- private Position point;
- private T object;
-
- public QuadTreeLeaf(Position point, T obj) {
- this.point = point;
- this.object = obj;
- }
-
- public Position getPoint() {
- return point;
- }
-
- public T getObject() {
- return object;
- }
-
- public String toString() {
- return "QuadTreeLeaf at (" + point.getLatDeg() + ", " + point.getLngDeg() + ") with object " + getObject();
- }
-
-}
\ No newline at end of file
diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeNode.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeNode.java
deleted file mode 100644
index ee6229d64a6..00000000000
--- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/quadtree/impl/QuadTreeNode.java
+++ /dev/null
@@ -1,407 +0,0 @@
-// **********************************************************************
-//
-//
-//
-// BBN Technologies
-// 10 Moulton Street
-// Cambridge, MA 02138
-// (617) 873-8000
-//
-// Copyright (C) BBNT Solutions LLC. All rights reserved.
-//
-//
-// **********************************************************************
-//
-// $Source$
-// $RCSfile$
-// $Revision: 101 $
-// $Date: 2008-07-09 23:09:42 +0200 (Wed, 09 Jul 2008) $
-// $Author: axel.uhl $
-//
-// **********************************************************************
-
-package com.sap.sailing.domain.common.quadtree.impl;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Vector;
-
-import com.sap.sailing.domain.common.Position;
-import com.sap.sailing.domain.common.impl.DegreePosition;
-import com.sap.sailing.domain.common.quadtree.QuadTree;
-
-/**
- * The QuadTreeNode is the part of the QuadTree that either holds
- * children nodes, or objects as leaves. Currently, the nodes that
- * have children do not hold items that span across children
- * boundaries, since this was designed to handle point data.
- */
-public class QuadTreeNode implements Serializable {
-
- static final long serialVersionUID = -6111633198469889444L;
-
- private final static int NORTHWEST = 0;
- private final static int NORTHEAST = 1;
- private final static int SOUTHEAST = 2;
- private final static int SOUTHWEST = 3;
- public final static double NO_MIN_SIZE = -1;
-
- private Vector> items;
- private QuadTreeNode[] children;
- private int maxItems;
- private double minSize;
- private Bounds bounds;
-
- /**
- * Added to avoid problems when a node is completely filled with a
- * single point value.
- */
- private boolean allTheSamePoint;
- private Position firstPoint;
-
- /**
- * Constructor to use if you are going to store the objects in
- * lat/lon space, and there is really no smallest node size.
- *
- * @param maximumItems number of items to hold in a node before
- * splitting itself into four children and redispensing the
- * items into them.
- */
- public QuadTreeNode(Bounds rect, int maximumItems) {
- this(rect, maximumItems, NO_MIN_SIZE);
- }
-
- /**
- * Constructor to use if you are going to store the objects in x/y
- * space, and there is a smallest node size because you don't want
- * the nodes to be smaller than a group of pixels.
- *
- * @param north northern border of node coverage.
- * @param west western border of node coverage.
- * @param south southern border of node coverage.
- * @param east eastern border of node coverage.
- * @param maximumItems number of items to hold in a node before
- * splitting itself into four children and redispensing the
- * items into them.
- * @param minimumSize the minimum difference between the
- * boundaries of the node.
- */
- public QuadTreeNode(Bounds rect, int maximumItems, double minimumSize) {
- bounds = rect;
- maxItems = maximumItems;
- minSize = minimumSize;
- items = new Vector>();
- }
-
- /** Return true if the node has children. */
- public boolean hasChildren() {
- if (children != null)
- return true;
- else
- return false;
- }
-
- /**
- * This method splits the node into four children, and disperses
- * the items into the children. The split only happens if the
- * boundary size of the node is larger than the minimum size (if
- * we care). The items in this node are cleared after they are put
- * into the children.
- */
- @SuppressWarnings("unchecked")
- protected void split() {
- // Make sure we're bigger than the minimum, if we care,
- if (minSize != NO_MIN_SIZE) {
- if (Math.abs(bounds.getNorthEast().getLatDeg() - bounds.getSouthWest().getLatDeg()) < minSize
- && Math.abs(bounds.getNorthEast().getLngDeg() - bounds.getSouthWest().getLngDeg()) < minSize)
- return;
- }
-
- double nsHalf = (bounds.getNorthEast().getLatDeg() + bounds.getSouthWest().getLatDeg()) / 2.0;
- double ewHalf = (bounds.getNorthEast().getLngDeg() + bounds.getSouthWest().getLngDeg()) / 2.0;
- children = new QuadTreeNode[4];
- children[NORTHWEST] = new QuadTreeNode(new Bounds(new DegreePosition(nsHalf, bounds.getSouthWest().getLngDeg()), new DegreePosition(bounds.getNorthEast().getLatDeg(), ewHalf)), maxItems);
- children[NORTHEAST] = new QuadTreeNode(new Bounds(new DegreePosition(nsHalf, ewHalf), bounds.getNorthEast()), maxItems);
- children[SOUTHEAST] = new QuadTreeNode(new Bounds(new DegreePosition(bounds.getSouthWest().getLatDeg(), ewHalf), new DegreePosition(nsHalf, bounds.getNorthEast().getLngDeg())), maxItems);
- children[SOUTHWEST] = new QuadTreeNode(new Bounds(bounds.getSouthWest(), new DegreePosition(nsHalf, ewHalf)), maxItems);
- Vector