package nodebox.node;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import nodebox.graphics.Point;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.*;
public final class Node {
public static String path(String parentPath, Node node) {
checkNotNull(node);
return path(parentPath, node.getName());
}
public static String path(String parentPath, String nodeName) {
checkNotNull(parentPath);
checkNotNull(nodeName);
checkArgument(parentPath.startsWith("/"), "Only absolute paths are supported.");
if (nodeName.isEmpty()) return parentPath;
if (parentPath.equals("/")) {
return "/" + nodeName;
} else {
return Joiner.on("/").join(parentPath, nodeName);
}
}
private enum Nodes {ROOT_NODE, NETWORK_NODE}
public enum Attribute {PROTOTYPE, NAME, COMMENT, CATEGORY, DESCRIPTION, IMAGE, FUNCTION, POSITION, INPUTS, OUTPUT_TYPE, OUTPUT_RANGE, IS_NETWORK, CHILDREN, RENDERED_CHILD_NAME, CONNECTIONS, HANDLE, ALWAYS_RENDERED}
/**
* Check if data from the output node can be converted and used in the input port.
* <p/>
* The relation is not commutative:
* an output port that can be converted to an input port does not imply the reverse.
*
* @param outputNode The output node
* @param inputPort The input port
* @return true if the input port is compatible
*/
public static boolean isCompatible(Node outputNode, Port inputPort) {
checkNotNull(outputNode);
checkNotNull(inputPort);
return isCompatible(outputNode.getOutputType(), inputPort.getType());
}
/**
* Check if data from the output can be converted and used in the input.
* <p/>
* The relation is not commutative:
* an output port that can be converted to an input port does not imply the reverse.
*
* @param outputType The type of the output port of the upstream node
* @param inputType The type of the input port of the downstream node
* @return true if the types are compatible
*/
public static boolean isCompatible(String outputType, String inputType) {
checkNotNull(outputType);
checkNotNull(inputType);
// If the output and input type are the same, they are compatible.
if (outputType.equals(inputType)) return true;
// Everything can be converted to a string.
if (inputType.equals(Port.TYPE_STRING)) return true;
// Integers can be converted to floating-point numbers without loss of information.
if (outputType.equals(Port.TYPE_INT) && inputType.equals(Port.TYPE_FLOAT)) return true;
// Floating-point numbers can be converted to integers: they are rounded.
if (outputType.equals(Port.TYPE_FLOAT) && inputType.equals(Port.TYPE_INT)) return true;
// A number can be converted to a point: both X and Y then get the same value.
if (outputType.equals(Port.TYPE_INT) && inputType.equals(Port.TYPE_POINT)) return true;
if (outputType.equals(Port.TYPE_FLOAT) && inputType.equals(Port.TYPE_POINT)) return true;
// If none of these tests pass, the types are not compatible.
return false;
}
private static final Pattern NODE_NAME_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]{0,29}$");
private static final Pattern DOUBLE_UNDERSCORE_PATTERN = Pattern.compile("^__.*$");
private static final Pattern NUMBER_AT_THE_END = Pattern.compile("^(.*?)(\\d*)$");
private static final Pattern UNDERSCORE_NUMBER_AT_THE_END = Pattern.compile("^(.*?)((\\_(\\d*))?)$");
private static final Pattern RESERVED_WORD_PATTERN = Pattern.compile("^(node|network)$");
public static final Node ROOT = new Node(Nodes.ROOT_NODE);
public static final Node NETWORK = new Node(Nodes.NETWORK_NODE);
public static final Map<String, Node> coreNodes;
static {
ImmutableMap.Builder<String, Node> builder = new ImmutableMap.Builder<String, Node>();
builder.put("ROOT", ROOT);
builder.put("NETWORK", NETWORK);
coreNodes = builder.build();
}
private final Node prototype;
private final String name;
private final String comment;
private final String category;
private final String description;
private final String image;
private final String function;
private final Point position;
private final ImmutableList<Port> inputs;
private final String outputType;
private final Port.Range outputRange;
private final boolean isNetwork;
private final ImmutableList<Node> children;
private final String renderedChildName;
private final ImmutableList<Connection> connections;
private final String handle;
private final boolean isAlwaysRendered;
private final int hashCode;
//// Constructors ////
/**
* Constructor for the root and network nodes. This can only be called once for each of them.
*/
private Node(Nodes coreNode) {
switch (coreNode) {
case ROOT_NODE:
default:
checkState(ROOT == null, "You cannot create more than one root node.");
prototype = null;
name = "node";
comment = "";
description = "Base node to be extended for custom nodes.";
image = "node.png";
outputRange = Port.DEFAULT_RANGE;
isNetwork = false;
break;
case NETWORK_NODE:
checkState(ROOT != null, "The root node has not been created yet.");
checkState(NETWORK == null, "You cannot create more than one network node.");
prototype = ROOT;
name = "network";
comment = "";
image = "network.png";
description = "Create an empty subnetwork.";
outputRange = Port.Range.LIST;
isNetwork = true;
break;
}
category = "";
function = "core/zero";
position = Point.ZERO;
inputs = ImmutableList.of();
outputType = Port.TYPE_FLOAT;
children = ImmutableList.of();
renderedChildName = "";
connections = ImmutableList.of();
handle = "";
isAlwaysRendered = false;
hashCode = calcHashCode();
}
private void checkAllNotNull(Object... args) {
for (Object o : args) {
checkNotNull(o);
}
}
private Node(Node prototype, String name, String comment, String category, String description, String image, String function,
Point position, ImmutableList<Port> inputs,
String outputType, Port.Range outputRange, boolean isNetwork, ImmutableList<Node> children,
String renderedChildName, ImmutableList<Connection> connections, String handle, boolean isAlwaysRendered) {
checkAllNotNull(prototype, name, description, image, function,
position, inputs, outputType, children,
renderedChildName, connections);
checkArgument(!name.equals("node"), "The name node is a reserved internal name.");
checkArgument(!name.equals("network"), "The name network is a reserved internal name.");
this.prototype = prototype;
this.name = name;
this.comment = comment;
this.category = category;
this.description = description;
this.image = image;
this.function = function;
this.position = position;
this.inputs = inputs;
this.outputType = outputType;
this.outputRange = outputRange;
this.isNetwork = isNetwork;
this.children = children;
this.renderedChildName = renderedChildName;
this.connections = connections;
this.handle = handle;
this.isAlwaysRendered = isAlwaysRendered;
this.hashCode = calcHashCode();
}
private int calcHashCode() {
return Objects.hashCode(prototype, name, comment, category, description, image, function, position,
inputs, outputType, outputRange, isNetwork, children, renderedChildName, connections, handle, isAlwaysRendered);
}
//// Getters ////
public Node getPrototype() {
return prototype;
}
public String getName() {
return name;
}
public String getComment() {
return comment;
}
public boolean hasComment() {
return comment != null && !comment.trim().isEmpty();
}
public String getCategory() {
return category;
}
public String getDescription() {
return description;
}
public String getImage() {
return image;
}
public String getFunction() {
return function;
}
public Point getPosition() {
return position;
}
public boolean isNetwork() {
return isNetwork;
}
public boolean isAlwaysRendered() {
return isAlwaysRendered;
}
public boolean hasChildren() {
return !children.isEmpty();
}
public ImmutableList<Node> getChildren() {
return children;
}
public Node getChild(String name) {
checkNotNull(name, "Name cannot be null.");
for (Node child : getChildren()) {
if (child.getName().equals(name)) {
return child;
}
}
return null;
}
public boolean hasChild(String name) {
checkNotNull(name, "Name cannot be null.");
return getChild(name) != null;
}
public boolean hasChild(Node node) {
checkNotNull(node, "Node cannot be null.");
return children.contains(node);
}
public boolean isEmpty() {
return children.isEmpty();
}
public List<Port> getInputs() {
return inputs;
}
public Port getInput(String name) {
checkNotNull(name, "Port name cannot be null.");
for (Port p : getInputs()) {
if (p.getName().equals(name)) {
return p;
}
}
return null;
}
public ImmutableList<Port> getInputsOfType(String type) {
ImmutableList.Builder<Port> b = ImmutableList.builder();
for (Port p : getInputs()) {
if (p.getType().equals(type)) {
b.add(p);
}
}
return b.build();
}
public boolean hasInput(String name) {
return getInput(name) != null;
}
public String getOutputType() {
return outputType;
}
public Port.Range getOutputRange() {
return outputRange;
}
public boolean hasValueOutputRange() {
return outputRange.equals(Port.Range.VALUE);
}
public boolean hasListOutputRange() {
return outputRange.equals(Port.Range.LIST);
}
public boolean hasListInputs() {
for (Port port : getInputs()) {
if (port.hasListRange())
return true;
}
return false;
}
/**
* Get the name of the rendered child. This node is guaranteed to exist as a child on the network.
* The rendered child name can be null, indicating no child node will be rendered.
*
* @return the name of the rendered child or null.
*/
public String getRenderedChildName() {
return renderedChildName;
}
/**
* Get the rendered child Node.
*
* @return The rendered child node or null if none is set.
*/
public Node getRenderedChild() {
if (getRenderedChildName().isEmpty()) return null;
Node renderedChild = getChild(getRenderedChildName());
checkNotNull(renderedChild, "The child with name %s cannot be found. This is a bug in NodeBox.", getRenderedChildName());
return renderedChild;
}
public boolean hasRenderedChild() {
return hasChild(getRenderedChildName());
}
public List<Connection> getConnections() {
return connections;
}
public String getHandle() {
return handle;
}
public Object getAttributeValue(Attribute attribute) {
if (attribute == Attribute.PROTOTYPE) {
return getPrototype();
} else if (attribute == Attribute.NAME) {
return getName();
} else if (attribute == Attribute.COMMENT) {
return getComment();
} else if (attribute == Attribute.CATEGORY) {
return getCategory();
} else if (attribute == Attribute.DESCRIPTION) {
return getDescription();
} else if (attribute == Attribute.IMAGE) {
return getImage();
} else if (attribute == Attribute.FUNCTION) {
return getFunction();
} else if (attribute == Attribute.POSITION) {
return getPosition();
} else if (attribute == Attribute.INPUTS) {
return getInputs();
} else if (attribute == Attribute.OUTPUT_TYPE) {
return getOutputType();
} else if (attribute == Attribute.OUTPUT_RANGE) {
return getOutputRange();
} else if (attribute == Attribute.IS_NETWORK) {
return isNetwork();
} else if (attribute == Attribute.CHILDREN) {
return getChildren();
} else if (attribute == Attribute.RENDERED_CHILD_NAME) {
return getRenderedChildName();
} else if (attribute == Attribute.CONNECTIONS) {
return getConnections();
} else if (attribute == Attribute.HANDLE) {
return getHandle();
} else {
throw new AssertionError("Unknown node attribute " + attribute);
}
}
public String uniqueName(String prefix) {
Matcher m = NUMBER_AT_THE_END.matcher(prefix);
m.find();
String namePrefix = m.group(1);
String number = m.group(2);
int counter;
if (number.length() > 0) {
counter = Integer.parseInt(number);
} else {
counter = 1;
}
while (true) {
String suggestedName = namePrefix + counter;
if (!hasChild(suggestedName)) {
// We don't use rename here, since it assumes the node will be in
// this network.
return suggestedName;
}
++counter;
}
}
public String uniqueInputName(String prefix) {
Matcher m = UNDERSCORE_NUMBER_AT_THE_END.matcher(prefix);
m.find();
String namePrefix = m.group(1);
String number = m.group(4);
int counter;
if (number != null && number.length() > 0) {
counter = Integer.parseInt(number);
} else {
counter = 1;
}
while (true) {
String suggestedName = namePrefix + "_" + counter;
if (!hasInput(suggestedName)) {
return suggestedName;
}
++counter;
}
}
//// Mutation functions ////
/**
* Create a new node with this node as the prototype.
*
* @return The new node.
*/
public Node extend() {
return newNodeWithAttribute(Attribute.PROTOTYPE, this);
}
/**
* Checks if the given name would be valid for this node.
*
* @param name the name to check.
* @throws InvalidNameException if the name was invalid.
*/
public static void validateName(String name) throws InvalidNameException {
Matcher m1 = NODE_NAME_PATTERN.matcher(name);
Matcher m2 = DOUBLE_UNDERSCORE_PATTERN.matcher(name);
Matcher m3 = RESERVED_WORD_PATTERN.matcher(name);
if (!m1.matches()) {
throw new InvalidNameException(null, name, "Names can only contain lowercase letters, numbers, and the underscore. Names cannot be longer than 29 characters.");
}
if (m2.matches()) {
throw new InvalidNameException(null, name, "Names starting with double underscore are reserved for internal use.");
}
if (m3.matches()) {
throw new InvalidNameException(null, name, "Names cannot be a reserved word (network, node).");
}
}
/**
* Create a new node with the given name.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param name The new node name.
* @return A new Node.
*/
public Node withName(String name) {
validateName(name);
return newNodeWithAttribute(Attribute.NAME, name);
}
/**
* Create a new node with the given comment.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param comment The new node comment.
* @return A new Node.
*/
public Node withComment(String comment) {
return newNodeWithAttribute(Attribute.COMMENT, comment);
}
/**
* Create a new node with the given category.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param category new node category.
* @return A new Node.
*/
public Node withCategory(String category) {
return newNodeWithAttribute(Attribute.CATEGORY, category);
}
/**
* Create a new node with the given description.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param description new node description.
* @return A new Node.
*/
public Node withDescription(String description) {
return newNodeWithAttribute(Attribute.DESCRIPTION, description);
}
/**
* Create a new node with the given image.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param image new node image.
* @return A new Node.
*/
public Node withImage(String image) {
return newNodeWithAttribute(Attribute.IMAGE, image);
}
/**
* Create a new node with the given function identifier.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param function The new function identifier.
* @return A new Node.
*/
public Node withFunction(String function) {
return newNodeWithAttribute(Attribute.FUNCTION, function);
}
/**
* Create a new node with the given position.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param position The new position.
* @return A new Node.
*/
public Node withPosition(Point position) {
return newNodeWithAttribute(Attribute.POSITION, position);
}
/**
* Create a new node with the given input port added.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param port The port to add.
* @return A new Node.
*/
public Node withInputAdded(Port port) {
checkNotNull(port, "Port cannot be null.");
checkArgument(!hasInput(port.getName()), "An input port named %s already exists on node %s.", port.getName(), this);
ImmutableList.Builder<Port> b = ImmutableList.builder();
b.addAll(getInputs());
b.add(port);
return newNodeWithAttribute(Attribute.INPUTS, b.build());
}
/**
* Create a new node with the given input port removed.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param portName The name of the port to remove.
* @return A new Node.
*/
public Node withInputRemoved(String portName) {
Port portToRemove = getInput(portName);
checkArgument(portToRemove != null, "Input port %s does not exist on node %s.", portName, this);
ImmutableList.Builder<Port> b = ImmutableList.builder();
for (Port port : getInputs()) {
if (portToRemove != port)
b.add(port);
}
return newNodeWithAttribute(Attribute.INPUTS, b.build());
}
/**
* Create a new node with the given child node renamed.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param childName The name of the child node to rename.
* @param newName The new name of the child node.
* @return A new Node.
*/
public Node withChildRenamed(String childName, String newName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(!newName.equals("root"), "A child node of a network cannot have the name 'root'.");
if (childName.equals(newName)) return this;
Node newNode = getChild(childName).withName(newName);
Node newParent = withChildRemoved(childName).withChildAdded(newNode);
if (renderedChildName.equals(childName))
newParent = newParent.withRenderedChild(newNode);
if (hasPublishedChildInputs(childName)) {
ImmutableList.Builder<Port> b = ImmutableList.builder();
for (Port p : inputs) {
if (p.getChildNodeName().equals(childName)) {
b.add(Port.publishedPort(newNode, newNode.getInput(p.getChildPortName()), p.getName()));
} else
b.add(p);
}
newParent = newParent.newNodeWithAttribute(Attribute.INPUTS, b.build());
}
for (Connection c : getConnections()) {
if (c.getInputNode().equals(childName)) {
newParent = newParent.connect(c.getOutputNode(), newName, c.getInputPort());
} else if (c.getOutputNode().equals(childName)) {
newParent = newParent.connect(newName, c.getInputNode(), c.getInputPort());
}
}
return newParent;
}
/**
* Create a new node with a comment added to the given child.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param childName The name of the child node to comment.
* @param comment The new comment of the child node.
* @return A new Node.
*/
public Node withChildCommented(String childName, String comment) {
Node newNode = getChild(childName).withComment(comment);
return withChildReplaced(childName, newNode);
}
/**
* Create a new node with the given child input port removed.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param childName The name of the child node to which the child port belongs to.
* @param portName The name of the child port to remove.
* @return A new Node.
*/
public Node withChildInputRemoved(String childName, String portName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(childName), "Node %s does not have a child named %s.", this, childName);
Node child = getChild(childName);
checkArgument(child.hasInput(portName), "Node %s does not have an input port %s.", childName, portName);
if (hasPublishedInput(childName, portName))
return unpublish(childName, portName).withChildInputRemoved(childName, portName);
if (isConnected(childName, portName))
return disconnect(childName, portName).withChildInputRemoved(childName, portName);
return withChildReplaced(childName, child.withInputRemoved(portName));
}
private Node withChildInputChanged(String childName, String portName, Port newPort) {
// todo: checks
checkArgument(isNetwork(), "Node %s is not a network node.", this);
return withChildReplaced(childName, getChild(childName).withInputChanged(portName, newPort));
}
public Node withChildPositionChanged(String childName, double xOffset, double yOffset) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(childName), "Node %s does not have a child named %s.", this, childName);
Node child = getChild(childName);
return withChildReplaced(childName, child.withPosition(child.getPosition().moved(xOffset, yOffset)));
}
/**
* Create a new node with the given input port replaced.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param portName The name of the port to replace.
* @param newPort The new Port instance.
* @return A new Node.
*/
public Node withInputChanged(String portName, Port newPort) {
Port oldPort = getInput(portName);
checkNotNull(oldPort, "Input port %s does not exist on node %s.", portName, this);
ImmutableList.Builder<Port> b = ImmutableList.builder();
// Add all ports back in the correct order.
for (Port port : getInputs()) {
if (port == oldPort) {
b.add(newPort);
} else {
b.add(port);
}
}
return newNodeWithAttribute(Attribute.INPUTS, b.build());
}
/**
* Create a new node with the given input port set to a new value.
* Only standard port types (int, float, string, point) can have their value set.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param portName The name of the port to set.
* @param value The new Port value.
* @return A new Node.
*/
public Node withInputValue(String portName, Object value) {
Port p = getInput(portName);
checkNotNull(p, "Input port %s does not exist on node %s.", portName, this);
p = p.withValue(value);
Node n = this;
if (isNetwork() && p.isPublishedPort()) {
String childNodeName = p.getChildNodeName();
Node newChildNode = p.getChildNode(this).withInputValue(p.getChildPortName(), value);
n = n.withChildReplaced(childNodeName, newChildNode);
}
return n.withInputChanged(portName, p);
}
/**
* Create a new node with the given input port set to the new range.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param portName The name of the port to set.
* @param range The new range.
* @return A new Node.
*/
public Node withInputRange(String portName, Port.Range range) {
Port p = getInput(portName);
checkNotNull(p, "Input port %s does not exist on node %s.", portName, this);
p = p.withRange(range);
return withInputChanged(portName, p);
}
public List<Port> getPublishedPorts() {
if (!isNetwork()) return ImmutableList.of();
ImmutableList.Builder<Port> b = ImmutableList.builder();
for (Port p : inputs) {
if (p.isPublishedPort())
b.add(p);
}
return b.build();
}
public Port getPublishedPort(Port port) {
if (!isNetwork()) return null;
checkArgument(port.isPublishedPort(), "Given port %s is not a published port.", port);
return port.getChildPort(this);
}
public Port getPublishedPort(String publishedPortName) {
if (!isNetwork()) return null;
checkArgument(hasInput(publishedPortName), "Given port %s does not exist.", publishedPortName);
return getPublishedPort(getInput(publishedPortName));
}
public Port getPortByChildReference(Node childNode, Port childPort) {
return getPortByChildReference(childNode.getName(), childPort.getName());
}
public Port getPortByChildReference(String childNodeName, String childPortName) {
if (!isNetwork()) return null;
for (Port p : inputs) {
if (p.isPublishedPort() && p.getChildNodeName().equals(childNodeName) && p.getChildPortName().equals(childPortName)) {
return p;
}
}
return null;
}
public boolean hasPublishedInput(String childNodeName, String childPortName) {
if (!isNetwork()) return false;
for (Port p : inputs) {
if (p.isPublishedPort() && p.getChildNodeName().equals(childNodeName) && p.getChildPortName().equals(childPortName)) {
return true;
}
}
return false;
}
public boolean hasPublishedChildInputs(String childNodeName) {
if (!isNetwork()) return false;
for (Port p : inputs) {
if (p.isPublishedPort() && p.getChildNodeName().equals(childNodeName)) {
return true;
}
}
return false;
}
public boolean hasPublishedInput(String publishedName) {
if (!isNetwork()) return false;
return hasInput(publishedName) && getInput(publishedName).isPublishedPort();
}
/**
* Create a new node with the given child input node/port published.
*
* @param childNodeName The name of the child input Node.
* @param childPortName The name of the child input Port.
* @param publishedName The name of by which the published port is known.
* @return A new Node.
*/
public Node publish(String childNodeName, String childPortName, String publishedName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkNotNull(publishedName, "Published name cannot be null.");
checkArgument(hasChild(childNodeName), "Node %s does not have a child named %s.", this, childNodeName);
Node childNode = getChild(childNodeName);
checkArgument(childNode.hasInput(childPortName), "Child node %s does not have an child node port %s.", childNodeName, childPortName);
Port childPort = childNode.getInput(childPortName);
checkArgument(!hasPublishedInput(childNodeName, childPortName), "The port %s on node %s has already been published.", childPortName, childNodeName);
checkArgument(!hasInput(publishedName), "Node %s already has an childNode named %s.", this, publishedName);
for (Connection c : getConnections()) {
if (c.getInputNode().equals(childNodeName) && c.getInputPort().equals(childPortName))
return disconnect(c).publish(childNodeName, childPortName, publishedName);
}
Port newPort = Port.publishedPort(childNode, childPort, publishedName);
return withInputAdded(newPort);
}
public Node unpublish(String childNodeName, String childPortName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(childNodeName), "Node %s does not have a child named %s.", this, childNodeName);
Node childNode = getChild(childNodeName);
checkArgument(childNode.hasInput(childPortName), "Child node %s does not have a port named %s.", childNodeName, childPortName);
Port childPort = childNode.getInput(childPortName);
Port p = getPortByChildReference(childNode, childPort);
return withInputRemoved(p.getName());
}
public Node unpublishChildNode(Node childNode) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(childNode), "Node %s does not have a child named %s.", this, childNode);
Node n = this;
for (Port p : getPublishedPorts()) {
if (p.getChildNodeName().equals(childNode.getName())) {
n = n.withInputRemoved(p.getName());
}
}
return n;
}
public Node unpublish(String publishedName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
return withInputRemoved(publishedName);
}
/**
* Create a new node with the given output type.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param outputType The new output type.
* @return A new Node.
*/
public Node withOutputType(String outputType) {
return newNodeWithAttribute(Attribute.OUTPUT_TYPE, outputType);
}
/**
* Create a new node with the output range set to the given value.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param outputRange The new output range.
* @return A new Node.
*/
public Node withOutputRange(Port.Range outputRange) {
return newNodeWithAttribute(Attribute.OUTPUT_RANGE, outputRange);
}
/**
* Create a new node with the given child added.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param node The child node to add.
* @return A new Node.
*/
public Node withChildAdded(Node node) {
checkNotNull(node, "Node cannot be null.");
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(!node.getName().equals("root"), "A child node of a network cannot have the name 'root'.");
if (hasChild(node.getName())) {
String uniqueName = uniqueName(node.getName());
node = node.withName(uniqueName);
}
ImmutableList.Builder<Node> b = ImmutableList.builder();
b.addAll(getChildren());
b.add(node);
return newNodeWithAttribute(Attribute.CHILDREN, b.build());
}
/**
* Create a new node with the given child removed.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param childName The name of the child node to remove.
* @return A new Node.
*/
public Node withChildRemoved(String childName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
Node childToRemove = getChild(childName);
checkArgument(childToRemove != null, "Node %s is not a child of node %s.", childName, this);
if (hasPublishedChildInputs(childName))
return unpublishChildNode(childToRemove).withChildRemoved(childName);
if (isConnected(childName))
return disconnect(childName).withChildRemoved(childName);
if (renderedChildName.equals(childName))
return withRenderedChild(null).withChildRemoved(childName);
ImmutableList.Builder<Node> b = ImmutableList.builder();
for (Node child : getChildren()) {
if (child != childToRemove)
b.add(child);
}
return newNodeWithAttribute(Attribute.CHILDREN, b.build());
}
/**
* Checks if a new node of which the given node would become a new child node is internally
* consistent with the published inputs it already has, for example if the network
* still exposes a child port that was removed from the candidate node.
*
* @param childName The name of the child node to be replaced
* @param newChild The new candidate node
* @return true if the resulting network would be internally consistent.
*/
private boolean isConsistentWithPublishedInputs(String childName, Node newChild) {
// TODO Implement
return true;
// for (PublishedPort pp : publishedInputs) {
// if (pp.getChildNode().equals(childName)) {
// if (!newChild.hasInput(pp.getChildPort()))
// return false;
// }
// }
// return true;
}
/**
* Checks if a new node of which the given node would become a new child node is internally
* consistent with the connections it already has, for example if the network
* still exposes a child port that was removed from the candidate node.
*
* @param childName The name of the child node to be replaced
* @param newChild The new candidate node
* @return true if the resulting network would be internally consistent.
*/
private boolean isConsistentWithConnections(String childName, Node newChild) {
for (Connection c : connections) {
if (c.getInputNode().equals(childName)) {
if (!newChild.hasInput(c.getInputPort()))
return false;
}
}
return true;
}
/**
* Create a new node of which the published inputs are consistent with
* the given node if the given node would become a new child of this node.
* Note that the given child node is NOT added as a new child on this node.
*
* @param childName The name of the child node to be replaced
* @param newChild The candidate node
* @return A new node
*/
private Node withConsistentPublishedInputs(String childName, Node newChild) {
// TODO Implement
return this;
}
/**
* Create a new node of which the connections are consistent with
* the given node if the given node would become a new child of this node.
* Note that the given child node is NOT added as a new child on this node.
*
* @param childName The name of the child node to be replaced
* @param newChild The candidate node
* @return A new node
*/
private Node withConsistentConnections(String childName, Node newChild) {
ImmutableList.Builder<Connection> b = ImmutableList.builder();
for (Connection c : connections) {
if (c.getInputNode().equals(childName)) {
if (newChild.hasInput(c.getInputPort()))
b.add(c);
} else
b.add(c);
}
return newNodeWithAttribute(Attribute.CONNECTIONS, b.build());
}
/**
* Create a new node with the child replaced by the given node.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param childName The name of the child node to replace.
* @param newChild The new child node.
* @return A new Node.
*/
public Node withChildReplaced(String childName, Node newChild) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
Node childToReplace = getChild(childName);
checkNotNull(newChild);
checkArgument(newChild.getName().equals(childName), "New child %s does not have the same name as old child %s.", newChild, childName);
checkArgument(childToReplace != null, "Node %s is not a child of node %s.", childName, this);
if (!isConsistentWithPublishedInputs(childName, newChild))
return withConsistentPublishedInputs(childName, newChild)
.withChildReplaced(childName, newChild);
if (!isConsistentWithConnections(childName, newChild))
return withConsistentConnections(childName, newChild)
.withChildReplaced(childName, newChild);
ImmutableList.Builder<Node> b = ImmutableList.builder();
for (Node child : getChildren()) {
if (child != childToReplace) {
b.add(child);
} else {
b.add(newChild);
}
}
return newNodeWithAttribute(Attribute.CHILDREN, b.build());
}
/**
* Create a new node with the given child set as rendered.
* <p/>
* The rendered node should exist as a child on this node.
* If you don't want a child node to be rendered, set it to an empty string ("").
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param name The new rendered child.
* @return A new Node.
*/
public Node withRenderedChildName(String name) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkNotNull(name, "Rendered child name cannot be null.");
checkArgument(name.isEmpty() || hasChild(name), "Node does not have a child named %s.", name);
return newNodeWithAttribute(Attribute.RENDERED_CHILD_NAME, name);
}
/**
* Create a new node with the given child set as rendered.
* <p/>
* The rendered node should exist as a child on this node.
* If you don't want a child node to be rendered, set it to null.
* <p/>
* If you call this on ROOT, extend() is called implicitly.
*
* @param renderedChild The new rendered child or null if you don't want anything rendered.
* @return A new Node.
*/
public Node withRenderedChild(Node renderedChild) {
return withRenderedChildName(renderedChild == null ? "" : renderedChild.getName());
}
/**
* Create a new node that connects the given child nodes.
*
* @param outputNode The name of the output (upstream) Node.
* @param inputNode The name of the input (downstream) Node.
* @param inputPort The name of the input (downstream) Port.
* @return A new Node.
*/
public Node connect(String outputNode, String inputNode, String inputPort) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(outputNode), "Node %s does not have a child named %s.", this, outputNode);
checkArgument(hasChild(inputNode), "Node %s does not have a child named %s.", this, inputNode);
Node input = getChild(inputNode);
checkArgument(input.hasInput(inputPort), "Node %s does not have an input port %s.", inputNode, inputPort);
checkArgument(!hasPublishedInput(inputNode, inputPort), "Node %s has a published input for port %s of child %s.", this, inputNode, inputPort);
Connection newConnection = new Connection(outputNode, inputNode, inputPort);
ImmutableList.Builder<Connection> b = ImmutableList.builder();
for (Connection c : getConnections()) {
if (c.getInputNode().equals(inputNode) && c.getInputPort().equals(inputPort)) {
// There was already a connection, on this input port.
// We "disconnect" it by not including it in the new list.
} else {
b.add(c);
}
}
b.add(newConnection);
return newNodeWithAttribute(Attribute.CONNECTIONS, b.build());
}
/**
* Create a new node with the given connection removed.
*
* @param connection The connection to remove.
* @return A new Node.
*/
public Node disconnect(Connection connection) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(getConnections().contains(connection), "Node %s does not have a connection %s", this, connection);
ImmutableList.Builder<Connection> b = ImmutableList.builder();
for (Connection c : getConnections()) {
if (c != connection)
b.add(c);
}
return newNodeWithAttribute(Attribute.CONNECTIONS, b.build());
}
/**
* Create a new node with all existing connections of the given child node removed.
*
* @param node The node of which to remove all the connections.
* @return A new Node.
*/
public Node disconnect(String node) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(node), "Node %s does not have a child named %s.", this, node);
ImmutableList.Builder<Connection> b = ImmutableList.builder();
for (Connection c : getConnections()) {
if (c.getInputNode().equals(node) || c.getOutputNode().equals(node)) {
// The node is part of this connection,
// so don't include it in the new list.
} else {
b.add(c);
}
}
return newNodeWithAttribute(Attribute.CONNECTIONS, b.build());
}
/**
* Create a new node with a connection to the given child/port removed.
*
* @param node The node of which to remove the connection.
* @param portName The port of which to remove the connection.
* @return A new Node.
*/
public Node disconnect(String node, String portName) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
checkArgument(hasChild(node), "Node %s does not have a child named %s.", this, node);
Node child = getChild(node);
checkArgument(child.hasInput(portName), "Node %s does not have an input port %s.", node, portName);
ImmutableList.Builder<Connection> b = ImmutableList.builder();
for (Connection c : getConnections()) {
if (c.getInputNode().equals(node) && c.getInputPort().equals(portName)) {
} else {
b.add(c);
}
}
return newNodeWithAttribute(Attribute.CONNECTIONS, b.build());
}
public Node withConnectionAdded(Connection connection) {
return connect(connection.getOutputNode(), connection.getInputNode(), connection.getInputPort());
}
public boolean isConnected(String node) {
if (!isNetwork()) return false;
for (Connection c : getConnections()) {
if (c.getInputNode().equals(node) || c.getOutputNode().equals(node))
return true;
}
return false;
}
public boolean isConnected(String node, String port) {
if (!isNetwork()) return false;
for (Connection c : getConnections()) {
if (c.getInputNode().equals(node) && c.getInputPort().equals(port))
return true;
}
return false;
}
/**
* Create a new node with a number of nodes from another network copied into.
* </p>
* The original parent of the nodes is given so that previous connections can be recreated.
*
* @param nodesParent The original parent of the nodes to copy.
* @param nodes The nodes to copy.
* @return A new Node.
*/
public Node withChildrenAdded(Node nodesParent, Iterable<Node> nodes) {
checkArgument(isNetwork(), "Node %s is not a network node.", this);
Map<String, String> newNames = new HashMap<String, String>();
Node newParent = this;
for (Node node : nodes) {
newParent = newParent.withChildAdded(node);
Node newNode = Iterables.getLast(newParent.getChildren());
newNames.put(node.getName(), newNode.getName());
}
// TODO: Recreate published inputs?
for (Connection c : nodesParent.getConnections()) {
String outputNodeName = c.getOutputNode();
String inputNodeName = c.getInputNode();
if (newNames.containsKey(outputNodeName)) {
outputNodeName = newNames.get(outputNodeName);
}
if (newParent.hasChild(outputNodeName) && newNames.containsKey(inputNodeName)) {
inputNodeName = newNames.get(inputNodeName);
if (newParent.hasChild(inputNodeName)) {
Node outputNode = newParent.getChild(outputNodeName);
Node inputNode = newParent.getChild(inputNodeName);
Port inputPort = inputNode.getInput(c.getInputPort());
newParent = newParent.connect(outputNode.getName(), inputNode.getName(), inputPort.getName());
}
}
}
return newParent;
}
/**
* Find the connection with the given inputNode and port.
*
* @param inputNode The child input node
* @param inputPort The child input port
* @return the Connection object, or null if the connection could not be found.
*/
public Connection getConnection(String inputNode, String inputPort) {
if (!isNetwork()) return null;
for (Connection c : getConnections()) {
if (c.getInputNode().equals(inputNode) && c.getInputPort().equals(inputPort))
return c;
}
return null;
}
/**
* Create a new node with the given handle added.
*
* @param handle The handle to add.
* @return A new Node.
*/
public Node withHandle(String handle) {
return newNodeWithAttribute(Attribute.HANDLE, handle);
}
public boolean hasHandle() {
return handle != null;
}
public Node withAlwaysRenderedSet(boolean alwaysRendered) {
return newNodeWithAttribute(Attribute.ALWAYS_RENDERED, alwaysRendered);
}
/**
* Change an attribute on the node and return a new copy.
* The prototype remains the same.
* <p/>
* We use this more complex function instead of having every withXXX method call the constructor, because
* it allows us a to be more flexible when changing Node attributes.
*
* @param attribute The Node's attribute.
* @param value The value for the attribute. The type needs to match the internal type.
* @return A copy of this node with the attribute changed.
*/
@SuppressWarnings("unchecked")
private Node newNodeWithAttribute(Attribute attribute, Object value) {
Node prototype = this.prototype;
String name = this.name;
String comment = this.comment;
String category = this.category;
String description = this.description;
String image = this.image;
String function = this.function;
Point position = this.position;
ImmutableList<Port> inputs = this.inputs;
String outputType = this.outputType;
Port.Range outputRange = this.outputRange;
boolean isNetwork = this.isNetwork;
ImmutableList<Node> children = this.children;
String renderedChildName = this.renderedChildName;
ImmutableList<Connection> connections = this.connections;
String handle = this.handle;
boolean alwaysRendered = this.isAlwaysRendered;
switch (attribute) {
case PROTOTYPE:
prototype = (Node) value;
break;
case NAME:
name = (String) value;
break;
case COMMENT:
comment = (String) value;
break;
case CATEGORY:
category = (String) value;
break;
case DESCRIPTION:
description = (String) value;
break;
case IMAGE:
image = (String) value;
break;
case FUNCTION:
function = (String) value;
break;
case POSITION:
position = (Point) value;
break;
case INPUTS:
inputs = (ImmutableList<Port>) value;
break;
case OUTPUT_TYPE:
outputType = (String) value;
break;
case OUTPUT_RANGE:
outputRange = (Port.Range) value;
break;
case IS_NETWORK:
isNetwork = (Boolean) value;
break;
case CHILDREN:
children = (ImmutableList<Node>) value;
break;
case RENDERED_CHILD_NAME:
renderedChildName = (String) value;
break;
case CONNECTIONS:
connections = (ImmutableList<Connection>) value;
break;
case HANDLE:
handle = (String) value;
break;
case ALWAYS_RENDERED:
alwaysRendered = (Boolean) value;
break;
default:
throw new AssertionError("Unknown attribute " + attribute);
}
// If we're "changing" an attribute on ROOT or NETWORK, make it the prototype.
if (this == ROOT || this == NETWORK) {
prototype = this;
}
// The name of a node can never be "node" or "network".
if (name.equals("node"))
name = "node1";
else if (name.equals("network"))
name = "network1";
return new Node(prototype, name, comment, category, description, image, function, position,
inputs, outputType, outputRange, isNetwork, children, renderedChildName, connections, handle, alwaysRendered);
}
//// Object overrides ////
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Node)) return false;
final Node other = (Node) o;
return Objects.equal(prototype, other.prototype)
&& Objects.equal(name, other.name)
&& Objects.equal(comment, other.comment)
&& Objects.equal(category, other.category)
&& Objects.equal(description, other.description)
&& Objects.equal(image, other.image)
&& Objects.equal(function, other.function)
&& Objects.equal(position, other.position)
&& Objects.equal(inputs, other.inputs)
&& Objects.equal(outputType, other.outputType)
&& Objects.equal(isNetwork, other.isNetwork)
&& Objects.equal(children, other.children)
&& Objects.equal(renderedChildName, other.renderedChildName)
&& Objects.equal(connections, other.connections)
&& Objects.equal(handle, other.handle)
&& Objects.equal(isAlwaysRendered, other.isAlwaysRendered);
}
@Override
public String toString() {
return String.format("<Node %s:%s>", getName(), getFunction());
}
}