com.clarkparsia.sbol.editor.SBOLDesign.java Source code

Java tutorial

Introduction

Here is the source code for com.clarkparsia.sbol.editor.SBOLDesign.java

Source

/*
 * Copyright (c) 2012 - 2015, Clark & Parsia, LLC. <http://www.clarkparsia.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.clarkparsia.sbol.editor;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButton;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;

import org.sbolstandard.core.DnaComponent;
import org.sbolstandard.core.DnaSequence;
import org.sbolstandard.core.SBOLDocument;
import org.sbolstandard.core.SBOLFactory;
import org.sbolstandard.core.SequenceAnnotation;
import org.sbolstandard.core.StrandType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adamtaft.eb.EventBus;
import com.clarkparsia.sbol.CharSequences;
import com.clarkparsia.sbol.SBOLTextWriter;
import com.clarkparsia.sbol.SBOLUtils;
import com.clarkparsia.sbol.SublimeSBOLFactory;
import com.clarkparsia.sbol.editor.dialog.PartEditDialog;
import com.clarkparsia.sbol.editor.dialog.SelectPartDialog;
import com.clarkparsia.sbol.editor.event.DesignChangedEvent;
import com.clarkparsia.sbol.editor.event.DesignLoadedEvent;
import com.clarkparsia.sbol.editor.event.FocusInEvent;
import com.clarkparsia.sbol.editor.event.FocusOutEvent;
import com.clarkparsia.sbol.editor.event.PartVisibilityChangedEvent;
import com.clarkparsia.sbol.editor.event.SelectionChangedEvent;
import com.clarkparsia.sbol.order.PartialOrder;
import com.clarkparsia.sbol.order.PartialOrderComparator;
import com.clarkparsia.sbol.order.PartialOrderRelation;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;

/**
 * 
 * @author Evren Sirin
 */
public class SBOLDesign {
    private static Logger LOGGER = LoggerFactory.getLogger(SBOLDesign.class.getName());

