workcraft/workcraft

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

Summary

Maintainability
D
3 days
Test Coverage
package org.workcraft.plugins.circuit;

import org.workcraft.dom.Container;
import org.workcraft.dom.DefaultGroupImpl;
import org.workcraft.dom.Node;
import org.workcraft.dom.visual.*;
import org.workcraft.gui.properties.PropertyDeclaration;
import org.workcraft.gui.tools.Decoration;
import org.workcraft.observation.*;
import org.workcraft.plugins.builtin.settings.SignalCommonSettings;
import org.workcraft.plugins.builtin.settings.VisualCommonSettings;
import org.workcraft.plugins.circuit.VisualContact.Direction;
import org.workcraft.serialisation.NoAutoSerialisation;
import org.workcraft.utils.ColorUtils;
import org.workcraft.utils.Hierarchy;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.awt.geom.*;
import java.io.File;
import java.util.List;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class VisualCircuitComponent extends VisualComponent
        implements Container, CustomTouchable, StateObserver, ObservableHierarchy {

    private static final AffineTransform TRANSFORM = AffineTransform.getScaleInstance(1000.0, 1000.0);
    private static final FontRenderContext CONTEXT = new FontRenderContext(TRANSFORM, true, true);

    public static final String PROPERTY_RENDER_TYPE = "Render type";

    private static final double labelMargin = 0.2;
    private static final double contactLength = 0.5;
    private static final double contactMinOffset = 2.0;
    private static final double contactStep = 1.0;
    private static final double contactMargin = 0.5;

    public Rectangle2D internalBB = null;
    public DefaultGroupImpl groupImpl = new DefaultGroupImpl(this);

    private final HashMap<VisualContact, GlyphVector> contactNameGlyphs = new HashMap<>();
    private static double contactFontSize = CircuitSettings.getContactFontSize();

    public VisualCircuitComponent(CircuitComponent component) {
        super(component, true, true, true);
        component.addObserver(this);
        addPropertyDeclarations();
    }

    private void addPropertyDeclarations() {
        addPropertyDeclaration(new PropertyDeclaration<>(Boolean.class, CircuitComponent.PROPERTY_IS_ENVIRONMENT,
                this::setIsEnvironment, this::getIsEnvironment).setCombinable().setTemplatable());

        // TODO: Rename label to module name (?)
        //  renamePropertyDeclarationByName(PROPERTY_LABEL, CircuitComponent.PROPERTY_MODULE);
    }

    @Override
    public CircuitComponent getReferencedComponent() {
        return (CircuitComponent) super.getReferencedComponent();
    }

    @NoAutoSerialisation
    public boolean getIsEnvironment() {
        if (getReferencedComponent() != null) {
            return getReferencedComponent().getIsEnvironment();
        }
        return false;
    }

    @NoAutoSerialisation
    public void setIsEnvironment(boolean value) {
        if (getReferencedComponent() != null) {
            getReferencedComponent().setIsEnvironment(value);
        }
    }

    private LinkedList<VisualContact> getOrderedOutsideContacts(Direction dir) {
        LinkedList<VisualContact> list = new LinkedList<>();
        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();
        for (VisualContact vc : Hierarchy.getChildrenOfType(this, VisualContact.class)) {
            if ((vc.getDirection() == dir) && !bb.contains(vc.getX(), vc.getY())) {
                list.add(vc);
            }
        }
        list.sort((vc1, vc2) -> {
            if ((dir == Direction.NORTH) || (dir == Direction.SOUTH)) {
                return Double.compare(vc1.getX(), vc2.getX());
            } else {
                return Double.compare(vc1.getY(), vc2.getY());
            }
        });
        return list;
    }

    private int getContactCount(final Direction dir) {
        int count = 0;
        for (VisualContact vc: Hierarchy.getChildrenOfType(this, VisualContact.class)) {
            if (vc.getDirection() == dir) {
                count++;
            }
        }
        return count;
    }

    private void spreadContactsEvenly() {
        int westCount = getContactCount(Direction.WEST);
        int northCount = getContactCount(Direction.NORTH);
        int eastCount = getContactCount(Direction.EAST);
        int southCount = getContactCount(Direction.SOUTH);

        double westPosition = -contactStep * (westCount - 1) / 2;
        double northPosition = -contactStep * (northCount - 1) / 2;
        double eastPosition = -contactStep * (eastCount - 1) / 2;
        double southPosition = -contactStep * (southCount - 1) / 2;
        for (VisualContact contact : Hierarchy.getChildrenOfType(this, VisualContact.class)) {
            switch (contact.getDirection()) {
            case WEST:
                contact.setY(westPosition);
                westPosition += contactStep;
                break;
            case NORTH:
                contact.setX(northPosition);
                northPosition += contactStep;
                break;
            case EAST:
                contact.setY(eastPosition);
                eastPosition += contactStep;
                break;
            case SOUTH:
                contact.setX(southPosition);
                southPosition += contactStep;
                break;
            }
        }
        invalidateBoundingBox();
    }

    public void setContactsDefaultPosition() {
        spreadContactsEvenly();

        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();

        Collection<VisualContact> contacts = getVisualContacts();
        for (VisualContact contact : contacts) {
            switch (contact.getDirection()) {
            case WEST:
                contact.setX(bb.getMinX() - contactLength);
                break;
            case NORTH:
                contact.setY(bb.getMinY() - contactLength);
                break;
            case EAST:
                contact.setX(bb.getMaxX() + contactLength);
                break;
            case SOUTH:
                contact.setY(bb.getMaxY() + contactLength);
                break;
            }
        }
        invalidateBoundingBox();
    }

    public void repackContactsPosition() {
        Collection<VisualContact> contacts = getVisualContacts();
        double westStart = 0.0;
        double northStart = 0.0;
        double eastStart = 0.0;
        double southStart = 0.0;
        for (VisualContact contact : contacts) {
            switch (contact.getDirection()) {
            case NORTH:
            case SOUTH:
                eastStart = Math.max(eastStart, contact.getX());
                westStart = Math.min(westStart, contact.getX());
                break;
            case WEST:
            case EAST:
                northStart = Math.min(northStart, contact.getY());
                southStart = Math.max(southStart, contact.getY());
                break;
            }
        }
        double westPosition = -contactMinOffset;
        double northPosition = -contactMinOffset;
        double eastPosition = contactMinOffset;
        double southPosition = contactMinOffset;
        for (VisualContact contact : contacts) {
            GlyphVector gv = getContactNameGlyphs(contact, contact::getName);
            double textWidth = gv.getVisualBounds().getWidth();
            double contactBestPosition = textWidth + contactMargin + contactLength;
            switch (contact.getDirection()) {
            case WEST:
                westPosition = Math.min(westPosition, -contactBestPosition);
                break;
            case NORTH:
                northPosition = Math.min(northPosition, -contactBestPosition);
                break;
            case EAST:
                eastPosition = Math.max(eastPosition, contactBestPosition);
                break;
            case SOUTH:
                southPosition = Math.max(southPosition, contactBestPosition);
                break;
            }
        }
        for (VisualContact contact : contacts) {
            switch (contact.getDirection()) {
            case WEST:
                contact.setX(TransformHelper.snapP5(westStart + westPosition));
                break;
            case NORTH:
                contact.setY(TransformHelper.snapP5(northStart + northPosition));
                break;
            case EAST:
                contact.setX(TransformHelper.snapP5(eastStart + eastPosition));
                break;
            case SOUTH:
                contact.setY(TransformHelper.snapP5(southStart + southPosition));
                break;
            }
        }
        invalidateBoundingBox();
    }

    @Override
    public void centerPivotPoint(boolean horizontal, boolean vertical) {
        super.centerPivotPoint(horizontal, vertical);
        invalidateBoundingBox();
    }

    public VisualContact createContact(Contact.IOType ioType) {
        VisualContact vc = new VisualContact(new Contact(ioType));
        addContact(vc);
        return vc;
    }

    public void addContact(VisualContact vc) {
        if (!getChildren().contains(vc)) {
            getReferencedComponent().add(vc.getReferencedComponent());
            add(vc);
        }
    }

    public void setPositionByDirection(VisualContact contact, Direction direction, boolean reverseProgression) {
        contact.setDirection(direction);
        Collection<VisualContact> contacts = Hierarchy.getChildrenOfType(this, VisualContact.class);
        contacts.remove(contact);
        double contactOffset = getContactBestOffset(contacts, direction);
        switch (direction) {
        case WEST:
        case EAST:
            contact.setX(contactOffset);
            positionVertical(contact, reverseProgression);
            break;
        case NORTH:
        case SOUTH:
            contact.setY(contactOffset);
            positionHorizontal(contact, reverseProgression);
            break;
        }
        invalidateBoundingBox();
    }

    private double getContactBestOffset(Collection<VisualContact> contacts, Direction direction) {
        Rectangle2D bb = getContactExpandedBox(contacts);
        contacts = contacts.stream()
                .filter(contact -> contact.getDirection() == direction)
                .collect(Collectors.toSet());

        switch (direction) {
        case WEST:
            double westOffset = contacts.stream().mapToDouble(VisualContact::getX).max().orElse(-contactMinOffset);
            return Math.min(TransformHelper.snapP5(bb.getMinX() - contactLength), westOffset);
        case NORTH:
            double northOffset = contacts.stream().mapToDouble(VisualContact::getY).max().orElse(-contactMinOffset);
            return Math.min(TransformHelper.snapP5(bb.getMinY() - contactLength), northOffset);
        case EAST:
            double eastOffset = contacts.stream().mapToDouble(VisualContact::getX).min().orElse(contactMinOffset);
            return Math.max(TransformHelper.snapP5(bb.getMaxX() + contactLength), eastOffset);
        case SOUTH:
            double southOffset = contacts.stream().mapToDouble(VisualContact::getY).min().orElse(contactMinOffset);
            return Math.max(TransformHelper.snapP5(bb.getMaxY() + contactLength), southOffset);
        default:
            return 0.0;
        }
    }

    private void positionHorizontal(VisualContact vc, boolean reverseProgression) {
        LinkedList<VisualContact> contacts = getOrderedOutsideContacts(vc.getDirection());
        contacts.remove(vc);
        double x = 0.0;
        if (!contacts.isEmpty()) {
            if (reverseProgression) {
                x = TransformHelper.snapP5(contacts.getFirst().getX() - contactStep);
                for (VisualContact contact : getOrderedOutsideContacts(Direction.WEST)) {
                    if (contact.getX() > x - contactMargin - contactLength) {
                        contact.setX(x - contactMargin - contactLength);
                    }
                }
            } else {
                x = TransformHelper.snapP5(contacts.getLast().getX() + contactStep);
                for (VisualContact contact : getOrderedOutsideContacts(Direction.EAST)) {
                    if (contact.getX() < x + contactMargin + contactLength) {
                        contact.setX(x + contactMargin + contactLength);
                    }
                }
            }
        }
        vc.setX(x);
    }

    private void positionVertical(VisualContact vc, boolean reverseProgression) {
        LinkedList<VisualContact> contacts = getOrderedOutsideContacts(vc.getDirection());
        contacts.remove(vc);
        double y = 0.0;
        if (!contacts.isEmpty()) {
            if (reverseProgression) {
                y = TransformHelper.snapP5(contacts.getFirst().getY() - contactStep);
                for (VisualContact contact : getOrderedOutsideContacts(Direction.NORTH)) {
                    if (contact.getY() > y - contactMargin - contactLength) {
                        contact.setY(y - contactMargin - contactLength);
                    }
                }
            } else {
                y = TransformHelper.snapP5(contacts.getLast().getY() + contactStep);
                for (VisualContact contact : getOrderedOutsideContacts(Direction.SOUTH)) {
                    if (contact.getY() < y + contactMargin + contactLength) {
                        contact.setY(y + contactMargin + contactLength);
                    }
                }
            }
        }
        vc.setY(y);
    }

    public void invalidateBoundingBox() {
        internalBB = null;
    }

    private Rectangle2D getContactMinimalBox(Collection<VisualContact> contacts) {
        double size = VisualCommonSettings.getNodeSize();
        double xMin = -size / 2;
        double yMin = -size / 2;
        double xMax = size / 2;
        double yMax = size / 2;
        for (VisualContact contact : contacts) {
            switch (contact.getDirection()) {
            case WEST:
                double xWest = contact.getX() + contactLength;
                if ((xWest < -size / 2) && (xWest > xMin)) {
                    xMin = xWest;
                }
                break;
            case NORTH:
                double yNorth = contact.getY() + contactLength;
                if ((yNorth < -size / 2) && (yNorth > yMin)) {
                    yMin = yNorth;
                }
                break;
            case EAST:
                double xEast = contact.getX() - contactLength;
                if ((xEast > size / 2) && (xEast < xMax)) {
                    xMax = xEast;
                }
                break;
            case SOUTH:
                double ySouth = contact.getY() - contactLength;
                if ((ySouth > size / 2) && (ySouth < yMax)) {
                    yMax = ySouth;
                }
                break;
            }
        }
        return new Rectangle2D.Double(xMin, yMin, xMax - xMin, yMax - yMin);
    }

    private Rectangle2D getContactExpandedBox(Collection<VisualContact> contacts) {
        Rectangle2D minBox = getContactMinimalBox(contacts);
        double x1 = minBox.getMinX();
        double y1 = minBox.getMinY();
        double x2 = minBox.getMaxX();
        double y2 = minBox.getMaxY();
        for (VisualContact contact : contacts) {
            double x = contact.getX();
            double y = contact.getY();
            switch (contact.getDirection()) {
            case WEST:
                if (contact.getX() < minBox.getMinX()) {
                    y1 = Math.min(y1, y - contactMargin);
                    y2 = Math.max(y2, y + contactMargin);
                }
                break;
            case NORTH:
                if (contact.getY() < minBox.getMinY()) {
                    x1 = Math.min(x1, x - contactMargin);
                    x2 = Math.max(x2, x + contactMargin);
                }
                break;
            case EAST:
                if (contact.getX() > minBox.getMaxX()) {
                    y1 = Math.min(y1, y - contactMargin);
                    y2 = Math.max(y2, y + contactMargin);
                }
                break;
            case SOUTH:
                if (contact.getY() > minBox.getMaxY()) {
                    x1 = Math.min(x1, x - contactMargin);
                    x2 = Math.max(x2, x + contactMargin);
                }
                break;
            }
        }
        return new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
    }

    private Rectangle2D getContactBestBox() {
        Collection<VisualContact> contacts = Hierarchy.getChildrenOfType(this, VisualContact.class);
        Rectangle2D bb = getContactExpandedBox(contacts);
        double x1 = bb.getMinX();
        double y1 = bb.getMinY();
        double x2 = bb.getMaxX();
        double y2 = bb.getMaxY();

        boolean westFirst = true;
        boolean northFirst = true;
        boolean eastFirst = true;
        boolean southFirst = true;

        for (VisualContact contact : contacts) {
            double x = contact.getX();
            double y = contact.getY();
            switch (contact.getDirection()) {
            case WEST:
                if (westFirst) {
                    x1 = x + contactLength;
                } else {
                    x1 = Math.max(x1, x + contactLength);
                }
                westFirst = false;
                break;
            case NORTH:
                if (northFirst) {
                    y1 = y + contactLength;
                } else {
                    y1 = Math.max(y1, y + contactLength);
                }
                northFirst = false;
                break;
            case EAST:
                if (eastFirst) {
                    x2 = x - contactLength;
                } else {
                    x2 = Math.min(x2, x - contactLength);
                }
                eastFirst = false;
                break;
            case SOUTH:
                if (southFirst) {
                    y2 = y - contactLength;
                } else {
                    y2 = Math.min(y2, y - contactLength);
                }
                southFirst = false;
                break;
            }
        }

        if (x1 > x2) {
            x1 = x2 = (x1 + x2) / 2;
        }
        if (y1 > y2) {
            y1 = y2 = (y1 + y2) / 2;
        }
        Rectangle2D maxBox = new Rectangle2D.Double(x1, y1, x2 - x1, y2 - y1);
        return BoundingBoxHelper.union(bb, maxBox);
    }

    private Point2D getContactLinePosition(VisualContact vc) {
        Point2D result = null;
        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();
        switch (vc.getDirection()) {
        case NORTH:
            result = new Point2D.Double(vc.getX(), bb.getMinY());
            break;
        case EAST:
            result = new Point2D.Double(bb.getMaxX(), vc.getY());
            break;
        case SOUTH:
            result = new Point2D.Double(vc.getX(), bb.getMaxY());
            break;
        case WEST:
            result = new Point2D.Double(bb.getMinX(), vc.getY());
            break;
        }
        return result;
    }

    private void drawContactLines(DrawRequest r) {
        for (VisualContact vc: Hierarchy.getChildrenOfType(this, VisualContact.class)) {
            Point2D p1 = vc.getPosition();
            Point2D p2 = getContactLinePosition(vc);
            if (p2 != null) {
                Graphics2D g = r.getGraphics();
                Decoration d = r.getDecoration();
                Color colorisation = d.getColorisation();
                g.setStroke(new BasicStroke((float) CircuitSettings.getWireWidth()));
                g.setColor(ColorUtils.colorise(getForegroundColor(), colorisation));
                Line2D line = new Line2D.Double(p1, p2);
                g.draw(line);
            }
        }
    }

    public Font getContactFont() {
        return NAME_FONT.deriveFont((float) CircuitSettings.getContactFontSize());
    }

    private GlyphVector getContactNameGlyphs(VisualContact vc, Supplier<String> getName) {
        if (Math.abs(CircuitSettings.getContactFontSize() - contactFontSize) > 0.001) {
            contactFontSize = CircuitSettings.getContactFontSize();
            contactNameGlyphs.clear();
        }
        GlyphVector gv = contactNameGlyphs.get(vc);
        if (gv == null) {
            gv = getContactFont().createGlyphVector(CONTEXT, getName.get());
            contactNameGlyphs.put(vc, gv);
        }
        return gv;
    }

    private void drawContactName(DrawRequest r, VisualContact vc) {
        Graphics2D g = r.getGraphics();
        Decoration d = r.getDecoration();
        Color colorisation = d.getColorisation();
        Color color = vc.isInput() ? SignalCommonSettings.getInputColor() : SignalCommonSettings.getOutputColor();
        g.setColor(ColorUtils.colorise(color, colorisation));

        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();
        GlyphVector gv = getContactNameGlyphs(vc, () -> r.getModel().getMathName(vc));
        LineMetrics lineMetrics = getNameFont().getLineMetrics(vc.getName(), CONTEXT);
        double textWidth = gv.getVisualBounds().getWidth();
        double yCenterOffset = 0.2 * lineMetrics.getHeight();

        float x = 0.0f;
        float y = 0.0f;
        switch (vc.getDirection()) {
        case NORTH:
            x = (float) (-bb.getMinY() - labelMargin - textWidth);
            y = (float) (vc.getX() + yCenterOffset);
            break;
        case EAST:
            x = (float) (bb.getMaxX() - labelMargin - textWidth);
            y = (float) (vc.getY() + yCenterOffset);
            break;
        case SOUTH:
            x = (float) (-bb.getMaxY() + labelMargin);
            y = (float) (vc.getX() + yCenterOffset);
            break;
        case WEST:
            x = (float) (bb.getMinX() + labelMargin);
            y = (float) (vc.getY() + yCenterOffset);
            break;
        }
        g.drawGlyphVector(gv, x, y);
    }

    public void drawContactNames(DrawRequest r) {
        Graphics2D g = r.getGraphics();
        AffineTransform savedTransform = g.getTransform();

        for (VisualContact vc: Hierarchy.getChildrenOfType(this, VisualContact.class,
                contact -> (contact.getDirection() == Direction.WEST) || (contact.getDirection() == Direction.EAST))) {

            drawContactName(r, vc);
        }

        AffineTransform rotateTransform = new AffineTransform();
        rotateTransform.quadrantRotate(-1);
        g.transform(rotateTransform);

        for (VisualContact vc: Hierarchy.getChildrenOfType(this, VisualContact.class,
                contact -> (contact.getDirection() == Direction.NORTH) || (contact.getDirection() == Direction.SOUTH))) {

            drawContactName(r, vc);
        }

        g.setTransform(savedTransform);
    }

    @Override
    public Rectangle2D getInternalBoundingBoxInLocalSpace() {
        if ((groupImpl != null) && (internalBB == null)) {
            internalBB = getContactBestBox();
        }
        if (internalBB != null) {
            return BoundingBoxHelper.copy(internalBB);
        }
        return super.getInternalBoundingBoxInLocalSpace();
    }

    @Override
    public Rectangle2D getBoundingBoxInLocalSpace() {
        Rectangle2D bb = super.getBoundingBoxInLocalSpace();
        Collection<Touchable> touchableChildren = Hierarchy.getChildrenOfType(this, Touchable.class);
        Rectangle2D childrenBB = BoundingBoxHelper.mergeBoundingBoxes(touchableChildren);
        return BoundingBoxHelper.union(bb, childrenBB);
    }

    @Override
    public void draw(DrawRequest r) {
        // Cache rendered text to better estimate the bounding box
        cacheRenderedText(r);
        drawOutline(r);
        drawRefinement(r);
        drawPivot(r);
        drawContactLines(r);
        drawContactNames(r);
        drawLabelInLocalSpace(r);
        drawNameInLocalSpace(r);
        // External decorations
        Graphics2D g = r.getGraphics();
        Decoration d = r.getDecoration();
        d.decorate(g);
    }

    @Override
    public void drawOutline(DrawRequest r) {
        Decoration d = r.getDecoration();
        Graphics2D g = r.getGraphics();
        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();
        if (bb != null) {
            g.setColor(ColorUtils.colorise(getFillColor(), d.getBackground()));
            g.fill(bb);
            g.setColor(ColorUtils.colorise(getForegroundColor(), d.getColorisation()));
            setStroke(g);
            g.draw(bb);
        }
    }

    private void drawRefinement(DrawRequest r) {
        Graphics2D g = r.getGraphics();
        Rectangle2D bb = getInternalBoundingBoxInLocalSpace();
        File file = getReferencedComponent().getRefinementFile();
        if ((bb != null) && (file != null)) {
            double dx = VisualCommonSettings.getNodeSize() / 10;
            double dy = VisualCommonSettings.getNodeSize() / 10;
            double x = bb.getCenterX();
            double y = bb.getCenterY() + 0.3 * VisualCommonSettings.getNodeSize();
            double w = VisualCommonSettings.getNodeSize() / 20;
            double w2 = w / 2;
            Path2D p = new Path2D.Double();
            p.moveTo(x - dx - w, y + dy);
            p.lineTo(x - dx + w2, y + dy + w2 + w);
            p.lineTo(x - dx + w2, y + dy + w2);
            p.lineTo(x + dx + w2, y + dy + w2);
            p.lineTo(x + dx + w2, y);
            p.lineTo(x + dx - w2, y);
            p.lineTo(x + dx - w2, y + dy - w2);
            p.lineTo(x - dx + w2, y + dy - w2);
            p.lineTo(x - dx + w2, y + dy - w2 - w);
            p.closePath();
            g.fill(p);
        }
    }

    public void setStroke(Graphics2D g) {
        if (getIsEnvironment()) {
            g.setStroke(new BasicStroke((float) CircuitSettings.getBorderWidth(), BasicStroke.CAP_SQUARE,
                    BasicStroke.JOIN_MITER, 1.0f, new float[]{0.2f, 0.2f}, 0.0f));
        } else {
            g.setStroke(new BasicStroke((float) CircuitSettings.getBorderWidth()));
        }
    }

    @Override
    public void add(Node node) {
        groupImpl.add(node);
        if (node instanceof VisualContact) {
            ((VisualContact) node).addObserver(this);
        }
    }

    @Override
    public Collection<Node> getChildren() {
        return groupImpl.getChildren();
    }

    @Override
    public Node getParent() {
        return groupImpl.getParent();
    }

    @Override
    public void setParent(Node parent) {
        groupImpl.setParent(parent);
    }

    @Override
    public void remove(Node node) {
        if (node instanceof VisualContact) {
            invalidateBoundingBox();
            contactNameGlyphs.remove(node);
        }
        groupImpl.remove(node);
    }

    @Override
    public void add(Collection<? extends Node> nodes) {
        groupImpl.add(nodes);
        for (Node node : nodes) {
            if (node instanceof VisualContact) {
                ((VisualContact) node).addObserver(this);
            }
        }
    }

    @Override
    public void remove(Collection<? extends Node> nodes) {
        for (Node n : nodes) {
            remove(n);
        }
    }

    @Override
    public void reparent(Collection<? extends Node> nodes, Container newParent) {
        groupImpl.reparent(nodes, newParent);
    }

    @Override
    public void reparent(Collection<? extends Node> nodes) {
        groupImpl.reparent(nodes);
    }

    @Override
    public Node hitCustom(Point2D point) {
        Point2D pointInLocalSpace = getParentToLocalTransform().transform(point, null);
        for (Node node : getChildren()) {
            if (node instanceof VisualNode) {
                VisualNode vn = (VisualNode) node;
                if (vn.hitTest(pointInLocalSpace)) {
                    return vn;
                }
            }
        }
        return hitTest(point) ? this : null;
    }

    @Override
    public void notify(StateEvent e) {
        if (e instanceof TransformChangedEvent) {
            TransformChangedEvent t = (TransformChangedEvent) e;
            if (t.sender instanceof VisualContact) {
                VisualContact vc = (VisualContact) t.sender;

                AffineTransform at = t.sender.getTransform();
                double x = at.getTranslateX();
                double y = at.getTranslateY();
                Collection<VisualContact> contacts = Hierarchy.getChildrenOfType(this, VisualContact.class);
                Rectangle2D bb = getContactExpandedBox(contacts);
                if ((x <= bb.getMinX()) && (y > bb.getMinY()) && (y < bb.getMaxY())) {
                    vc.setDirection(Direction.WEST);
                }
                if ((x >= bb.getMaxX()) && (y > bb.getMinY()) && (y < bb.getMaxY())) {
                    vc.setDirection(Direction.EAST);
                }
                if ((y <= bb.getMinY()) && (x > bb.getMinX()) && (x < bb.getMaxX())) {
                    vc.setDirection(Direction.NORTH);
                }
                if ((y >= bb.getMaxY()) && (x > bb.getMinX()) && (x < bb.getMaxX())) {
                    vc.setDirection(Direction.SOUTH);
                }
                invalidateBoundingBox();
            }
        }

        if (e instanceof PropertyChangedEvent) {
            PropertyChangedEvent pc = (PropertyChangedEvent) e;
            String propertyName = pc.getPropertyName();
            if (propertyName.equals(Contact.PROPERTY_NAME)
                    || propertyName.equals(Contact.PROPERTY_IO_TYPE)
                    || propertyName.equals(VisualContact.PROPERTY_DIRECTION)) {

                invalidateBoundingBox();
                contactNameGlyphs.clear();
            }
        }
    }

    @Override
    public void addObserver(HierarchyObserver obs) {
        groupImpl.addObserver(obs);
    }

    @Override
    public void removeObserver(HierarchyObserver obs) {
        groupImpl.removeObserver(obs);
    }

    @Override
    public void removeAllObservers() {
        groupImpl.removeAllObservers();
    }

    @Override
    public void copyStyle(Stylable src) {
        super.copyStyle(src);
        if (src instanceof VisualCircuitComponent) {
            VisualCircuitComponent srcComponent = (VisualCircuitComponent) src;
            setIsEnvironment(srcComponent.getIsEnvironment());
        }
    }

    @Override
    public String getLabel() {
        return getReferencedComponent().getModule();
    }

    @Override
    public void setLabel(String label) {
        getReferencedComponent().setModule(label);
        super.setLabel(label);
    }

    public Collection<VisualContact> getVisualContacts() {
        return Hierarchy.getChildrenOfType(this, VisualContact.class);
    }

    public List<VisualContact> getVisualInputs() {
        ArrayList<VisualContact> result = new ArrayList<>();
        for (VisualContact contact: getVisualContacts()) {
            if (contact.isInput()) {
                result.add(contact);
            }
        }
        return result;
    }

    public Collection<VisualContact> getVisualOutputs() {
        ArrayList<VisualContact> result = new ArrayList<>();
        for (VisualContact contact: getVisualContacts()) {
            if (contact.isOutput()) {
                result.add(contact);
            }
        }
        return result;
    }

    public VisualContact getFirstVisualInput() {
        VisualContact result = null;
        for (VisualContact contact: getVisualContacts()) {
            if (contact.isInput()) {
                result = contact;
                break;
            }
        }
        return result;
    }

    public VisualContact getFirstVisualOutput() {
        VisualContact result = null;
        for (VisualContact contact: getVisualContacts()) {
            if (contact.isOutput()) {
                result = contact;
                break;
            }
        }
        return result;
    }

    public VisualContact getMainVisualOutput() {
        VisualContact result = null;
        Collection<VisualContact> outputs = getVisualOutputs();
        if (outputs.size() == 1) {
            result = outputs.iterator().next();
        }
        return result;
    }

}