workcraft/workcraft

View on GitHub
workcraft/CircuitPlugin/src/org/workcraft/plugins/circuit/VisualCircuit.java

Summary

Maintainability
B
4 hrs
Test Coverage
package org.workcraft.plugins.circuit;

import org.workcraft.annotations.DisplayName;
import org.workcraft.annotations.ShortName;
import org.workcraft.commands.AbstractLayoutCommand;
import org.workcraft.dom.Container;
import org.workcraft.dom.Node;
import org.workcraft.dom.hierarchy.NamespaceHelper;
import org.workcraft.dom.math.MathConnection;
import org.workcraft.dom.math.MathNode;
import org.workcraft.dom.visual.*;
import org.workcraft.dom.visual.connections.VisualConnection;
import org.workcraft.exceptions.InvalidConnectionException;
import org.workcraft.gui.properties.ModelProperties;
import org.workcraft.gui.properties.PropertyDescriptor;
import org.workcraft.gui.tools.CommentGeneratorTool;
import org.workcraft.gui.tools.Decorator;
import org.workcraft.plugins.circuit.commands.CircuitLayoutCommand;
import org.workcraft.plugins.circuit.routing.RouterClient;
import org.workcraft.plugins.circuit.routing.RouterVisualiser;
import org.workcraft.plugins.circuit.routing.impl.Router;
import org.workcraft.plugins.circuit.routing.impl.RouterTask;
import org.workcraft.plugins.circuit.tools.*;
import org.workcraft.plugins.circuit.utils.CircuitUtils;
import org.workcraft.utils.Hierarchy;

import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;

@DisplayName("Digital Circuit")
@ShortName("circuit")
public class VisualCircuit extends AbstractVisualModel {

    public VisualCircuit(Circuit model) {
        this(model, null);
    }

    public VisualCircuit(Circuit model, VisualGroup root)  {
        super(model, root);
    }

    @Override
    public void registerGraphEditorTools() {
        addGraphEditorTool(new CircuitSelectionTool());
        addGraphEditorTool(new CommentGeneratorTool());
        addGraphEditorTool(new CircuitConnectionTool());
        addGraphEditorTool(new FunctionComponentGeneratorTool());
        addGraphEditorTool(new ContactGeneratorTool());
        addGraphEditorTool(new CircuitSimulationTool());
        addGraphEditorTool(new InitialisationAnalyserTool());
        addGraphEditorTool(new CycleAnalyserTool());
    }

    @Override
    public Circuit getMathModel() {
        return (Circuit) super.getMathModel();
    }