    private static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 12);

    private static final int IMG_GAP = 10;
    private static final int IMG_HEIGHT = Part.IMG_HEIGHT;
    private static final int IMG_WIDTH = Part.IMG_WIDTH + IMG_GAP;
    private static final int IMG_PAD = 20;

    private static final boolean HEADLESS = GraphicsEnvironment.isHeadless();

    private enum ReadOnly {
        REGISTRY_COMPONENT, UNCOVERED_SEQUENCE, MISSING_START_END
    }

    public final SBOLEditorAction EDIT_ROOT = new SBOLEditorAction("Edit root component",
            "Edit root component information", "edit_root.gif") {
        @Override
        protected void perform() {
            editRootComponent();
        }
    };

    public final SBOLEditorAction FIND = new SBOLEditorAction("Find components",
            "Find components in the part registry", "find.gif") {
        @Override
        protected void perform() {
            findPartForSelectedComponent();
        }
    };

    public final SBOLEditorAction EDIT = new SBOLEditorAction("Edit component",
            "Edit selected component information", "edit.gif") {
        @Override
        protected void perform() {
            editSelectedComponent();

        }
    };
    public final SBOLEditorAction DELETE = new SBOLEditorAction("Delete component", "Delete the selected component",
            "delete.gif") {
        @Override
        protected void perform() {
            DnaComponent comp = getSelectedComponent();
            deleteComponent(comp);
        }
    };

    public final SBOLEditorAction FLIP = new SBOLEditorAction("Flip strand",
            "Flip the strand for the selected component", "flipStrand.png") {
        @Override
        protected void perform() {
            DnaComponent comp = getSelectedComponent();
            flipStrand(comp);
        }
    };

    public final SBOLEditorAction HIDE_SCARS = new SBOLEditorAction("Hide scars", "Hide scars in the design",
            "hideScars.png") {
        @Override
        protected void perform() {
            boolean isVisible = isPartVisible(Parts.SCAR);
            setPartVisible(Parts.SCAR, !isVisible);
        }
    }.toggle();

    public final SBOLEditorAction ADD_SCARS = new SBOLEditorAction("Add scars",
            "Add a scar between every two non-scar component in the design", "addScars.png") {
        @Override
        protected void perform() {
            addScars();
        }
    };

    public final SBOLEditorAction FOCUS_IN = new SBOLEditorAction("Focus in",
            "Focus in the component to view and edit its subcomponents", "go_down.png") {
        @Override
        protected void perform() {
            focusIn();
        }
    };

    public final SBOLEditorAction FOCUS_OUT = new SBOLEditorAction("Focus out", "Focus out to the parent component",
            "go_up.png") {
        @Override
        protected void perform() {
            focusOut();
        }
    };

    private final EventBus eventBus;

    private final List<DesignElement> elements = Lists.newArrayList();
    private final Map<DesignElement, JLabel> buttons = Maps.newHashMap();
    private final Set<Part> hiddenParts = Sets.newHashSet();

    private final Set<ReadOnly> readOnly = EnumSet.noneOf(ReadOnly.class);

    private boolean loading = false;

    private boolean isCircular = false;
    private DesignElement selectedElement = null;

    private final Box elementBox;
    private final Box backboneBox;
    private final JPanel panel;

    private final JPopupMenu selectionPopupMenu = createPopupMenu(FIND, EDIT, FLIP, DELETE, FOCUS_IN);
    private final JPopupMenu noSelectionPopupMenu = createPopupMenu(EDIT_ROOT, FOCUS_OUT);

    private DnaComponent currentComponent;

    private boolean hasSequence;

    private final Deque<DnaComponent> parentComponents = new ArrayDeque<DnaComponent>();

    public SBOLDesign(EventBus eventBus) {
        this.eventBus = eventBus;

        elementBox = Box.createHorizontalBox();
        elementBox.setBorder(BorderFactory.createEmptyBorder());
        elementBox.setOpaque(false);

        backboneBox = Box.createHorizontalBox();
        backboneBox.setBorder(BorderFactory.createEmptyBorder());
        backboneBox.setOpaque(false);

        JPanel contentPanel = new JPanel();
        contentPanel.setAlignmentX(0.5f);
        contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
        contentPanel.setBorder(BorderFactory.createEmptyBorder());
        contentPanel.setOpaque(false);
        contentPanel.setAlignmentY(0);
        contentPanel.add(elementBox);
        contentPanel.add(backboneBox);

        panel = new DesignPanel();
        panel.setOpaque(false);
        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
        panel.setAlignmentX(0.5f);
        panel.setBorder(BorderFactory.createEmptyBorder());
        panel.add(Box.createHorizontalGlue());
        Component leftStrut = Box.createHorizontalStrut(IMG_PAD + IMG_GAP);
        if (leftStrut instanceof JComponent) {
            ((JComponent) leftStrut).setOpaque(false);
        }
        panel.add(leftStrut);
        panel.add(contentPanel);
        Component rightStrut = Box.createHorizontalStrut(IMG_PAD + IMG_GAP);
        if (rightStrut instanceof JComponent) {
            ((JComponent) rightStrut).setOpaque(false);
        }
        panel.add(rightStrut);
        panel.add(Box.createHorizontalGlue());

        panel.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent event) {
                setSelectedElement(null);
                if (event.isPopupTrigger()) {
                    noSelectionPopupMenu.show(panel, event.getX(), event.getY());
                }
            }
        });

        ActionListener deleteAction = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent paramActionEvent) {
                if (selectedElement != null) {
                    deleteComponent(getSelectedComponent());
                }
            }
        };
        KeyStroke deleteKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
        KeyStroke backspaceKey = KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0);
        panel.registerKeyboardAction(deleteAction, deleteKey, JComponent.WHEN_IN_FOCUSED_WINDOW);
        panel.registerKeyboardAction(deleteAction, backspaceKey, JComponent.WHEN_IN_FOCUSED_WINDOW);
    }

    private static JPopupMenu createPopupMenu(SBOLEditorAction... actions) {
        final JPopupMenu popup = new JPopupMenu();

        for (SBOLEditorAction action : actions) {
            popup.add(action.createMenuItem());
        }

        return popup;
    }

    public boolean canFocusIn() {
        DnaComponent comp = getSelectedComponent();
        return comp != null;
    }

    public void focusIn() {
        Preconditions.checkState(canFocusIn(), "No selection to focus in");

        DnaComponent comp = getSelectedComponent();

        BufferedImage snapshot = getSnapshot();

        updateRootComponent();
        parentComponents.push(currentComponent);

        load(comp);

        eventBus.publish(new FocusInEvent(this, comp, snapshot));
    }

    public boolean canFocusOut() {
        return !parentComponents.isEmpty();
    }

    public void focusOut() {
        Preconditions.checkState(canFocusOut(), "No parent design to focus out");

        focusOut(getParentComponent());
    }

    public void focusOut(DnaComponent comp) {
        if (currentComponent == comp) {
            return;
        }

        updateRootComponent();

        DnaComponent parentComponent = parentComponents.pop();
        while (parentComponent != comp) {
            parentComponent = parentComponents.pop();
        }

        load(parentComponent);

        eventBus.publish(new FocusOutEvent(this, parentComponent));
    }

    public void load(SBOLDocument doc) {
        if (doc == null) {
            JOptionPane.showMessageDialog(panel, "No document to load.", "Load error", JOptionPane.ERROR_MESSAGE);
            return;
        }

        Iterator<DnaComponent> components = SBOLUtils.getRootComponents(doc);
        DnaComponent newComponent = null;
        if (components.hasNext()) {
            newComponent = components.next();
            if (components.hasNext()) {
                JOptionPane.showMessageDialog(panel, "Cannot load documents with multiple root DnaComponents.",
                        "Load error", JOptionPane.ERROR_MESSAGE);
                return;
            }
        } else {
            newComponent = SublimeSBOLFactory.createDnaComponent();
            newComponent.setURI(SBOLUtils.createURI());
            newComponent.setDisplayId("Unnamed");
        }

        parentComponents.clear();
        load(newComponent);

        eventBus.publish(new DesignLoadedEvent(this));
    }

    private void load(DnaComponent newRoot) {
        loading = true;

        elementBox.removeAll();
        backboneBox.removeAll();
        elements.clear();
        buttons.clear();
        isCircular = false;
        readOnly.clear();

        currentComponent = newRoot;
        populateComponents(currentComponent);

        hasSequence = (currentComponent.getDnaSequence() != null) && elements.isEmpty();

        detectReadOnly();

        selectedElement = null;

        loading = false;

        refreshUI();
        fireSelectionChangedEvent();
    }

    private void detectReadOnly() {
        if (SBOLUtils.isRegistryComponent(currentComponent)) {
            readOnly.add(ReadOnly.REGISTRY_COMPONENT);
        }

        Map<Integer, DnaSequence> uncoveredSequences = findUncoveredSequences();
        if (uncoveredSequences == null) {
            readOnly.add(ReadOnly.MISSING_START_END);
        } else if (!uncoveredSequences.isEmpty()) {
            readOnly.add(ReadOnly.UNCOVERED_SEQUENCE);
        }
    }

    private boolean confirmEditable() {
        if (readOnly.contains(ReadOnly.REGISTRY_COMPONENT)) {
            if (!PartEditDialog.confirmEditing(panel, currentComponent)) {
                return false;
            }
            readOnly.remove(ReadOnly.REGISTRY_COMPONENT);
        }

        if (readOnly.contains(ReadOnly.MISSING_START_END)) {
            int result = JOptionPane.showConfirmDialog(panel,
                    "The component '" + currentComponent.getDisplayId() + "' has a DNA sequence but the\n"
                            + "subcomponents don't have start or end\n"
                            + "coordinates. If you edit the design you will\n" + "lose the DNA sequence.\n\n"
                            + "Do you want to continue with editing?",
                    "Uncovered sequence", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);

            if (result == JOptionPane.NO_OPTION) {
                return false;
            }
            readOnly.remove(ReadOnly.REGISTRY_COMPONENT);
        } else if (readOnly.contains(ReadOnly.UNCOVERED_SEQUENCE)) {
            String msg = "The sub components do not cover the DNA sequence\n" + "of the component '"
                    + currentComponent.getDisplayId() + "' completely.\n"
                    + "You need to add SCAR components to cover the missing\n"
                    + "parts or you will lose the uncovered DNA sequence.\n\n" + "How do you want to continue?";

            JRadioButton[] buttons = { new JRadioButton("Add SCAR Parts to handle uncovered sequences"),
                    new JRadioButton("Continue with editing and lose the root DNA sequence"),
                    new JRadioButton("Cancel the operation and do not edit the component") };

            JTextArea textArea = new JTextArea(msg);
            textArea.setEditable(false);
            textArea.setLineWrap(true);
            textArea.setOpaque(false);
            textArea.setBorder(BorderFactory.createEmptyBorder());
            textArea.setAlignmentX(Component.LEFT_ALIGNMENT);

            Box box = Box.createVerticalBox();
            box.add(textArea);

            ButtonGroup group = new ButtonGroup();
            for (JRadioButton button : buttons) {
                button.setSelected(true);
                button.setAlignmentX(Component.LEFT_ALIGNMENT);
                group.add(button);
                box.add(button);
            }

            int result = JOptionPane.showConfirmDialog(panel, box, "Uncovered sequence",
                    JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);

            if (result == JOptionPane.CANCEL_OPTION || buttons[2].isSelected()) {
                return false;
            }

            readOnly.remove(ReadOnly.UNCOVERED_SEQUENCE);

            if (buttons[0].isSelected()) {
                addScarsForUncoveredSequences();
            }
        }

        return true;
    }

    private void addScarsForUncoveredSequences() {
        Map<Integer, DnaSequence> uncoveredSequences = findUncoveredSequences();
        int insertCount = 0;
        int lastIndex = elements.size();
        for (Entry<Integer, DnaSequence> entry : uncoveredSequences.entrySet()) {
            int index = entry.getKey();
            DnaSequence seq = entry.getValue();
            if (index >= 0) {
                int updateIndex = index + insertCount;
                DesignElement e = elements.get(updateIndex);
                e.getComponent().setDnaSequence(seq);
            } else {
                int insertIndex = -index - 1 + insertCount++;

                addComponent(Parts.SCAR, false);

                DesignElement e = elements.get(lastIndex);
                e.getComponent().setDnaSequence(seq);

                moveComponent(lastIndex++, insertIndex);
            }
        }

        createDocument();
    }

    private Map<Integer, DnaSequence> findUncoveredSequences() {
        return SBOLUtils.findUncoveredSequences(currentComponent,
                Lists.transform(elements, new Function<DesignElement, SequenceAnnotation>() {
                    @Override
                    public SequenceAnnotation apply(DesignElement e) {
                        return e.getAnnotation();
                    }
                }));
    }

    private void populateComponents(DnaComponent comp) {
        if (comp.getAnnotations().isEmpty()) {
            if (currentComponent != comp) {
                addComponent(comp);
            }
            return;
        }

        Iterable<SequenceAnnotation> sortedAnnotations = sortedAnnotations(comp.getAnnotations());
        //      System.out.println(Iterators.toString(Iterators.transform(sortedAnnotations.iterator(), new Function<SequenceAnnotation,String>() {
        //         public String apply(SequenceAnnotation ann) {
        //            return "\n" + ann.getURI().toString() + " " + ann.getSubComponent().getURI() + " " + ann.getBioStart() + " " + ann.getBioEnd();
        //         }
        //      })));
        int lastStart = -1;
        int lastEnd = -1;
        for (SequenceAnnotation ann : sortedAnnotations) {
            if (ann.getBioStart() != null && ann.getBioEnd() != null) {
                if (ann.getBioStart() >= lastStart && ann.getBioEnd() <= lastEnd) {
                    continue;
                }
                lastStart = ann.getBioStart();
                lastEnd = ann.getBioEnd();
            }

            DnaComponent subComp = ann.getSubComponent();
            if (subComp != null) {
                addComponent(ann, subComp, Parts.forComponent(subComp));
            }
        }
    }

    private Multimap<SequenceAnnotation, SequenceAnnotation> computePrecedesTransitive(
            Iterable<SequenceAnnotation> annotations) {
        Multimap<SequenceAnnotation, SequenceAnnotation> precedes = HashMultimap.create();
        Set<SequenceAnnotation> visited = Sets.newLinkedHashSet();
        for (SequenceAnnotation ann : annotations) {
            computePrecedesTransitive(ann, precedes, visited);
        }
        return precedes;
    }

    private void computePrecedesTransitive(SequenceAnnotation ann,
            Multimap<SequenceAnnotation, SequenceAnnotation> precedes, Set<SequenceAnnotation> visited) {
        if (!visited.add(ann)) {
            LOGGER.warn("Circular precedes relation: " + Iterators
                    .toString(Iterators.transform(visited.iterator(), new Function<SequenceAnnotation, String>() {
                        public String apply(SequenceAnnotation ann) {
                            return ann.getURI().toString();
                        }
                    })));
            return;
        }

        if (!precedes.containsKey(ann)) {
            for (SequenceAnnotation nextAnn : ann.getPrecedes()) {
                computePrecedesTransitive(nextAnn, precedes, visited);
                precedes.put(ann, nextAnn);
                precedes.putAll(ann, precedes.get(nextAnn));
            }
        }

        visited.remove(ann);
    }

    private Iterable<SequenceAnnotation> sortedAnnotations(List<SequenceAnnotation> annotations) {
        final Multimap<SequenceAnnotation, SequenceAnnotation> precedesTransitive = computePrecedesTransitive(
                annotations);
        return new PartialOrder<SequenceAnnotation>(annotations, new PartialOrderComparator<SequenceAnnotation>() {
            @Override
            public PartialOrderRelation compare(SequenceAnnotation a, SequenceAnnotation b) {
                if (precedesTransitive.containsEntry(a, b)) {
                    return PartialOrderRelation.LESS;
                }

                if (precedesTransitive.containsEntry(b, a)) {
                    return PartialOrderRelation.GREATER;
                }

                if (a.getBioStart() != null && a.getBioEnd() != null && b.getBioStart() != null
                        && b.getBioEnd() != null) {
                    int cmpStart = Ints.compare(a.getBioStart(), b.getBioStart());
                    int cmpEnd = Ints.compare(a.getBioEnd(), b.getBioEnd());
                    if (cmpStart < 0) {
                        return PartialOrderRelation.LESS;
                    } else if (cmpStart > 0) {
                        return PartialOrderRelation.GREATER;
                    } else if (cmpEnd < 0) {
                        return PartialOrderRelation.GREATER;
                    } else if (cmpEnd > 0) {
                        return PartialOrderRelation.LESS;
                    } else {
                        return PartialOrderRelation.EQUAL;
                    }
                }

                return PartialOrderRelation.INCOMPARABLE;
            }
        });
    }

    public boolean isCircular() {
        return isCircular;
    }

    public JPanel getPanel() {
        return panel;
    }

    public Part getPart(DnaComponent comp) {
        DesignElement e = getElement(comp);
        return e == null ? null : e.part;
    }

    private DesignElement getElement(DnaComponent comp) {
        int index = getElementIndex(comp);
        return index < 0 ? null : elements.get(index);
    }

    private int getElementIndex(DnaComponent comp) {
        for (int i = 0, n = elements.size(); i < n; i++) {
            DesignElement e = elements.get(i);
            if (e.getComponent() == comp) {
                return i;
            }
        }
        return -1;
    }

    public DnaComponent getRootComponent() {
        return parentComponents.isEmpty() ? currentComponent : parentComponents.getFirst();
    }

    public DnaComponent getCurrentComponent() {
        return currentComponent;
    }

    public DnaComponent getParentComponent() {
        return parentComponents.peek();
    }

    public DnaComponent getSelectedComponent() {
        return selectedElement == null ? null : selectedElement.getComponent();
    }

    public boolean setSelectedComponent(DnaComponent comp) {
        DesignElement e = (comp == null) ? null : getElement(comp);
        setSelectedElement(e);
        return (e != null);
    }

    private void setSelectedElement(DesignElement element) {
        if (selectedElement != null) {
            buttons.get(selectedElement).setEnabled(true);
        }

        selectedElement = element;

        if (selectedElement != null) {
            buttons.get(selectedElement).setEnabled(false);
        }

        fireSelectionChangedEvent();
    }

    public void addComponent(DnaComponent comp) {
        addComponent(null, comp, Parts.forComponent(comp));
    }

    public DnaComponent addComponent(Part part, boolean edit) {
        if (!confirmEditable()) {
            return null;
        }

        DnaComponent comp = part.createComponent();

        if (edit && !PartEditDialog.editPart(panel.getParent(), comp, edit)) {
            return null;
        }

        addComponent(null, comp, part);

        return comp;
    }

    private void addComponent(SequenceAnnotation seqAnn, DnaComponent comp, Part part) {
        boolean backbone = (part == Parts.ORI);
        DesignElement e = new DesignElement(seqAnn, comp, part);
        JLabel button = createComponentButton(e);

        if (backbone) {
            if (isCircular) {
                throw new IllegalArgumentException("Cannot add multiple origin of replication parts");
            }
            elements.add(0, e);
            backboneBox.add(button);
            isCircular = true;
        } else {
            elements.add(e);
            elementBox.add(button);
        }
        buttons.put(e, button);

        if (!isPartVisible(part)) {
            setPartVisible(part, true);
        }

        if (!loading) {
            fireDesignChangedEvent();
        }
    }

    public void moveComponent(int source, int target) {
        if (!confirmEditable()) {
            return;
        }

        DesignElement element = elements.remove(source);
        elements.add(element);

        JLabel button = buttons.get(element);
        elementBox.remove(button);
        elementBox.add(button, target);

        fireDesignChangedEvent();
    }

    private void setupIcons(final JLabel button, final DesignElement e) {
        Image image = e.getPart().getImage(e.getStrand());
        Image selectedImage = Images.createBorderedImage(image, Color.LIGHT_GRAY);
        button.setIcon(new ImageIcon(image));
        button.setDisabledIcon(new ImageIcon(selectedImage));
    }

    private JLabel createComponentButton(final DesignElement e) {
        final JLabel button = new JLabel();
        setupIcons(button, e);
        button.setVerticalAlignment(JLabel.TOP);
        button.setVerticalTextPosition(JLabel.TOP);
        button.setIconTextGap(2);
        button.setText(e.getComponent().getDisplayId());
        button.setVerticalTextPosition(SwingConstants.BOTTOM);
        button.setHorizontalTextPosition(SwingConstants.CENTER);
        button.setToolTipText(getTooltipText(e));
        button.setMaximumSize(new Dimension(IMG_WIDTH, IMG_HEIGHT + 20));
        button.setPreferredSize(new Dimension(IMG_WIDTH, IMG_HEIGHT + 20));
        button.setBorder(BorderFactory.createEmptyBorder());
        button.setFont(LABEL_FONT);
        button.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent event) {
                setSelectedElement(e);
                if (event.isPopupTrigger()) {
                    selectionPopupMenu.show(button, event.getX(), event.getY());
                }
            }

            @Override
            public void mouseClicked(MouseEvent event) {
                if (event.getClickCount() == 2) {
                    focusIn();
                }
            }
        });
        //      button.setComponentPopupMenu(popupMenu);

        boolean isDraggable = (e.getPart() != Parts.ORI);
        if (isDraggable) {
            setupDragActions(button, e);
        }

        return button;
    }

    private void setupDragActions(final JLabel button, final DesignElement e) {
        if (HEADLESS) {
            return;
        }
        final DragSource dragSource = DragSource.getDefaultDragSource();
        dragSource.createDefaultDragGestureRecognizer(button, DnDConstants.ACTION_COPY_OR_MOVE,
                new DragGestureListener() {
                    @Override
                    public void dragGestureRecognized(DragGestureEvent event) {
                        Transferable transferable = new JLabelTransferable(button);
                        dragSource.startDrag(event, DragSource.DefaultMoveDrop, transferable,
                                new DragSourceAdapter() {
                                });
                    }
                });

        new DropTarget(button, new DropTargetAdapter() {
            @Override
            public void drop(DropTargetDropEvent event) {
                int index = elements.indexOf(e);
                if (index >= 0) {
                    Point loc = event.getLocation();
                    if (loc.getX() > button.getWidth() * 0.75 && index < elements.size() - 1) {
                        index++;
                    }
                    moveSelectedElement(index);
                }
                event.dropComplete(true);
            }
        });
    }

    private String getTooltipText(DesignElement e) {
        final DnaComponent comp = e.getComponent();
        StringBuilder sb = new StringBuilder();
        sb.append("<html>");
        sb.append("<b>Display ID:</b> ").append(comp.getDisplayId()).append("<br>");
        sb.append("<b>Name:</b> ").append(Strings.nullToEmpty(comp.getName())).append("<br>");
        sb.append("<b>Description:</b> ").append(Strings.nullToEmpty(comp.getDescription())).append("<br>");
        if (e.getStrand() != null) {
            sb.append("<b>Strand:</b> ").append(e.getStrand()).append("<br>");
        }
        if (comp.getDnaSequence() != null && comp.getDnaSequence().getNucleotides() != null) {
            String sequence = comp.getDnaSequence().getNucleotides();
            sb.append("<b>Sequence Length:</b> ").append(sequence.length()).append("<br>");
            sb.append("<b>Sequence:</b> ").append(CharSequences.shorten(sequence, 25));
            sb.append("<br>");
        }
        sb.append("</html>");
        return sb.toString();
    }

    private void moveSelectedElement(int index) {
        if (selectedElement != null) {
            int selectedIndex = elements.indexOf(selectedElement);
            if (selectedIndex >= 0 && selectedIndex != index) {
                elements.remove(selectedIndex);
                elements.add(index, selectedElement);

                int indexAdjustment = isCircular ? -1 : 0;
                JLabel button = buttons.get(selectedElement);
                elementBox.remove(selectedIndex + indexAdjustment);
                elementBox.add(button, index + indexAdjustment);

                fireDesignChangedEvent();
            }
        }
    }

    public void flipStrand(DnaComponent comp) {
        if (!confirmEditable()) {
            return;
        }

        DesignElement e = getElement(comp);
        e.flipStrand();

        JLabel button = buttons.get(e);
        setupIcons(button, e);
        button.setToolTipText(getTooltipText(e));

        fireDesignChangedEvent();
    }

    public void deleteComponent(DnaComponent component) {
        if (!confirmEditable()) {
            return;
        }

        int index = getElementIndex(component);
        if (index >= 0) {
            DesignElement e = elements.get(index);

            if (e == selectedElement) {
                setSelectedElement(null);
            }

            JLabel button = buttons.remove(e);
            elements.remove(index);
            if (isCircular && index == 0) {
                backboneBox.remove(button);
                isCircular = false;
            } else {
                elementBox.remove(button);
            }

            fireDesignChangedEvent();
        }
    }

    private void replaceComponent(DnaComponent component, DnaComponent newComponent) {
        int index = getElementIndex(component);
        if (index >= 0) {
            DesignElement e = elements.get(index);
            JLabel button = buttons.get(e);
            e.setComponent(newComponent);
            if (!newComponent.getTypes().contains(e.getPart().getType())) {
                Part newPart = Parts.forComponent(newComponent);
                if (newPart == null) {
                    newComponent.addType(e.getPart().getType());
                } else {
                    e.setPart(newPart);
                    setupIcons(button, e);
                }
            }
            button.setText(newComponent.getDisplayId());
            button.setToolTipText(getTooltipText(e));

            fireDesignChangedEvent();
        }
    }

    private void refreshUI() {
        panel.revalidate();
        panel.repaint();
    }

    private void fireDesignChangedEvent() {
        refreshUI();
        eventBus.publish(new DesignChangedEvent(this));
    }

    private void fireSelectionChangedEvent() {
        updateEnabledActions();
        eventBus.publish(new SelectionChangedEvent(getSelectedComponent()));
    }

    private void updateEnabledActions() {
        boolean isEnabled = (selectedElement != null);
        FIND.setEnabled(isEnabled);
        EDIT.setEnabled(isEnabled);
        DELETE.setEnabled(isEnabled);
        FLIP.setEnabled(isEnabled);
        FOCUS_IN.setEnabled(canFocusIn());
        FOCUS_OUT.setEnabled(canFocusOut());
    }

    public boolean isPartVisible(Part part) {
        return !hiddenParts.contains(part);
    }

    public void setPartVisible(Part part, boolean isVisible) {
        boolean visibilityChanged = isVisible ? hiddenParts.remove(part) : hiddenParts.add(part);

        if (visibilityChanged) {
            for (DesignElement e : elements) {
                if (e.getPart().equals(part)) {
                    JLabel button = buttons.get(e);
                    button.setVisible(isVisible);
                }
            }

            if (part.equals(Parts.SCAR)) {
                HIDE_SCARS.putValue(Action.SELECTED_KEY, !isVisible);
            }

            refreshUI();

            eventBus.publish(new PartVisibilityChangedEvent(part, isVisible));

            if (selectedElement != null && part.equals(selectedElement.getPart())) {
                setSelectedElement(null);
            }
        }
    }

    public void addScars() {
        if (!confirmEditable()) {
            return;
        }

        int size = elements.size();
        int start = isCircular ? 1 : 0;
        int end = size - 1;
        DesignElement curr = (size == 0) ? null : elements.get(start);
        for (int i = start; i < end; i++) {
            DesignElement next = elements.get(i + 1);

            if (curr.getPart() != Parts.SCAR && next.getPart() != Parts.SCAR) {
                DesignElement scar = new DesignElement(null, Parts.SCAR.createComponent(), Parts.SCAR);
                JLabel button = createComponentButton(scar);

                elements.add(i + 1, scar);
                elementBox.add(button, i + 1 - start);
                buttons.put(scar, button);
                end++;
                i++;
            }
            curr = next;
        }

        if (size != elements.size()) {
            fireDesignChangedEvent();
        }

        setPartVisible(Parts.SCAR, true);
    }

    public void editRootComponent() {
        if (!confirmEditable()) {
            return;
        }

        DnaComponent comp = getCurrentComponent();

        boolean edited = PartEditDialog.editPart(panel.getParent(), comp, false);

        if (edited) {
            fireDesignChangedEvent();
        }
    }

    public void editSelectedComponent() {
        if (!confirmEditable()) {
            return;
        }

        DnaComponent comp = getSelectedComponent();

        boolean edited = PartEditDialog.editPart(panel.getParent(), comp, false);

        if (edited) {
            try {
                // if the component type or the displyId has been edited we need to update the 
                // component view so we'll replace it with itself
                replaceComponent(comp, comp);
            } catch (Exception e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(panel, "There was an error applying the edits");
            }
        }
    }

    public void findPartForSelectedComponent() {
        Part part = selectedElement.getPart();
        DnaComponent newComponent = new SelectPartDialog(panel.getParent(), part).getInput();

        if (newComponent != null) {
            if (!confirmEditable()) {
                return;
            }

            try {
                replaceComponent(selectedElement.getComponent(), newComponent);
            } catch (Exception e) {
                e.printStackTrace();
                JOptionPane.showMessageDialog(panel, "There was an error adding the selected part to the design");
            }
        }
    }

    public BufferedImage getSnapshot() {
        BufferedImage image = Images.createImage(panel);

        int totalWidth = panel.getWidth();
        int designWidth = elementBox.getWidth();
        int designHeight = elementBox.getHeight();

        int x = (totalWidth - designWidth) / 2;
        if (isCircular) {
            x -= IMG_PAD;
            designWidth += (2 * IMG_PAD);
            designHeight += backboneBox.getHeight();
        }

        return image.getSubimage(Math.max(0, x - IMG_PAD), 0, Math.min(designWidth + 2 * IMG_PAD, totalWidth),
                designHeight);
    }

    public SBOLDocument createDocument() {
        updateRootComponent();

        DnaComponent comp = parentComponents.isEmpty() ? currentComponent : parentComponents.getFirst();
        SBOLDocument doc = SBOLFactory.createDocument();
        doc.addContent(comp);

        return doc;
    }

    private void updateRootComponent() {
        currentComponent.getAnnotations().clear();

        StringBuilder rootSequence = new StringBuilder();
        int location = 1;
        SequenceAnnotation prev = null;
        for (DesignElement e : elements) {
            DnaComponent comp = e.getComponent();
            SequenceAnnotation ann = e.getAnnotation();

            if (location >= 0 && comp.getDnaSequence() != null && comp.getDnaSequence().getNucleotides() != null) {
                String nucleotides = comp.getDnaSequence().getNucleotides();
                rootSequence.append(nucleotides);
                ann.setBioStart(location);
                location += nucleotides.length();
                ann.setBioEnd(location - 1);
            } else {
                location = -1;
                ann.setBioStart(null);
                ann.setBioEnd(null);
            }

            if (prev != null) {
                prev.getPrecedes().clear();
                prev.addPrecede(ann);
            }

            currentComponent.addAnnotation(ann);
            prev = ann;
        }

        if (location > 0 && !elements.isEmpty()) {
            DnaSequence seq = SBOLFactory.createDnaSequence();
            seq.setURI(SBOLUtils.createURI());
            seq.setNucleotides(rootSequence.toString());

            currentComponent.setDnaSequence(seq);
        } else if (!hasSequence) {
            currentComponent.setDnaSequence(null);
        }

        LOGGER.debug("Updated root:\n{}", new SBOLTextWriter().write(currentComponent));
    }

    private static class DesignElement {
        private final SequenceAnnotation seqAnn;
        private Part part;

        public DesignElement(SequenceAnnotation sa, DnaComponent comp, Part part) {
            this.seqAnn = sa != null ? sa : createAnnotation(comp);
            this.part = part;
        }

        private static SequenceAnnotation createAnnotation(DnaComponent component) {
            SequenceAnnotation seqAnn = SublimeSBOLFactory.createSequenceAnnotation();
            seqAnn.setURI(SBOLUtils.createURI());
            seqAnn.setSubComponent(component);
            seqAnn.setStrand(StrandType.POSITIVE);
            return seqAnn;
        }

        SequenceAnnotation getAnnotation() {
            return seqAnn;
        }

        void setComponent(DnaComponent component) {
            seqAnn.setSubComponent(component);
        }

        DnaComponent getComponent() {
            return seqAnn.getSubComponent();
        }

        void setPart(Part part) {
            this.part = part;
        }

        Part getPart() {
            return part;
        }

        public StrandType getStrand() {
            return seqAnn.getStrand();
        }

        void flipStrand() {
            StrandType strand = seqAnn.getStrand();
            seqAnn.setStrand(strand == StrandType.NEGATIVE ? StrandType.POSITIVE : StrandType.NEGATIVE);
        }

        public String toString() {
            return getComponent().getDisplayId() + (seqAnn.getStrand() == StrandType.NEGATIVE ? "-" : "");
        }
    }

    private class DesignPanel extends JPanel {
        private static final long serialVersionUID = 1L;

        @Override
        protected void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;

            // clear the background
            g2d.setColor(Color.white);
            g2d.fillRect(0, 0, getWidth(), getHeight());

            // draw the line
            g2d.setColor(Color.black);
            g2d.setPaint(Color.black);
            g2d.setStroke(new BasicStroke(4.0f));

            if (!elements.isEmpty()) {
                int totalWidth = getWidth();
                int designWidth = Math.max(elementBox.getWidth(), backboneBox.getWidth());

                int x = (totalWidth - designWidth) / 2;
                int y = IMG_HEIGHT / 2;

                if (!isCircular) {
                    g.drawLine(x, y, totalWidth - x, y);
                } else {
                    g.drawRoundRect(x - IMG_PAD, y, designWidth + 2 * IMG_PAD, backboneBox.getHeight(), IMG_PAD,
                            IMG_PAD);
                }
            }

            // draw the rest
            super.paintComponent(g);
        }
    }

    private static class JLabelTransferable implements Transferable {
        // A flavor that transfers a copy of the JLabel
        public static final DataFlavor FLAVOR = new DataFlavor(JButton.class, "JLabel");

        private static final DataFlavor[] FLAVORS = new DataFlavor[] { FLAVOR };

        private JLabel label; // The label being transferred

        public JLabelTransferable(JLabel label) {
            this.label = label;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return FLAVORS;
        }

        public boolean isDataFlavorSupported(DataFlavor fl) {
            return fl.equals(FLAVOR);
        }

        public Object getTransferData(DataFlavor fl) {
            if (!isDataFlavorSupported(fl)) {
                return null;
            }

            return label;
        }
    }
}