Package nodebox.node

Source Code of nodebox.node.Node

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());
    }

}
TOP

Related Classes of nodebox.node.Node

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.