    @Override
    public void validateConnection(VisualNode first, VisualNode second) throws InvalidConnectionException {
        // Automatic addition of pins to blackbox components
        if ((first instanceof VisualFunctionComponent) || (second instanceof VisualFunctionComponent)) {
            if (first instanceof VisualFunctionComponent) {
                VisualFunctionComponent firstComponent = (VisualFunctionComponent) first;
                if (!firstComponent.isBlackbox()) {
                    throw new InvalidConnectionException("Cannot add output pin to component with set/reset functions.");
                }
            }
            if (second instanceof VisualFunctionComponent) {
                VisualFunctionComponent secondComponent = (VisualFunctionComponent) second;
                if (!secondComponent.isBlackbox()) {
                    throw new InvalidConnectionException("Cannot add input pin to component with set/reset functions.");
                }
            }
            return;
        }

        if (first == second) {
            throw new InvalidConnectionException("Connections are only valid between different objects.");
        }

        if (second instanceof VisualConnection) {
            throw new InvalidConnectionException("Merging connections is not allowed.");
        }

        if ((second instanceof VisualContact) || (second instanceof VisualJoint)) {
            for (VisualConnection connection : getConnections(second)) {
                if (connection.getSecond() == second) {
                    throw new InvalidConnectionException("Only one connection is allowed as a driver.");
                }
            }
        }

        if (first instanceof VisualContact) {
            VisualContact secondContact = (VisualContact) first;
            if (secondContact.isInput() && !secondContact.isPort()) {
                throw new InvalidConnectionException("Input pin of a component cannot be a driver.");
            }
            if (secondContact.isOutput() && secondContact.isPort()) {
                throw new InvalidConnectionException("Primary output cannot be a driver.");
            }
        }

        if (second instanceof VisualContact) {
            VisualContact secondContact = (VisualContact) second;
            if (secondContact.isOutput() && !secondContact.isPort()) {
                throw new InvalidConnectionException("Output pin of a component cannot be driven.");
            }
            if (secondContact.isInput() && secondContact.isPort()) {
                throw new InvalidConnectionException("Primary input cannot be driven.");
            }
        }

        // Analysis of driver/driven relation on the math model
        MathNode firstNode = CircuitUtils.getReferencedMathNode(first);
        if (firstNode == null) {
            throw new InvalidConnectionException("Cannot detect connection source.");
        }
        MathNode secondNode = CircuitUtils.getReferencedMathNode(second);
        if (secondNode == null) {
            throw new InvalidConnectionException("Cannot detect connection destination.");
        }
        Circuit circuit = getMathModel();
        Contact driver = CircuitUtils.findDriver(circuit, firstNode, false);
        Set<Contact> drivenSet = new HashSet<>(CircuitUtils.findDriven(circuit, secondNode, false));
        // Forbid zero-delay component to drive another zero-delay component or output port
        if ((driver != null) && (driver.isZeroDelayPin())) {
            for (Contact driven : drivenSet) {
                if (driven.isZeroDelayPin()) {
                    throw new InvalidConnectionException("Zero delay components cannot drive each other.");
                }
                if (driven.isOutput() && driven.isPort()) {
                    throw new InvalidConnectionException("Zero delay component cannot drive output port.");
                }
            }
        }
        // Forbid input port to drive output port (zero delay components are forbidden to drive output ports)
        if ((driver != null) && driver.isInput() && driver.isPort()) {
            for (Contact driven : drivenSet) {
                if (driven.isOutput() && driven.isPort()) {
                    throw new InvalidConnectionException("Input port cannot drive output port.");
                }
            }
        }
        // Forbid fork to several output ports (zero delay components are forbidden to drive output ports)
        drivenSet.addAll(CircuitUtils.findDriven(circuit, firstNode, false));
        int outputPortCount = 0;
        for (Contact driven : drivenSet) {
            if (driven.isOutput() && driven.isPort()) {
                if (outputPortCount > 0) {
                    throw new InvalidConnectionException("Fork several output ports is not allowed.");
                }
                outputPortCount++;
            }
        }
    }

    @Override
    public VisualCircuitConnection connect(VisualNode first, VisualNode second, MathConnection mConnection)
            throws InvalidConnectionException {

        validateConnection(first, second);

        if (first instanceof VisualConnection) {
            VisualConnection connection = (VisualConnection) first;
            Point2D splitPoint = connection.getSplitPoint();
            LinkedList<Point2D> prefixLocationsInRootSpace = ConnectionHelper.getPrefixControlPoints(connection, splitPoint);
            LinkedList<Point2D> suffixLocationsInRootSpace = ConnectionHelper.getSuffixControlPoints(connection, splitPoint);

            Container container = (Container) connection.getParent();
            VisualJoint joint = createJoint(container);
            joint.setPosition(splitPoint);

            VisualConnection predConnection = connect(connection.getFirst(), joint);
            predConnection.copyStyle(connection);
            ConnectionHelper.addControlPoints(predConnection, prefixLocationsInRootSpace);

            // Original connection must be removed at this point:
            // * AFTER creating a new connection from its first node (so first node is not automatically cleared out)
            // * BEFORE creating a connection to the second node (as only one driver is allowed)
            remove(connection);

            VisualConnection succConnection = connect(joint, connection.getSecond());
            ConnectionHelper.addControlPoints(succConnection, suffixLocationsInRootSpace);
            succConnection.copyStyle(connection);

            first = joint;
        }

        if (first instanceof VisualCircuitComponent) {
            first = ((VisualCircuitComponent) first).createContact(Contact.IOType.OUTPUT);
        }

        if (second instanceof VisualCircuitComponent) {
            second = ((VisualCircuitComponent) second).createContact(Contact.IOType.INPUT);
        }

        if (mConnection == null) {
            mConnection = getMathModel().connect(getReferencedComponent(first), getReferencedComponent(second));
        }
        VisualCircuitConnection result = new VisualCircuitConnection(mConnection, first, second);
        result.setArrowLength(0.0);

        Node vParent = Hierarchy.getCommonParent(first, second);
        Container vContainer = (Container) Hierarchy.getNearestAncestor(vParent,
                node -> (node instanceof VisualGroup) || (node instanceof VisualPage));

        if (vContainer != null) {
            vContainer.add(result);
        }
        return result;
    }

