diff --git a/src/main/java/org/mastodon/util/TreeUtils.java b/src/main/java/org/mastodon/util/TreeUtils.java new file mode 100644 index 000000000..3d20e1c82 --- /dev/null +++ b/src/main/java/org/mastodon/util/TreeUtils.java @@ -0,0 +1,158 @@ +package org.mastodon.util; + +import java.util.Collection; + +import org.mastodon.collection.RefCollections; +import org.mastodon.collection.RefList; +import org.mastodon.collection.RefSet; +import org.mastodon.collection.RefStack; +import org.mastodon.graph.Edge; +import org.mastodon.graph.Graph; +import org.mastodon.graph.Vertex; + +public class TreeUtils +{ + + private TreeUtils() + { + // prevent instantiation of utility class + } + + /** + * This method finds a subset of the given {@code selectedVertices} that + * contains only the roots of the subtrees that are selected. The order + * of the returned list follows the order of the {@code roots} list and + * the order of the outgoing edges of the graphs vertices. + *

+ * Example {@code graph}: + *

+	 *   A                B
+	 *  / \             /   \
+	 * a1  a2          b1    b2
+	 *     |          / \   / \
+	 *     a3        b3 b4 b5 b6
+	 *    / \
+	 *   a4 a5
+	 * 
+ * + * If {@code selectedVertices} contains: {@code {a2, a4, a5, b1, b3, b4, b6}}, + * then the returned list will be: {@code [a2, b1, b6]}. + */ + public static < V extends Vertex< ? > > RefList< V > findSelectedSubtreeRoots( + final Graph< V, ? > graph, + final RefList< V > roots, + final RefSet< V > selectedVertices ) + { + final RefList< V > selectedSubtreeRoots = RefCollections.createRefList( graph.vertices() ); + final RefSet< V > visitedNodes = RefCollections.createRefSet( graph.vertices() ); + + for ( final V root : roots ) + for ( final DepthFirstIteration.Step< V > step : DepthFirstIteration.forRoot( graph, root ) ) + if ( ensureNoLoop( step, visitedNodes ) && !step.isSecondVisit() ) { + + final V node = step.node(); + if ( selectedVertices.contains( node ) ) + { + selectedSubtreeRoots.add( node ); + step.truncate(); // don't visit the child nodes + } + } + + return selectedSubtreeRoots; + } + + private static < V extends Vertex< ? > > boolean ensureNoLoop( DepthFirstIteration.Step< V > step, RefSet< V > visitedNodes ) + { + if ( !step.isFirstVisit() ) + return true; + + boolean isLoop = !visitedNodes.add( step.node() ); // The depth first iteration enters a node for the second time. -> there's a loop. + if ( isLoop ) + step.truncate(); // Break the loop by not visiting the child nodes. + + return !isLoop; + } + + /** + * This method returns the root nodes of the tracks (connected components) + * that contain any of the given {@code nodes}. + *

+ * Example: + *

+	 *   A                B      C
+	 *  / \             /   \  /
+	 * a1  a2          b1    b2
+	 *     |          / \   / \
+	 *     a3        b3 b4 b5 b6
+	 *    / \
+	 *   a4 a5
+	 * 
+ *

+ * If {@code nodes} contains {@code {a2, a4}} then the method will return + * {@code {A}}. + *

+ * If {@code nodes} contains {@code {a2, a4, b4}} then the method will + * return {@code {A, B, C}}. + */ + public static , E extends Edge< V > > RefSet< V > findRootsOfTheGivenNodes( Graph< V, E > graph, Collection< V > nodes ) + { + return filterRoots( graph, findAllConnectedNodes( graph, nodes ) ); + } + + /** + * @return the set of predecessors of the given {@code nodes}. Please note + * that returned set also contains all the given {@code nodes}. + */ + private static < V extends Vertex, E extends Edge< V > > RefSet< V > findAllConnectedNodes( Graph< V, E > graph, Collection< V > nodes ) + { + // The following code performs an inverse depth first search starting + // from the given nodes. The set of visited nodes is returned. + V ref = graph.vertexRef(); + V ref2 = graph.vertexRef(); + try + { + final RefSet< V > visited = RefCollections.createRefSet( graph.vertices() ); + visited.addAll( nodes ); + final RefStack< V > stack = RefCollections.createRefStack( graph.vertices() ); + stack.addAll( visited ); + while ( ! stack.isEmpty() ) { + V node = stack.pop( ref ); + for ( E edge : node.incomingEdges() ) { + V parentNode = edge.getSource( ref2 ); + addNode( visited, stack, parentNode ); + } + for ( E edge : node.outgoingEdges() ) { + V childNode = edge.getTarget( ref2 ); + addNode( visited, stack, childNode ); + } + } + return visited; + } + finally + { + graph.releaseRef( ref ); + graph.releaseRef( ref2 ); + } + } + + private static < V extends Vertex, E extends Edge< V > > void addNode( RefSet< V > visited, RefStack< V > stack, V parentNode ) + { + boolean firstVisit = visited.add( parentNode ); + if ( firstVisit ) + stack.add( parentNode ); + } + + /** + * @return a subset of the given {@code nodes} that contains only those + * nodes that are roots of the {@code graph}. (A note is considered a root + * if it has no incoming edges.) + */ + private static < V extends Vertex, E extends Edge< V > > RefSet< V > filterRoots( Graph< V, E > graph, Collection< V > nodes ) + { + final RefSet< V > roots = RefCollections.createRefSet( graph.vertices() ); + for ( V node : nodes ) + if ( node.incomingEdges().isEmpty() ) + roots.add( node ); + return roots; + } +} diff --git a/src/main/java/org/mastodon/views/trackscheme/display/ShowSelectedTracksActions.java b/src/main/java/org/mastodon/views/trackscheme/display/ShowSelectedTracksActions.java index 02ffc5868..50835dd8a 100644 --- a/src/main/java/org/mastodon/views/trackscheme/display/ShowSelectedTracksActions.java +++ b/src/main/java/org/mastodon/views/trackscheme/display/ShowSelectedTracksActions.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -41,7 +41,7 @@ import org.mastodon.ui.keymap.CommandDescriptionProvider; import org.mastodon.ui.keymap.CommandDescriptions; import org.mastodon.ui.keymap.KeyConfigContexts; -import org.mastodon.util.DepthFirstIteration; +import org.mastodon.util.TreeUtils; import org.mastodon.views.trackscheme.LexicographicalVertexOrder; import org.mastodon.views.trackscheme.TrackSchemeEdge; import org.mastodon.views.trackscheme.TrackSchemeGraph; @@ -172,7 +172,8 @@ private RefList< TrackSchemeVertex > getSelectedSubtreeRoots() final RefSet< TrackSchemeVertex > selectedNodes = new RefSetImp<>( viewGraph.getVertexPool() ); selectedNodes.addAll( selectionModel.getSelectedVertices() ); addEdgeTargets( selectedNodes, selectionModel.getSelectedEdges() ); - return filterRootNodes( selectedNodes ); + RefList< TrackSchemeVertex > sortedRoots = LexicographicalVertexOrder.sort( viewGraph, viewGraph.getRoots() ); + return TreeUtils.findSelectedSubtreeRoots( viewGraph, sortedRoots, selectedNodes ); } private RefList< TrackSchemeVertex > getSelectedWholeTrackRoots() @@ -192,40 +193,9 @@ private void addEdgeTargets( final RefSet< TrackSchemeVertex > selected, viewGraph.releaseRef( targetRef ); } - private RefList< TrackSchemeVertex > filterRootNodes( final RefSet< TrackSchemeVertex > selectedVertices ) - { - final RefList< TrackSchemeVertex > roots = new RefArrayList<>( viewGraph.getVertexPool() ); - for ( final TrackSchemeVertex realRoot : LexicographicalVertexOrder.sort( viewGraph, viewGraph.getRoots() ) ) - for ( final DepthFirstIteration.Step< TrackSchemeVertex > step : DepthFirstIteration.forRoot( viewGraph, - realRoot ) ) - { - final TrackSchemeVertex node = step.node(); - if ( selectedVertices.contains( node ) ) - { - roots.add( node ); - step.truncate(); - } - } - return roots; - } - private RefList< TrackSchemeVertex > getRealRoots( final RefSet< TrackSchemeVertex > selectedNodes ) { - final TrackSchemeVertex parent = viewGraph.vertexRef(); - final RefSet< TrackSchemeVertex > roots = new RefSetImp<>( viewGraph.getVertexPool() ); - A: for ( final TrackSchemeVertex vertex : selectedNodes ) - { - parent.refTo( vertex ); - while ( !parent.incomingEdges().isEmpty() ) - { - parent.incomingEdges().iterator().next().getSource( parent ); - if ( selectedNodes.contains( parent ) ) - continue A; - } - roots.add( parent ); - } - viewGraph.releaseRef( parent ); + RefSet< TrackSchemeVertex > roots = TreeUtils.findRootsOfTheGivenNodes( viewGraph, selectedNodes ); return LexicographicalVertexOrder.sort( viewGraph, roots ); } - } diff --git a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph1.java b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph1.java index b3b4d5bdd..a6e924a7a 100644 --- a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph1.java +++ b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph1.java @@ -31,17 +31,26 @@ */ public class ExampleGraph1 extends AbstractExampleGraph { + public final Spot spot0; + + public final Spot spot1; + + public final Spot spot2; + + public final Spot spot3; + + public final Spot spot4; public final BranchSpot branchSpotA; public ExampleGraph1() { super(); - Spot spot0 = addNode( "0", 0, new double[] { 1d, 2d, 3d } ); - Spot spot1 = addNode( "1", 1, new double[] { 2d, 4d, 6d } ); - Spot spot2 = addNode( "2", 2, new double[] { 3d, 6d, 9d } ); - Spot spot3 = addNode( "3", 3, new double[] { 4d, 8d, 12d } ); - Spot spot4 = addNode( "4", 3, new double[] { 5d, 10d, 15d } ); + spot0 = addNode( "0", 0, new double[] { 1d, 2d, 3d } ); + spot1 = addNode( "1", 1, new double[] { 2d, 4d, 6d } ); + spot2 = addNode( "2", 2, new double[] { 3d, 6d, 9d } ); + spot3 = addNode( "3", 3, new double[] { 4d, 8d, 12d } ); + spot4 = addNode( "4", 3, new double[] { 5d, 10d, 15d } ); addEdge( spot0, spot1 ); addEdge( spot1, spot2 ); diff --git a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph2.java b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph2.java index 8dc1fdcf4..8ac7a55e5 100644 --- a/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph2.java +++ b/src/test/java/org/mastodon/mamut/feature/branch/exampleGraph/ExampleGraph2.java @@ -46,6 +46,31 @@ */ public class ExampleGraph2 extends AbstractExampleGraph { + + public final Spot spot0; + + public final Spot spot1; + + public final Spot spot2; + + public final Spot spot3; + + public final Spot spot4; + + public final Spot spot5; + + public final Spot spot6; + + public final Spot spot7; + + public final Spot spot8; + + public final Spot spot10; + + public final Spot spot11; + + public final Spot spot13; + public final BranchSpot branchSpotA; public final BranchSpot branchSpotB; @@ -58,18 +83,18 @@ public class ExampleGraph2 extends AbstractExampleGraph public ExampleGraph2() { - Spot spot0 = addNode( "0", 0, new double[]{1d, 2d, 3d} ); - Spot spot1 = addNode( "1", 1, new double[]{0d, 0d, 0d} ); - Spot spot2 = addNode( "2", 2, new double[]{3d, 6d, 9d} ); - Spot spot3 = addNode( "3", 3, new double[]{4d, 8d, 12d} ); - Spot spot4 = addNode( "4", 4, new double[]{5d, 10d, 15d} ); - Spot spot5 = addNode( "5", 5, new double[]{6d, 12d, 18d} ); - Spot spot6 = addNode( "6", 6, new double[]{0d, 0d, 0d} ); - Spot spot7 = addNode( "7", 7, new double[]{8d, 16d, 24d} ); - Spot spot8 = addNode( "8", 5, new double[]{9d, 18d, 27d} ); - Spot spot10 = addNode( "10", 7, new double[]{11d, 22d, 33d} ); - Spot spot11 = addNode( "11", 3, new double[]{12d, 24d, 36d} ); - Spot spot13 = addNode( "13", 5, new double[]{14d, 28d, 42d} ); + spot0 = addNode( "0", 0, new double[] { 1d, 2d, 3d } ); + spot1 = addNode( "1", 1, new double[] { 0d, 0d, 0d } ); + spot2 = addNode( "2", 2, new double[] { 3d, 6d, 9d } ); + spot3 = addNode( "3", 3, new double[] { 4d, 8d, 12d } ); + spot4 = addNode( "4", 4, new double[] { 5d, 10d, 15d } ); + spot5 = addNode( "5", 5, new double[] { 6d, 12d, 18d } ); + spot6 = addNode( "6", 6, new double[] { 0d, 0d, 0d } ); + spot7 = addNode( "7", 7, new double[] { 8d, 16d, 24d } ); + spot8 = addNode( "8", 5, new double[] { 9d, 18d, 27d } ); + spot10 = addNode( "10", 7, new double[] { 11d, 22d, 33d } ); + spot11 = addNode( "11", 3, new double[] { 12d, 24d, 36d } ); + spot13 = addNode( "13", 5, new double[] { 14d, 28d, 42d } ); addEdge( spot0, spot1 ); addEdge( spot1, spot2 ); diff --git a/src/test/java/org/mastodon/util/MastodonUtils.java b/src/test/java/org/mastodon/util/MastodonUtils.java new file mode 100644 index 000000000..88d78e198 --- /dev/null +++ b/src/test/java/org/mastodon/util/MastodonUtils.java @@ -0,0 +1,166 @@ +/*- + * #%L + * A Mastodon plugin data allows to show the embryo in Blender. + * %% + * Copyright (C) 2022 - 2023 Matthias Arzt + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.mastodon.util; + +import java.io.IOException; + +import javax.swing.WindowConstants; + +import org.mastodon.graph.io.RawGraphIO; +import org.mastodon.grouping.GroupHandle; +import org.mastodon.mamut.MainWindow; +import org.mastodon.mamut.MamutAppModel; +import org.mastodon.mamut.WindowManager; +import org.mastodon.mamut.feature.MamutRawFeatureModelIO; +import org.mastodon.mamut.model.Link; +import org.mastodon.mamut.model.Model; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; +import org.mastodon.mamut.project.MamutProject; +import org.mastodon.mamut.project.MamutProjectIO; +import org.mastodon.model.FocusModel; +import org.mastodon.model.NavigationHandler; +import org.mastodon.model.NavigationListener; +import org.mastodon.model.TimepointModel; +import org.mastodon.model.tag.TagSetModel; +import org.scijava.Context; + +import mpicbg.spim.data.SpimDataException; + +public class MastodonUtils +{ + + private static final boolean LOG_STACK_TRACE = false; + + private MastodonUtils() { + // prevent from instantiation + } + + static Model openMastodonModel( Context context, String projectPath ) + { + try + { + MamutProject project = new MamutProjectIO().load( projectPath ); + final Model model = new Model( project.getSpaceUnits(), project.getTimeUnits() ); + final boolean isNewProject = project.getProjectRoot() == null; + if ( !isNewProject ) + { + try (final MamutProject.ProjectReader reader = project.openForReading()) + { + final RawGraphIO.FileIdToGraphMap idmap = model.loadRaw( reader ); + // Load features. + MamutRawFeatureModelIO.deserialize( context, model, idmap, reader ); + } + catch ( final ClassNotFoundException e ) + { + e.printStackTrace(); + } + } + return model; + } + catch ( IOException e ) + { + throw new RuntimeException( e ); + } + } + + public static WindowManager showGui(String projectPath) { + try { + final WindowManager windowManager = new WindowManager( new Context() ); + windowManager.getProjectManager().open( new MamutProjectIO().load( projectPath ) ); + final MainWindow mainWindow = new MainWindow(windowManager); + mainWindow.setVisible( true ); + mainWindow.setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); + return windowManager; + } catch (IOException | SpimDataException e) { + throw new RuntimeException(e); + } + } + + public static void logMastodonEvents( MamutAppModel appModel ) + { + GroupHandle groupHandle = appModel.getGroupManager().createGroupHandle(); + groupHandle.setGroupId( 0 ); + logNavigationHandle( groupHandle.getModel( appModel.NAVIGATION ) ); + logTimePointModel(groupHandle.getModel( appModel.TIMEPOINT ) ); + logFocusModel(appModel); + logTagSetModel(appModel); + } + + private static void logFocusModel( MamutAppModel appModel ) + { + FocusModel focusModel = appModel.getFocusModel(); + ModelGraph graph = appModel.getModel().getGraph(); + focusModel.listeners().add(() -> { + Spot ref = graph.vertexRef(); + Spot focusedSpot = focusModel.getFocusedVertex( ref ); + log( "FocusModel: focused vertex: " + focusedSpot ); + graph.releaseRef( ref ); + }); + } + + private static void logNavigationHandle( NavigationHandler navigationHandler ) + { + navigationHandler.listeners().add( new NavigationListener() + { + @Override + public void navigateToVertex( Spot vertex ) + { + log( "NavigationHandler: navigate to vertex " + vertex ); + } + + @Override + public void navigateToEdge( Link edge ) + { + log( "NavigationHandler: navigate to edge " + edge ); + } + } ); + } + + private static void logTimePointModel( TimepointModel model ) + { + model.listeners().add( () -> log( "Time point changed: (to " + model.getTimepoint() + ")" ) ); + } + + private static void logTagSetModel( MamutAppModel appModel ) + { + // TODO + Model model = appModel.getModel(); + TagSetModel tagSetModel = model.getTagSetModel(); + tagSetModel.listeners().add( () -> log( "tag set changed" ) ); + } + + private static void log( String text ) + { + System.out.println( text + " " + Thread.currentThread().getName() ); + if( LOG_STACK_TRACE ) + for ( StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace() ) + System.out.println( " " + stackTraceElement ); + } +} diff --git a/src/test/java/org/mastodon/util/TreeUtilsTest.java b/src/test/java/org/mastodon/util/TreeUtilsTest.java new file mode 100644 index 000000000..c57ee389a --- /dev/null +++ b/src/test/java/org/mastodon/util/TreeUtilsTest.java @@ -0,0 +1,150 @@ +package org.mastodon.util; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; +import org.mastodon.collection.RefCollections; +import org.mastodon.collection.RefList; +import org.mastodon.collection.RefSet; +import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2; +import org.mastodon.mamut.model.ModelGraph; +import org.mastodon.mamut.model.Spot; + +/** + * Tests {@link TreeUtils}. + * + * @author Matthias Arzt + */ +public class TreeUtilsTest +{ + /** + * Test {@link TreeUtils#findSelectedSubtreeRoots} on + * {@link ExampleGraph2}. + */ + @Test + public void testFindSelectedSubtreeRoots() + { + // setup + ExampleGraph2 exampleGraph = new ExampleGraph2(); + ModelGraph graph = exampleGraph.getModel().getGraph(); + + RefList< Spot > roots = RefCollections.createRefList( graph.vertices() ); + roots.add( exampleGraph.spot0 ); + + RefSet< Spot > selectedSpots = RefCollections.createRefSet( graph.vertices() ); + selectedSpots.add( exampleGraph.spot4 ); + selectedSpots.add( exampleGraph.spot7 ); + selectedSpots.add( exampleGraph.spot10 ); + + // process + RefList< Spot > result = TreeUtils.findSelectedSubtreeRoots( graph, roots, selectedSpots ); + + // test + assertEquals( 1, result.size() ); + assertEquals( exampleGraph.spot4, result.get( 0 ) ); + } + + /** + * Test {@link TreeUtils#findSelectedSubtreeRoots} on a simple graph + * with a loop. + */ + @Test + public void testFindSelectedSubTreeRoots_dontGetStuckInLoops() + { + // setup + ModelGraph graph = new ModelGraph(); + Spot a = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 0 ); + Spot b = graph.addVertex().init( 1, new double[] { 0, 0, 0 }, 0 ); + graph.addEdge( a, b ); + graph.addEdge( b, a ); // loop + + RefList< Spot > roots = RefCollections.createRefList( graph.vertices() ); + roots.add( a ); + + RefSet< Spot > selectedSpots = RefCollections.createRefSet( graph.vertices() ); + selectedSpots.add( b ); + + // process + RefList< Spot > result = TreeUtils.findSelectedSubtreeRoots( graph, roots, selectedSpots ); + + // test + assertEquals( Collections.singletonList( b ), result ); + } + + @Test + public void testFindRootsOfTheGivenNodes() { + // Example graph: + // a b + // | + // a1 + // / \ + // a2 a3 + + ModelGraph graph = new ModelGraph(); + Spot a = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot a1 = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot a2 = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot a3 = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot b = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + graph.addEdge( a, a1 ).init(); + graph.addEdge( a1, a2 ).init(); + graph.addEdge( a2, a3 ).init(); + + assertEquals( Collections.singleton( a ), TreeUtils.findRootsOfTheGivenNodes( graph, Arrays.asList( a, a1, a2, a3 ) ) ); + assertEquals( Collections.singleton( a ), TreeUtils.findRootsOfTheGivenNodes( graph, Collections.singleton( a3 ) ) ); + assertEquals( Collections.singleton( b ), TreeUtils.findRootsOfTheGivenNodes( graph, Collections.singleton( b ) ) ); + assertEquals( createSet( a, b ), TreeUtils.findRootsOfTheGivenNodes( graph, Arrays.asList( a2, b ) ) ); + } + + @Test + public void testFindRootsOfTheGivenNodes_dontGetStuckInLoops() { + // setup + ModelGraph graph = new ModelGraph(); + Spot a = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot b = graph.addVertex().init( 1, new double[] { 0, 0, 0 }, 0 ); + Spot c = graph.addVertex().init( 1, new double[] { 0, 0, 0 }, 0 ); + graph.addEdge( a, b ); + graph.addEdge( b, c ); + graph.addEdge( c, b ); // loop between b and c + // process + RefSet< Spot > result = TreeUtils.findRootsOfTheGivenNodes( graph, Collections.singleton( c ) ); + // test + assertEquals( Collections.singleton( a ), result ); + } + + @Test + public void testFindRootsOfTheGivenNodes_noTree() { + // Example graph: + // a b f + // \ / \ + // c e + // | + // d + ModelGraph graph = new ModelGraph(); + Spot a = graph.addVertex().init( 0, new double[] { 0, 0, 0 }, 1 ); + Spot b = graph.addVertex().init( 1, new double[] { 0, 0, 0 }, 1 ); + Spot c = graph.addVertex().init( 2, new double[] { 0, 0, 0 }, 1 ); + Spot d = graph.addVertex().init( 3, new double[] { 0, 0, 0 }, 1 ); + Spot e = graph.addVertex().init( 4, new double[] { 0, 0, 0 }, 1 ); + Spot f = graph.addVertex().init( 4, new double[] { 0, 0, 0 }, 1 ); + graph.addEdge( a, c ); + graph.addEdge( b, c ); + graph.addEdge( c, d ); + graph.addEdge( b, e ); + + assertEquals( createSet( a, b ), TreeUtils.findRootsOfTheGivenNodes( graph, Arrays.asList( b, c, d ) ) ); + assertEquals( createSet( a, b ), TreeUtils.findRootsOfTheGivenNodes( graph, Arrays.asList( a, c, d ) ) ); + assertEquals( createSet( a, b ), TreeUtils.findRootsOfTheGivenNodes( graph, Collections.singletonList( e ) ) ); + } + + @SafeVarargs + private static < T > Set< T > createSet( T... elements ) + { + return new HashSet<>( Arrays.asList( elements ) ); + } +}