Commit 1837b3b1 authored by Nico Mack's avatar Nico Mack

Fixes to KdTree and ClusterManager implementation.

Changes to POM and Eclipse project setup
parent 24907935
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="config"/>
<classpathentry excluding="main/java/|test/java/" kind="src" output="target/classes" path="src"/>
<classpathentry kind="src" output="target/test-classes" path="test"/>
<classpathentry kind="src" output="target/classes" path="src">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="test">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
<accessrules>
<accessrule kind="accessible" pattern="sun/java2d/**"/>
</accessrules>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
......
......@@ -11,12 +11,12 @@
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<name>org.fusesource.ide.project.RiderProjectBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.fusesource.ide.project.RiderProjectBuilder</name>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
......
eclipse.preferences.version=1
encoding/<project>=UTF-8
encoding/src=UTF-8
encoding/test=UTF-8
......@@ -25,6 +25,7 @@ CENTRE_NODE=centre
CENTRED_NODE=centred
CIRCLE_SIZE_NODE=circleSize
CLUSTERABLE_NODE=clusterable
CLUSTERED_BUNDLE_NODE=clusteredBundle
COLOUR_NODE=colour
COLOUR_PALETTE_NODE=colourPalette
COLOURSC_NODE=colourscheme
......
......@@ -25,8 +25,8 @@
</scm>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<plugins>
<plugin>
......@@ -70,6 +70,7 @@
<manifest>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
<manifestFile>config/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
......
......@@ -21,6 +21,9 @@ import lu.list.itis.dkd.tui.utility.ScreenCoordinates;
import lu.list.itis.dkd.tui.utility.kdtree.KdComparator;
import lu.list.itis.dkd.tui.utility.kdtree.KdTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
......@@ -44,6 +47,12 @@ public class ClusterManager<P extends Positionable> {
private KdTree<P> kdTree;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static final Logger LOGGER = LoggerFactory.getLogger(ClusterManager.class.getSimpleName());
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s)
......@@ -69,17 +78,6 @@ public class ClusterManager<P extends Positionable> {
// ***************************************************************************
// ---------------------------------------------------------------------------
private synchronized List<Clusterable> getClusterable(List<P> candidates) {
List<Clusterable> results = new ArrayList<>();
for (P candidate : candidates) {
if (Clusterable.class.isAssignableFrom(candidate.getClass())) {
results.add((Clusterable) candidate);
}
}
return results;
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Class Body
......@@ -107,6 +105,16 @@ public class ClusterManager<P extends Positionable> {
}
}
// ---------------------------------------------------------------------------
public void setup() {
if (!this.constituents.isEmpty()) {
kdTree = new KdTree<>(this.constituents, 2, this.comparator);
} else {
LOGGER.info("No clusterable items available! Clustering is disabled!");
}
}
// ---------------------------------------------------------------------------
/**
* @param positionables
......@@ -114,19 +122,47 @@ public class ClusterManager<P extends Positionable> {
// ---------------------------------------------------------------------------
public void refresh() {
kdTree = new KdTree<>(this.constituents, 2, this.comparator);
clustered.clear();
List<P> coallesced = new ArrayList<>();
for (P candidate : this.constituents) {
if ((candidate instanceof Clusterable) && !coallesced.contains(candidate)) {
List<P> coallescing = kdTree.findNearest(candidate, radius);
if (coallescing.size() > 1) {
List<Clusterable> clusterable = this.getClusterable(coallescing);
coallesced.addAll(coallescing);
((Clusterable) candidate).coalesce(clusterable);
if (kdTree == null)
return;
List<P> retained = new ArrayList<>();
List<P> coalesced = new ArrayList<>();
List<P> candidates = new ArrayList<>(this.constituents);
boolean clusterDetected;
int iterations = 0;
do {
clusterDetected = false;
for (P candidate : candidates) {
if ((candidate instanceof Clusterable) && !coalesced.contains(candidate)) {
List<P> coalescing = kdTree.findNearest(candidate, radius);
boolean isClustered = (!coalescing.isEmpty());
coalescing.removeAll(coalesced);
clusterDetected |= (!coalescing.isEmpty());
if (isClustered) {
coalesced.addAll(coalescing);
retained.removeAll(coalescing);
}
((Clusterable) candidate).setClustered(isClustered);
if (!retained.contains(candidate)) {
retained.add(candidate);
}
}
clustered.add(candidate);
}
candidates.clear();
candidates.addAll(retained);
iterations++;
} while (clusterDetected);
synchronized (clustered) {
clustered.clear();
clustered.addAll(retained);
LOGGER.info("Clustering completed in {} iteration(s)! {} Marker(s) remaining!", iterations, clustered.size());
}
}
......@@ -138,8 +174,8 @@ public class ClusterManager<P extends Positionable> {
// ---------------------------------------------------------------------------
public List<P> getClusters() {
return this.clustered;
public synchronized List<P> getClusters() {
return new ArrayList<>(this.clustered);
}
// ---------------------------------------------------------------------------
......
......@@ -16,8 +16,6 @@
*/
package lu.list.itis.dkd.tui.feature.cluster;
import java.util.List;
/**
* @author nico.mack@list.lu
* @since 2.6
......@@ -37,7 +35,7 @@ public interface Clusterable {
*
* @return <code>true</code> if object is currently in coalesced state, <code>false</code> otherwise
*/
public boolean isCoalesced();
public boolean isClustered();
/**
* Tells clusterable items to collaesce into a single clustered item.
......@@ -45,13 +43,5 @@ public interface Clusterable {
* @param disocciated
* @return
*/
public Clusterable coalesce(List<Clusterable> disocciated);
/**
* Tells a previously clustered item to break up into its constituting parts.
*
* @param coallesced
* @return
*/
public List<Clusterable> dissociate(Clusterable coallesced);
}
public void setClustered(boolean isClustered);
}
\ No newline at end of file
......@@ -22,6 +22,9 @@ import lu.list.itis.dkd.tui.utility.Positionable;
import lu.list.itis.dkd.tui.utility.ScreenCoordinates;
import lu.list.itis.dkd.tui.utility.kdtree.KdComparator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author mack
* @since [major].[minor]
......@@ -31,6 +34,15 @@ public class PositionableComparator implements KdComparator<Positionable> {
private Class<? extends CoordinateState> referenceState = ScreenCoordinates.class;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static final Logger LOGGER = LoggerFactory.getLogger(PositionableComparator.class.getSimpleName());
/**
* @param state
*/
public void setReferenceState(Class<? extends CoordinateState> state) {
this.referenceState = state;
}
......@@ -38,31 +50,34 @@ public class PositionableComparator implements KdComparator<Positionable> {
/** {@inheritDoc} */
@Override
public int compare(Positionable o1, Positionable o2, int axis) {
Point pos1 = o1.getPosition().clone();
Point pos2 = o2.getPosition().clone();
pos1.toCoordinates(this.referenceState);
pos2.toCoordinates(this.referenceState);
if (o1.equals(o2))
return 0;
Point pos1 = o1.getPosition().toCoordinates(this.referenceState);
Point pos2 = o2.getPosition().toCoordinates(this.referenceState);
return pos1.compareTo(pos2, axis);
}
/** {@inheritDoc} */
@Override
public double getDistance(Positionable o1, Positionable o2) {
Point pos1 = o1.getPosition().clone();
Point pos2 = o2.getPosition().clone();
pos1.toCoordinates(this.referenceState);
pos2.toCoordinates(this.referenceState);
return pos1.getDistance(pos2);
if (o1.equals(o2))
return 0;
Point pos1 = o1.getPosition().toCoordinates(this.referenceState);
Point pos2 = o2.getPosition().toCoordinates(this.referenceState);
return Math.abs(pos1.getDistance(pos2));
}
/** {@inheritDoc} */
@Override
public double getDistance(Positionable o1, Positionable o2, int axis) {
Point pos1 = o1.getPosition().clone();
Point pos2 = o2.getPosition().clone();
pos1.toCoordinates(this.referenceState);
pos2.toCoordinates(this.referenceState);
return pos1.getDistance(pos2, axis);
if (o1.equals(o2))
return 0;
Point pos1 = o1.getPosition().toCoordinates(this.referenceState);
Point pos2 = o2.getPosition().toCoordinates(this.referenceState);
return Math.abs(pos1.getDistance(pos2, axis));
}
}
......@@ -36,8 +36,7 @@ public abstract class Marker implements Positionable, Clusterable, Cloneable {
protected boolean active;
protected boolean isClusterable;
protected boolean isCoalesced;
protected List<Clusterable> clustered;
protected boolean isClustered;
private CoronaComparator coronaComparator;
......@@ -58,7 +57,7 @@ public abstract class Marker implements Positionable, Clusterable, Cloneable {
this.isClusterable = builder.isClusterable;
this.coronas = builder.coronas;
this.positions = builder.positions;
this.clustered = new ArrayList<>();
this.isClustered = false;
}
// ---------------------------------------------------------------------------
......@@ -68,7 +67,7 @@ public abstract class Marker implements Positionable, Clusterable, Cloneable {
this.isClusterable = original.isClusterable;
this.coronas = original.cloneCoronas();
this.positions = original.clonePositions();
this.clustered = new ArrayList<>();
this.isClustered = false;
}
// ---------------------------------------------------------------------------
......@@ -208,14 +207,17 @@ public abstract class Marker implements Positionable, Clusterable, Cloneable {
* @return
*/
public @Nullable Point getPosition(Integer handleId) {
return positions.get(handleId);
if (positions.containsKey(handleId)) {
return positions.get(handleId).clone();
}
return null;
}
// ---------------------------------------------------------------------------
@Override
public @Nullable Point getPosition() {
return positions.get(this.getHandleId());
return this.getPosition(this.getHandleId());
}
// ---------------------------------------------------------------------------
......@@ -321,49 +323,25 @@ public abstract class Marker implements Positionable, Clusterable, Cloneable {
// ---------------------------------------------------------------------------
@Override
public boolean isCoalesced() {
return this.isCoalesced;
public boolean isClustered() {
return this.isClustered;
}
// ---------------------------------------------------------------------------
@Override
public Clusterable coalesce(List<Clusterable> disocciated) {
this.clustered.addAll(disocciated);
this.isCoalesced = true;
return this;
public void setClustered(boolean isClustered) {
this.isClustered = isClustered;
}
// ---------------------------------------------------------------------------
@Override
public List<Clusterable> dissociate(Clusterable coalesced) {
List<Clusterable> dissociated = new ArrayList<>();
if (coalesced.equals(this)) {
dissociated.addAll(this.clustered);
this.clustered.clear();
this.isCoalesced = false;
} else {
List<Clusterable> affected = new ArrayList<>();
for (Clusterable member : this.clustered) {
if (member.isCoalesced()) {
List<Clusterable> returned = member.dissociate(coalesced);
if (!returned.isEmpty()) {
dissociated.addAll(returned);
affected.add(member);
}
}
}
this.clustered.removeAll(affected);
public boolean equals(Object o) {
if (o == this) {
return true;
}
return dissociated;
}
// ---------------------------------------------------------------------------
@Override
public boolean equals(Object o) {
if (o instanceof Marker) {
return this.getHandleId().equals(((Marker) o).getHandleId());
}
......
......@@ -26,6 +26,7 @@ import lu.list.itis.dkd.tui.widget.corona.bundle.CoronaBundle;
import com.google.common.collect.Multimap;
import com.google.common.collect.TreeMultimap;
import com.jgoodies.common.base.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -49,6 +50,7 @@ public class ModalMarker extends TetherableMarker {
protected Multimap<String, CoronaBundle> coronaBundles;
protected List<String> activeBundles;
protected String clusteredBundle;
// ***************************************************************************
// * Constants *
......@@ -70,8 +72,9 @@ public class ModalMarker extends TetherableMarker {
super(builder);
coronaBundles = builder.coronaBundles;
activeBundles = new ArrayList<>();
clusteredBundle = builder.clusteredBundle;
activeBundles = new ArrayList<>();
this.activateDefaults();
}
......@@ -84,6 +87,7 @@ public class ModalMarker extends TetherableMarker {
public ModalMarker(ModalMarker original) {
super(original);
clusteredBundle = original.clusteredBundle;
coronaBundles = TreeMultimap.create();
coronaBundles.putAll(original.coronaBundles);
activeBundles = new ArrayList<>();
......@@ -283,6 +287,16 @@ public class ModalMarker extends TetherableMarker {
// ---------------------------------------------------------------------------
@Override
public void setClustered(boolean isClustered) {
super.setClustered(isClustered);
if (Strings.isNotBlank(clusteredBundle)) {
this.activateBundle(clusteredBundle, isClustered);
}
}
// ---------------------------------------------------------------------------
public ModalMarker clone() {
return new ModalMarker(this);
}
......
......@@ -19,6 +19,7 @@ package lu.list.itis.dkd.tui.marker.builder;
import lu.list.itis.dkd.dbc.annotation.Nullable;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapCallback;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapContext;
import lu.list.itis.dkd.tui.bootstrapping.BootstrappingUtils;
import lu.list.itis.dkd.tui.exception.BuildException;
import lu.list.itis.dkd.tui.marker.ModalMarker;
import lu.list.itis.dkd.tui.utility.Externalization;
......@@ -43,6 +44,7 @@ import org.jdom2.Element;
public abstract class BaseModalMarkerBuilder<B extends BaseModalMarkerBuilder<B>> extends BaseTetherableMarkerBuilder<B> {
public Multimap<String, CoronaBundle> coronaBundles;
public String clusteredBundle;
// ***************************************************************************
// * Constants *
......@@ -88,6 +90,7 @@ public abstract class BaseModalMarkerBuilder<B extends BaseModalMarkerBuilder<B>
coronaBundles.put(bundle.getName(), bundle);
}
}
this.clusteredBundle = BootstrappingUtils.getContentAsString(rootElement, Externalization.CLUSTERED_BUNDLE_NODE, BootstrappingUtils.OPTIONAL, null, context);
}
// ---------------------------------------------------------------------------
......
......@@ -60,6 +60,7 @@ public class Externalization extends NLS {
public static String CENTRED_NODE;
public static String CIRCLE_SIZE_NODE;
public static String CLUSTERABLE_NODE;
public static String CLUSTERED_BUNDLE_NODE;
public static String COLOUR_NODE;
public static String COLOUR_PALETTE_NODE;
public static String COLOURSC_NODE;
......
......@@ -66,13 +66,12 @@ public class Point extends Float implements Comparable<Point> {
private static final Logger logger = LoggerFactory.getLogger(Point.class.getSimpleName());
private static final int NUMBER_OF_DIMENSIONS = 2;
/** Constant identifying the X Axis */
public static final int X_AXIS = 0;
/** Constant identifying the Y Axis */
public static final int Y_AXIS = 1;
protected static enum PointComparator implements Comparator<Point> {
protected enum PointComparator implements Comparator<Point> {
x {
@Override
public int compare(Point a, Point b) {
......@@ -84,6 +83,12 @@ public class Point extends Float implements Comparable<Point> {
public int compare(Point a, Point b) {
return java.lang.Double.compare(a.y, b.y);
}
},
angle {
@Override
public int compare(Point a, Point b) {
return java.lang.Double.compare(AngleUtils.moduloTwoPi(a.getAngle()), AngleUtils.moduloTwoPi(b.getAngle()));
}
}
}
......@@ -167,22 +172,6 @@ public class Point extends Float implements Comparable<Point> {
public Point(float x, float y, float angle, Class<? extends CoordinateState> initialState) {
super(x, y);
this.angle = angle;
// switch (initialState.getSimpleName()) {
// case "CameraCoordinates": //$NON-NLS-1$
// state = new CameraCoordinates(this, x, y);
// break;
// case "ScreenCoordinates": //$NON-NLS-1$
// state = new ScreenCoordinates(this, x, y);
// break;
// case "TableCoordinates": //$NON-NLS-1$
// state = new TableCoordinates(this, x, y);
// break;
// default:
// state = new CameraCoordinates(this, x, y);
// break;
// }
state = CoordinateStateFactory.newInstance(initialState, this, x, y);
}
......@@ -580,6 +569,28 @@ public class Point extends Float implements Comparable<Point> {
return comparison;
}
/**
* @param other
* @return
*/
@Override
public boolean equals(Object other) {
if (!(other instanceof Point))
return false;
Point otherPoint = (Point) other;
boolean isEqual = true;
isEqual &= (this.x == otherPoint.x);
isEqual &= (this.y == otherPoint.y);
isEqual &= (this.angle == otherPoint.angle);
isEqual &= (this.state.equals(otherPoint.state));
return isEqual;
}
/**
* Checks whether the specified point is within the acceptable bounds of its coordinate state.
*
......@@ -591,6 +602,11 @@ public class Point extends Float implements Comparable<Point> {
return state.withinBounds(this);
}
@Override
public String toString() {
return StringUtils.build("Point [x={},y={},a={}, coord={}]", x, y, angle, state.getClass().getSimpleName()); //$NON-NLS-1$
}
}
......
......@@ -60,19 +60,20 @@ public class DistanceComparator<T> implements Comparator<KdNode<T>> {
double dist1 = comparator.getDistance(origin, node1.element);
double dist2 = comparator.getDistance(origin, node2.element);
if (dist1 < dist2) {
return -1;
} else if (dist1 > dist2) {
return 1;
} else {
if (node1.hashCode() < node2.hashCode()) {
return -1;
} else if (node1.hashCode() > node2.hashCode()) {
return 1;
}
}
// if (dist1 < dist2) {
// return -1;
// } else if (dist1 > dist2) {
// return 1;
// } else {
// if (node1.hashCode() < node2.hashCode()) {
// return -1;
// } else if (node1.hashCode() > node2.hashCode()) {
// return 1;
// }
// }
// return 0;
return 0;
return Double.compare(dist1, dist2);
}
// ---------------------------------------------------------------------------
......
......@@ -28,12 +28,19 @@ package lu.list.itis.dkd.tui.utility.kdtree;
public interface KdComparator<T> {
/**
* @param o1
* @param o2
* @param axis
* @return
*/
public int compare(T o1, T o2, int axis);
/**
* returns the (squared) distance between this instance of T and the one specified.
*
* @param other
* @param o1
* @param o2
* specifies the other instance of T to get the distance from
* @return the squared distance along all axes.
*/
......@@ -42,8 +49,8 @@ public interface KdComparator<T> {
/**
* returns the (squared) distance along one axis between this instance of T and the one specified.
*
* @param other
* specifies the other instance of T to get the distance from
* @param o1
* @param o2
* @param axis
* specifies the axis to measure the distance along
* @return the squared distance along the specified axis.
......
......@@ -35,7 +35,6 @@ import java.util.TreeSet;
public class KdTree<T> {
private KdNode<T> root;
private int dimensions;
private KdComparator<T> comparator;
private Comparator<T> axisComparators[];
......@@ -52,9 +51,8 @@ public class KdTree<T> {
// ---------------------------------------------------------------------------
public KdTree(List<T> elements, int dimensions, KdComparator<T> comparator) {
this.dimensions = dimensions;
this.comparator = comparator;
this.buildAxisComparators();
this.buildAxisComparators(dimensions);
this.root = this.createNode(elements, dimensions, 0);
}
......@@ -65,9 +63,9 @@ public class KdTree<T> {
// ---------------------------------------------------------------------------
@SuppressWarnings("unchecked")
private void buildAxisComparators() {
axisComparators = new AxisComparator[this.dimensions];
for (int i = 0; i < this.dimensions; i++) {
private void buildAxisComparators(int dimensions) {
axisComparators = new AxisComparator[dimensions];
for (int i = 0; i < dimensions; i++) {
axisComparators[i] = new AxisComparator<>(i, comparator);
}
}
......@@ -127,7 +125,7 @@ public class KdTree<T> {
// ---------------------------------------------------------------------------
private void searchNode(T candidate, KdNode<T> current, KdNode<T> previous, TreeSet<KdNode<T>> nearest, HashSet<KdNode<T>> alreadySeen, double radius) {
private void searchNode(T candidate, KdNode<T> current, TreeSet<KdNode<T>> nearest, HashSet<KdNode<T>> alreadySeen, double radius) {
alreadySeen.add(current);
KdNode<T> lastNode = null;
......@@ -161,11 +159,11 @@ public class KdTree<T> {
boolean isRight = (direction >= 0);
if (left != null && !alreadySeen.contains(left) && (intersect || isLeft)) {
searchNode(candidate, left, null, nearest, alreadySeen, radius);
searchNode(candidate, left, nearest, alreadySeen, radius);
}
if (right != null && !alreadySeen.contains(right) && (intersect || isRight)) {
searchNode(candidate, right, null, nearest, alreadySeen, radius);
searchNode(candidate, right, nearest, alreadySeen, radius);
}
}
......@@ -195,20 +193,21 @@ public class KdTree<T> {
current = (current.compareTo(candidate, comparator) <= 0) ? current.right : current.left;