    public Collection<VisualFunctionContact> getVisualFunctionContacts() {
        return Hierarchy.getDescendantsOfType(getRoot(), VisualFunctionContact.class);
    }

    public Collection<VisualFunctionComponent> getVisualFunctionComponents() {
        return Hierarchy.getDescendantsOfType(getRoot(), VisualFunctionComponent.class);
    }

    public VisualFunctionContact getOrCreatePort(String name, Contact.IOType ioType) {
        VisualFunctionContact result = getVisualComponentByMathReference(name, VisualFunctionContact.class);
        if (result == null) {
            result = new VisualFunctionContact(new FunctionContact(ioType));
            result.setDefaultDirection();
            Container mathContainer = NamespaceHelper.getMathContainer(this, getRoot());
            mathContainer.add(result.getReferencedComponent());
            add(result);
            setMathName(result, name);
        }
        return result;
    }

    public VisualFunctionContact getOrCreateContact(VisualFunctionComponent component, String name, Contact.IOType ioType) {
        VisualFunctionContact result = getPin(component, name);
        if (result == null) {
            result = createPin(component, name, ioType);
        } else if (result.getReferencedComponent().getIOType() != ioType) {
            remove(result);
            result = createPin(component, name, ioType);
        }
        return result;
    }

    public VisualFunctionContact getExistingPinOrCreateInputPin(VisualFunctionComponent component, String name) {
        VisualFunctionContact result = getPin(component, name);
        if (result == null) {
            result = createPin(component, name, Contact.IOType.INPUT);
        }
        return result;
    }

    public boolean hasPort(String ref) {
        return getMathModel().hasPort(ref);
    }

    public boolean hasPin(VisualFunctionComponent component, String name) {
        return getMathModel().hasPin(component.getReferencedComponent(), name);
    }

    public VisualFunctionContact getPin(VisualFunctionComponent component, String name) {
        for (VisualFunctionContact contact : component.getVisualFunctionContacts()) {
            String contactName = getMathModel().getName(contact.getReferencedComponent());
            if (name.equals(contactName)) {
                return contact;
            }
        }
        return null;
    }

    private VisualFunctionContact createPin(VisualFunctionComponent component, String name, Contact.IOType ioType) {
        VisualFunctionContact result = component.createContact(ioType);
        setMathName(result, name);
        VisualContact.Direction direction = (ioType == Contact.IOType.INPUT)
                ? VisualContact.Direction.WEST : VisualContact.Direction.EAST;

        component.setPositionByDirection(result, direction, false);
        return result;
    }

    public VisualFunctionComponent createFunctionComponent(Container container) {
        if (container == null) {
            container = getRoot();
        }
        VisualFunctionComponent component = new VisualFunctionComponent(new FunctionComponent());
        Container mathContainer = NamespaceHelper.getMathContainer(this, container);
        mathContainer.add(component.getReferencedComponent());
        container.add(component);
        return component;
    }

    public VisualJoint createJoint(Container container) {
        if (container == null) {
            container = getRoot();
        }
        VisualJoint joint = new VisualJoint(new Joint());
        Container mathContainer = NamespaceHelper.getMathContainer(this, container);
        mathContainer.add(joint.getReferencedComponent());
        container.add(joint);
        return joint;
    }

    public Collection<VisualContact> getVisualPorts() {
        return Hierarchy.getDescendantsOfType(getRoot(), VisualContact.class, VisualContact::isPort);
    }

    public Collection<VisualContact> getVisualDrivers() {
        return Hierarchy.getDescendantsOfType(getRoot(), VisualContact.class, VisualContact::isDriver);
    }

