/*
*
* * Copyright (c) 2016. David Sowerby
* *
* * 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 uk.q3c.util;
import edu.uci.ics.jung.graph.DelegateForest;
import edu.uci.ics.jung.graph.DirectedOrderedSparseMultigraph;
import edu.uci.ics.jung.graph.Forest;
import edu.uci.ics.jung.graph.Tree;
import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A very simple semantic wrapper for the <a href=http://jung.sourceforge.net/site/index.html> Jung</a> library, to
* use the more familiar language of trees. Underneath is it a proper implementation of a graph - there are many
* methods not exposed through this wrapper, but you can access those via {@link #getGraph()}. Uses a {@link
* DirectedOrderedSparseMultigraph} to maintain insertion order
* <p>
* The E (edge) parameter for the underlying graph is a simple Integer.
* <p>
* Originally this implementation used a default DelegateForest which in turn uses a DirectedSparseGraph - this uses
* hash maps, so it would appear that a different hash algorithm is being in Java 8 to Java 7 used, yielding a
* different order for this test case.
* BasicForest previously made no commitment to maintaining insertion order; however, the failure of this case under
* Java 8 suggests that it might be expected to do so. The implementation has therefore been changed to use a
* {@link DirectedOrderedSparseMultigraph} (this uses LinkedHashMaps) to maintain insertion order.
*
* @param <V> the type of object to be contained (the 'node'). Must implement equals
*/
public class BasicForest<V> implements Serializable {
private int edgeCount = 0;
private Forest<V, Integer> graph;
public BasicForest() {
graph = new DelegateForest<V, Integer>(new DirectedOrderedSparseMultigraph<>());
}
public boolean hasChild(V parentNode, V childNode) {
return graph.getParent(childNode)
.equals(parentNode);
}
public V getParent(V childNode) {
return graph.getParent(childNode);
}
/**
* First step is to identify where this branch should join the tree - this is the last node in the branch which is
* already in the tree. The remainder of the branch is then appended to that node. If none of the nodes already
* exist, the first node of the branch is taken as a root node (that is, it has no parent).
*
* @param branch
* @return
*/
public V addBranch(List<V> branch) {
if ((branch == null) || (branch.isEmpty())) {
return null;
}
int startIndex = -1;
for (int i = 0; i < branch.size(); i++) {
if (!this.containsNode(branch.get(i))) {
startIndex = i - 1;
break;
}
}
// no join found
if (startIndex < 0) {
// put the first node in as a root
addNode(branch.get(0));
startIndex = 1;
}
if (startIndex == 0) {
addChild(null, branch.get(0));
startIndex++;
}
for (int j = startIndex; j < branch.size(); j++) {
addChild(branch.get(j - 1), branch.get(j));
}
return branch.get(0);
}
public void addNode(V node) {
graph.addVertex(node);
}
public boolean containsNode(V node) {
return graph.containsVertex(node);
}
/**
* Adds a {@code childNode} to {@code parentNode}. Note that if {@code parentNode} is not already in the tree, it
* will be added - which may mean that you no longer have a single root
*
* @param parentNode
* @param childNode
*/
public void addChild(V parentNode, V childNode) {
if (parentNode == null) {
addNode(childNode);
} else {
graph.addEdge(newEdge(), parentNode, childNode);
}
}
private Integer newEdge() {
edgeCount++;
return edgeCount;
}
/**
* Returns the node contained in the tree which matches (equals) the supplied {@code node}
*
* @param node
* @return
*/
public V getNode(V node) {
Collection<V> x = graph.getVertices();
List<V> list = new ArrayList<V>(x);
int n = list.indexOf(node);
if (n < 0) {
return null;
}
return list.get(n);
}
public List<V> getChildren(V parentNode) {
Collection<V> children = graph.getChildren(parentNode);
List<V> result = new ArrayList<V>();
if (children != null) {
result.addAll(children);
}
return result;
}
/**
* Get all the nodes which are below the {@code parentNode},that is children, children's children etc. The returned
* list includes the {@code parentNode}
*
* @param parentNode
* @return
*/
public List<V> getSubtreeNodes(V parentNode) {
Collection<V> children = graph.getChildren(parentNode);
List<V> list = new ArrayList<V>();
list.add(parentNode);
if (children != null) {
for (V v : children) {
list.addAll(getSubtreeNodes(v));
}
}
return list;
}
/**
* Finds all the leaves for the specified {@code parentNode}, that is, all those with no children;
*
* @param parentNode
* @param leaves
*/
private void findLeaves(V parentNode, List<V> leaves) {
if (leaves == null) {
return;
}
Collection<V> children = graph.getChildren(parentNode);
if (children == null) {
return;
}
if (children.isEmpty()) {
leaves.add(parentNode);
} else {
for (V v : children) {
findLeaves(v, leaves);
}
}
}
/**
* Finds all the leaves for the whole tree, that is, all those with no children, from the root of the tree. Use
* {@link #findLeaves(Object)} if you want leaves for a subset of the tree
*
* @see #findLeaves(Object)
*/
public List<V> findLeaves() {
List<V> leaves = new ArrayList<V>();
findLeaves(getRoot(), leaves);
return leaves;
}
public List<V> findLeaves(V parentNode) {
List<V> leaves = new ArrayList<V>();
findLeaves(parentNode, leaves);
return leaves;
}
/**
* Returns a list of all the entries in the tree
*
* @return
*/
public Collection<V> getEntries() {
return graph.getVertices();
}
public void clear() {
graph = new DelegateForest<V, Integer>();
edgeCount = 0;
}
/**
* Returns a list of all the roots - the entry which is at the start of each chain or branch. For the tree to be a
* tree, there should only be one of these
*
* @return
*/
public List<V> getRoots() {
Collection<Tree<V, Integer>> t = graph.getTrees();
List<V> branchRoots = new ArrayList<V>();
for (Tree<V, Integer> branch : t) {
branchRoots.add(branch.getRoot());
}
return branchRoots;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
for (V rootNode : getRoots()) {
buf.append('\n');
text(rootNode, buf, 0);
}
return buf.toString();
}
public void text(V node, StringBuilder buf, int level) {
String indent = StringUtils.repeat("-", level + 1);
buf.append(indent);
buf.append(node);
buf.append('\n');
for (V child : getChildren(node)) {
text(child, buf, level + 1);
}
}
public boolean hasChildren(V parentNode) {
return getChildCount(parentNode) > 0;
}
public int getChildCount(V parentNode) {
return graph.getChildCount(parentNode);
}
/**
* Assumes this is a genuine tree and that there is only one root, or just takes the first one
*
* @return
*/
public V getRoot() {
if (getRoots().isEmpty()) {
return null;
} else {
return getRoots().get(0);
}
}
public int getNodeCount() {
return graph.getVertexCount();
}
public Forest<V, Integer> getGraph() {
return graph;
}
/**
* Uses a list to return all vertices, but no ordering should be implied
*
* @return
*/
public List<V> getAllNodes() {
return new ArrayList<V>(graph.getVertices());
}
public V getRootFor(V node) {
if (node == null) {
return null;
}
V nut = node;
while (true) {
V parent = graph.getParent(nut);
if (parent == null) {
break;
} else {
nut = parent;
}
}
return nut;
}
public void removeNode(V node) {
graph.removeVertex(node);
}
/**
* Useful for immutable vertices, this method replaced the current vertex with a new vertex. To do so, it has to deep copy to a subgraph, and then copy
* back, so not very efficient. This method is made necessary by Jung's approach to maintaining the integrity of the map - so far I have not found a way
* to move an edge from one vertex to another without the associated vertices being deleted. https://github.com/davidsowerby/krail/issues/397
*
* @param currentVertex the vertex to be replaced
* @param newVertex the vertex to replace it with
*/
public void replaceNode(@Nonnull V currentVertex, @Nonnull V newVertex) {
checkNotNull(currentVertex);
checkNotNull(newVertex);
V parentVertex = getParent(currentVertex);
BasicForest<V> subGraph = subGraph(currentVertex, newVertex);
graph.removeVertex(currentVertex);
addChild(parentVertex, newVertex);
mergeSubGraph(subGraph, parentVertex);
}
/**
* Assumes a single root
*
* @param sourceSubGraph
* @param targetParentVertex
*/
private void mergeSubGraph(BasicForest<V> sourceSubGraph, V targetParentVertex) {
if (sourceSubGraph.getNodeCount() > 0) {
V sourceSubGraphRoot = sourceSubGraph.getRoot();
addChild(targetParentVertex, sourceSubGraphRoot);
copyChildren(sourceSubGraph, sourceSubGraphRoot, this, sourceSubGraph.getRoot());
}
}
/**
* Returns a BasicForest with a full depth sub-graph of {@code root}, but with {@code root} itself replaced by {@code newRoot}
*
* @param root
* @param newRoot
* @return
*/
private BasicForest<V> subGraph(V root, V newRoot) {
BasicForest<V> subGraph = new BasicForest<V>();
copyChildren(this, root, subGraph, newRoot);
return subGraph;
}
private void copyChildren(BasicForest<V> sourceGraph, V sourceParentVertex, BasicForest<V> targetGraph, V targetParentVertex) {
final List<V> children = sourceGraph.getChildren(sourceParentVertex);
if (children != null) {
for (V child : children) {
targetGraph.addChild(targetParentVertex, child);
copyChildren(sourceGraph, child, targetGraph, child);
}
}
}
}