    public Collection<VisualReplicaContact> getVisualReplicaContacts() {
        return Hierarchy.getDescendantsOfType(getRoot(), VisualReplicaContact.class);
    }

    @Override
    public void afterDeserialisation() {
        super.afterDeserialisation();
        // FIXME: For backward compatibility convert Environment nodes to Environment property.
        Collection<Environment> environments = new ArrayList<>();
        environments.addAll(Hierarchy.getChildrenOfType(getRoot(), Environment.class));
        environments.addAll(Hierarchy.getChildrenOfType(getMathModel().getRoot(), Environment.class));
        for (Environment environment : environments) {
            Container container = (Container) environment.getParent();
            container.remove(environment);
            getMathModel().setEnvironmentFile(environment.getRelativePath());
        }
    }

    @Override
    public void draw(Graphics2D g, Decorator decorator) {
        super.draw(g, decorator);
        if (CircuitLayoutSettings.getDebugRouting()) {
            RouterClient routerClient = new RouterClient();
            RouterTask routerTask = routerClient.registerObstacles(this);
            Router router = new Router();
            router.routeConnections(routerTask);
            RouterVisualiser.drawEverything(router, g);
        }
    }

    @Override
    public AbstractLayoutCommand getBestLayouter() {
        return new CircuitLayoutCommand();
    }

    @Override
    public ModelProperties getProperties(VisualNode node) {
        ModelProperties properties = super.getProperties(node);
        if (node == null) {
            properties.add(CircuitPropertyHelper.getEnvironmentProperty(this));
            properties.addAll(CircuitPropertyHelper.getComponentProperties(this));
        } else if (node instanceof VisualFunctionContact) {
            VisualFunctionContact contact = (VisualFunctionContact) node;
            properties.add(CircuitPropertyHelper.getSetFunctionProperty(this, contact));
            properties.add(CircuitPropertyHelper.getResetFunctionProperty(this, contact));
        } else if (node instanceof VisualFunctionComponent) {
            VisualFunctionComponent component = (VisualFunctionComponent) node;
            properties.add(CircuitPropertyHelper.getRefinementProperty(this, component));
            VisualFunctionContact mainOutput = component.getMainVisualOutput();
            if (mainOutput != null) {
                properties.add(CircuitPropertyHelper.getSetFunctionProperty(this, mainOutput));
                properties.add(CircuitPropertyHelper.getResetFunctionProperty(this, mainOutput));
                for (PropertyDescriptor property : mainOutput.getDescriptors()) {
                    String propertyName = property.getName();
                    if (Contact.PROPERTY_INIT_TO_ONE.equals(propertyName)
                            || Contact.PROPERTY_FORCED_INIT.equals(propertyName)
                            || Contact.PROPERTY_PATH_BREAKER.equals(propertyName)) {
                        properties.add(property);
                    }
                }
            }
        }
        return properties;
    }

    @Override
    public void applyRandomLayout(Point2D start, Point2D range) {
        Random r = new Random();
        for (VisualFunctionComponent component : getVisualFunctionComponents()) {
            for (VisualContact contact : component.getVisualContacts()) {
                contact.setPosition(new Point2D.Double(contact.isInput() ? -1.5 : 1.5, 0.0));
            }
            component.setContactsDefaultPosition();
            Rectangle2D box = component.getBoundingBoxInLocalSpace();
            double x = start.getX() + 0.5 * box.getWidth() + r.nextDouble() * (range.getX() - box.getWidth());
            double y = start.getY() + 0.5 * box.getHeight() + r.nextDouble() * (range.getY() - box.getHeight());
            component.setRootSpacePosition(new Point2D.Double(x, y));
        }
        for (VisualContact port : getVisualPorts()) {
            double x = start.getX() + (port.isOutput() ? range.getX() : 0);
            double y = start.getY() + r.nextDouble() * range.getY();
            port.setRootSpacePosition(new Point2D.Double(x, y));
            port.setDefaultDirection();
        }
        for (VisualConnection connection : Hierarchy.getDescendantsOfType(getRoot(), VisualConnection.class)) {
            connection.setConnectionType(VisualConnection.ConnectionType.POLYLINE);
            connection.getGraphic().setDefaultControlPoints();
        }
    }

}