Merge branch 'develop' into trial_merge/JAL-1950
[jalview.git] / src / jalview / gui / FeatureSettings.java
index d65016e..bd74db5 100644 (file)
@@ -23,10 +23,11 @@ package jalview.gui;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
 import jalview.bin.Cache;
-import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
+import jalview.io.JalviewFileView;
 import jalview.schemabinding.version2.JalviewUserColours;
 import jalview.schemes.FeatureColour;
 import jalview.util.Format;
@@ -39,6 +40,7 @@ import jalview.ws.dbsources.das.api.jalviewSourceI;
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
+import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
@@ -59,6 +61,7 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -79,7 +82,6 @@ import javax.swing.JInternalFrame;
 import javax.swing.JLabel;
 import javax.swing.JLayeredPane;
 import javax.swing.JMenuItem;
-import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
@@ -95,8 +97,8 @@ import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
 
-public class FeatureSettings extends JPanel implements
-        FeatureSettingsControllerI
+public class FeatureSettings extends JPanel
+        implements FeatureSettingsControllerI
 {
   DasSourceBrowser dassourceBrowser;
 
@@ -126,13 +128,22 @@ public class FeatureSettings extends JPanel implements
 
   JPanel transPanel = new JPanel(new GridLayout(1, 2));
 
+  private static final int MIN_WIDTH = 400;
+
+  private static final int MIN_HEIGHT = 400;
+
+  /**
+   * Constructor
+   * 
+   * @param af
+   */
   public FeatureSettings(AlignFrame af)
   {
     this.af = af;
     fr = af.getFeatureRenderer();
     // allow transparency to be recovered
-    transparency.setMaximum(100 - (int) ((originalTransparency = fr
-            .getTransparency()) * 100));
+    transparency.setMaximum(100
+            - (int) ((originalTransparency = fr.getTransparency()) * 100));
 
     try
     {
@@ -278,17 +289,19 @@ public class FeatureSettings extends JPanel implements
               MessageManager.getString("label.sequence_feature_settings"),
               400, 450);
     }
+    frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
-    frame.addInternalFrameListener(new javax.swing.event.InternalFrameAdapter()
-    {
-      @Override
-      public void internalFrameClosed(
-              javax.swing.event.InternalFrameEvent evt)
-      {
-        fr.removePropertyChangeListener(change);
-        dassourceBrowser.fs = null;
-      };
-    });
+    frame.addInternalFrameListener(
+            new javax.swing.event.InternalFrameAdapter()
+            {
+              @Override
+              public void internalFrameClosed(
+                      javax.swing.event.InternalFrameEvent evt)
+              {
+                fr.removePropertyChangeListener(change);
+                dassourceBrowser.fs = null;
+              };
+            });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
   }
 
@@ -298,8 +311,9 @@ public class FeatureSettings extends JPanel implements
   {
     final FeatureColourI featureColour = (FeatureColourI) typeCol;
 
-    JPopupMenu men = new JPopupMenu(MessageManager.formatMessage(
-            "label.settings_for_param", new String[] { type }));
+    JPopupMenu men = new JPopupMenu(MessageManager
+            .formatMessage("label.settings_for_param", new String[]
+            { type }));
     JMenuItem scr = new JMenuItem(
             MessageManager.getString("label.sort_by_score"));
     men.add(scr);
@@ -310,8 +324,9 @@ public class FeatureSettings extends JPanel implements
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.af.avc.sortAlignmentByFeatureScore(Arrays
-                .asList(new String[] { type }));
+        me.af.avc
+                .sortAlignmentByFeatureScore(Arrays.asList(new String[]
+                { type }));
       }
 
     });
@@ -323,8 +338,9 @@ public class FeatureSettings extends JPanel implements
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.af.avc.sortAlignmentByFeatureDensity(Arrays
-                .asList(new String[] { type }));
+        me.af.avc
+                .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
+                { type }));
       }
 
     });
@@ -394,8 +410,7 @@ public class FeatureSettings extends JPanel implements
               else
               {
                 // probably the color chooser!
-                table.setValueAt(
-                        new FeatureColour(colorChooser.getColor()),
+                table.setValueAt(new FeatureColour(colorChooser.getColor()),
                         selectedRow, 1);
                 table.validate();
                 me.updateFeatureRenderer(
@@ -419,8 +434,8 @@ public class FeatureSettings extends JPanel implements
                 false, type);
       }
     });
-    JMenuItem clearCols = new JMenuItem(
-            MessageManager.getString("label.select_columns_not_containing"));
+    JMenuItem clearCols = new JMenuItem(MessageManager
+            .getString("label.select_columns_not_containing"));
     clearCols.addActionListener(new ActionListener()
     {
       @Override
@@ -463,50 +478,26 @@ public class FeatureSettings extends JPanel implements
   private boolean handlingUpdate = false;
 
   /**
-   * contains a float[3] for each feature type string. created by setTableData
+   * holds {featureCount, totalExtent} for each feature type
    */
   Map<String, float[]> typeWidth = null;
 
   @Override
   synchronized public void discoverAllFeatureData()
   {
-    Vector<String> allFeatures = new Vector<String>();
-    Vector<String> allGroups = new Vector<String>();
-    SequenceFeature[] tmpfeatures;
-    String group;
-    for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
-    {
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+    Set<String> allGroups = new HashSet<String>();
+    AlignmentI alignment = af.getViewport().getAlignment();
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+    for (int i = 0; i < alignment.getHeight(); i++)
+    {
+      SequenceI seq = alignment.getSequenceAt(i);
+      for (String group : seq.getFeatures().getFeatureGroups(true))
       {
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
-        if (tmpfeatures[index].getFeatureGroup() != null)
-        {
-          group = tmpfeatures[index].featureGroup;
-          if (!allGroups.contains(group))
-          {
-            allGroups.addElement(group);
-            checkGroupState(group);
-          }
-        }
-
-        if (!allFeatures.contains(tmpfeatures[index].getType()))
+        if (group != null && !allGroups.contains(group))
         {
-          allFeatures.addElement(tmpfeatures[index].getType());
+          allGroups.add(group);
+          checkGroupState(group);
         }
-        index++;
       }
     }
 
@@ -525,27 +516,15 @@ public class FeatureSettings extends JPanel implements
   {
     boolean visible = fr.checkGroupVisibility(group, true);
 
-    if (groupPanel == null)
-    {
-      groupPanel = new JPanel();
-    }
-
-    boolean alreadyAdded = false;
     for (int g = 0; g < groupPanel.getComponentCount(); g++)
     {
       if (((JCheckBox) groupPanel.getComponent(g)).getText().equals(group))
       {
-        alreadyAdded = true;
         ((JCheckBox) groupPanel.getComponent(g)).setSelected(visible);
-        break;
+        return visible;
       }
     }
 
-    if (alreadyAdded)
-    {
-
-      return visible;
-    }
     final String grp = group;
     final JCheckBox check = new JCheckBox(group, visible);
     check.setFont(new Font("Serif", Font.BOLD, 12));
@@ -572,7 +551,7 @@ public class FeatureSettings extends JPanel implements
 
   synchronized void resetTable(String[] groupChanged)
   {
-    if (resettingTable == true)
+    if (resettingTable)
     {
       return;
     }
@@ -580,69 +559,59 @@ public class FeatureSettings extends JPanel implements
     typeWidth = new Hashtable<String, float[]>();
     // TODO: change avWidth calculation to 'per-sequence' average and use long
     // rather than float
-    float[] avWidth = null;
-    SequenceFeature[] tmpfeatures;
-    String group = null, type;
-    Vector<String> visibleChecks = new Vector<String>();
-
-    // Find out which features should be visible depending on which groups
-    // are selected / deselected
-    // and recompute average width ordering
+
+    Set<String> displayableTypes = new HashSet<String>();
+    Set<String> foundGroups = new HashSet<String>();
+
+    /*
+     * determine which feature types may be visible depending on 
+     * which groups are selected, and recompute average width data
+     */
     for (int i = 0; i < af.getViewport().getAlignment().getHeight(); i++)
     {
 
-      tmpfeatures = af.getViewport().getAlignment().getSequenceAt(i)
-              .getSequenceFeatures();
-      if (tmpfeatures == null)
-      {
-        continue;
-      }
+      SequenceI seq = af.getViewport().getAlignment().getSequenceAt(i);
 
-      int index = 0;
-      while (index < tmpfeatures.length)
+      /*
+       * get the sequence's groups for positional features
+       * and keep track of which groups are visible
+       */
+      Set<String> groups = seq.getFeatures().getFeatureGroups(true);
+      Set<String> visibleGroups = new HashSet<String>();
+      for (String group : groups)
       {
-        group = tmpfeatures[index].featureGroup;
-
-        if (tmpfeatures[index].begin == 0 && tmpfeatures[index].end == 0)
-        {
-          index++;
-          continue;
-        }
-
         if (group == null || checkGroupState(group))
         {
-          type = tmpfeatures[index].getType();
-          if (!visibleChecks.contains(type))
-          {
-            visibleChecks.addElement(type);
-          }
-        }
-        if (!typeWidth.containsKey(tmpfeatures[index].getType()))
-        {
-          typeWidth.put(tmpfeatures[index].getType(),
-                  avWidth = new float[3]);
-        }
-        else
-        {
-          avWidth = typeWidth.get(tmpfeatures[index].getType());
-        }
-        avWidth[0]++;
-        if (tmpfeatures[index].getBegin() > tmpfeatures[index].getEnd())
-        {
-          avWidth[1] += 1 + tmpfeatures[index].getBegin()
-                  - tmpfeatures[index].getEnd();
+          visibleGroups.add(group);
         }
-        else
+      }
+      foundGroups.addAll(groups);
+
+      /*
+       * get distinct feature types for visible groups
+       * record distinct visible types, and their count and total length
+       */
+      Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
+              visibleGroups.toArray(new String[visibleGroups.size()]));
+      for (String type : types)
+      {
+        displayableTypes.add(type);
+        float[] avWidth = typeWidth.get(type);
+        if (avWidth == null)
         {
-          avWidth[1] += 1 + tmpfeatures[index].getEnd()
-                  - tmpfeatures[index].getBegin();
+          avWidth = new float[2];
+          typeWidth.put(type, avWidth);
         }
-        index++;
+        // todo this could include features with a non-visible group
+        // - do we greatly care?
+        // todo should we include non-displayable features here, and only
+        // update when features are added?
+        avWidth[0] += seq.getFeatures().getFeatureCount(true, type);
+        avWidth[1] += seq.getFeatures().getTotalFeatureLength(type);
       }
     }
 
-    int fSize = visibleChecks.size();
-    Object[][] data = new Object[fSize][3];
+    Object[][] data = new Object[displayableTypes.size()][3];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -658,28 +627,29 @@ public class FeatureSettings extends JPanel implements
       List<String> frl = fr.getRenderOrder();
       for (int ro = frl.size() - 1; ro > -1; ro--)
       {
-        type = frl.get(ro);
+        String type = frl.get(ro);
 
-        if (!visibleChecks.contains(type))
+        if (!displayableTypes.contains(type))
         {
           continue;
         }
 
         data[dataIndex][0] = type;
         data[dataIndex][1] = fr.getFeatureStyle(type);
-        data[dataIndex][2] = new Boolean(af.getViewport()
-                .getFeaturesDisplayed().isVisible(type));
+        data[dataIndex][2] = new Boolean(
+                af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
-        visibleChecks.removeElement(type);
+        displayableTypes.remove(type);
       }
     }
 
-    fSize = visibleChecks.size();
-    for (int i = 0; i < fSize; i++)
+    /*
+     * process any extra features belonging only to 
+     * a group which was just selected
+     */
+    while (!displayableTypes.isEmpty())
     {
-      // These must be extra features belonging to the group
-      // which was just selected
-      type = visibleChecks.elementAt(i).toString();
+      String type = displayableTypes.iterator().next();
       data[dataIndex][0] = type;
 
       data[dataIndex][1] = fr.getFeatureStyle(type);
@@ -692,6 +662,7 @@ public class FeatureSettings extends JPanel implements
 
       data[dataIndex][2] = new Boolean(true);
       dataIndex++;
+      displayableTypes.remove(type);
     }
 
     if (originalData == null)
@@ -702,24 +673,105 @@ public class FeatureSettings extends JPanel implements
         System.arraycopy(data[i], 0, originalData[i], 0, 3);
       }
     }
+    else
+    {
+      updateOriginalData(data);
+    }
 
     table.setModel(new FeatureTableModel(data));
     table.getColumnModel().getColumn(0).setPreferredWidth(200);
 
-    if (groupPanel != null)
-    {
-      groupPanel.setLayout(new GridLayout(
-              fr.getFeatureGroupsSize() / 4 + 1, 4));
-
-      groupPanel.validate();
-      bigPanel.add(groupPanel, BorderLayout.NORTH);
-    }
+    groupPanel.setLayout(
+            new GridLayout(fr.getFeatureGroupsSize() / 4 + 1, 4));
+    pruneGroups(foundGroups);
+    groupPanel.validate();
 
     updateFeatureRenderer(data, groupChanged != null);
     resettingTable = false;
   }
 
   /**
+   * Updates 'originalData' (used for restore on Cancel) if we detect that
+   * changes have been made outwith this dialog
+   * <ul>
+   * <li>a new feature type added (and made visible)</li>
+   * <li>a feature colour changed (in the Amend Features dialog)</li>
+   * </ul>
+   * 
+   * @param foundData
+   */
+  protected void updateOriginalData(Object[][] foundData)
+  {
+    // todo LinkedHashMap instead of Object[][] would be nice
+
+    Object[][] currentData = ((FeatureTableModel) table.getModel())
+            .getData();
+    for (Object[] row : foundData)
+    {
+      String type = (String) row[0];
+      boolean found = false;
+      for (Object[] current : currentData)
+      {
+        if (type.equals(current[0]))
+        {
+          found = true;
+          /*
+           * currently dependent on object equality here;
+           * really need an equals method on FeatureColour
+           */
+          if (!row[1].equals(current[1]))
+          {
+            /*
+             * feature colour has changed externally - update originalData
+             */
+            for (Object[] original : originalData)
+            {
+              if (type.equals(original[0]))
+              {
+                original[1] = row[1];
+                break;
+              }
+            }
+          }
+          break;
+        }
+      }
+      if (!found)
+      {
+        /*
+         * new feature detected - add to original data (on top)
+         */
+        Object[][] newData = new Object[originalData.length + 1][3];
+        for (int i = 0; i < originalData.length; i++)
+        {
+          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
+        }
+        newData[0] = row;
+        originalData = newData;
+      }
+    }
+  }
+
+  /**
+   * Remove from the groups panel any checkboxes for groups that are not in the
+   * foundGroups set. This enables removing a group from the display when the
+   * last feature in that group is deleted.
+   * 
+   * @param foundGroups
+   */
+  protected void pruneGroups(Set<String> foundGroups)
+  {
+    for (int g = 0; g < groupPanel.getComponentCount(); g++)
+    {
+      JCheckBox checkbox = (JCheckBox) groupPanel.getComponent(g);
+      if (!foundGroups.contains(checkbox.getText()))
+      {
+        groupPanel.remove(checkbox);
+      }
+    }
+  }
+
+  /**
    * reorder data based on the featureRenderers global priority list.
    * 
    * @param data
@@ -748,12 +800,11 @@ public class FeatureSettings extends JPanel implements
 
   void load()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "fc",
-            "Sequence Feature Colours", "Sequence Feature Colours");
-    chooser.setFileView(new jalview.io.JalviewFileView());
-    chooser.setDialogTitle(MessageManager
-            .getString("label.load_feature_colours"));
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
+            "Sequence Feature Colours");
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(
+            MessageManager.getString("label.load_feature_colours"));
     chooser.setToolTipText(MessageManager.getString("action.load"));
 
     int value = chooser.showOpenDialog(this);
@@ -764,8 +815,8 @@ public class FeatureSettings extends JPanel implements
 
       try
       {
-        InputStreamReader in = new InputStreamReader(new FileInputStream(
-                file), "UTF-8");
+        InputStreamReader in = new InputStreamReader(
+                new FileInputStream(file), "UTF-8");
 
         JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 
@@ -841,12 +892,11 @@ public class FeatureSettings extends JPanel implements
 
   void save()
   {
-    JalviewFileChooser chooser = new JalviewFileChooser(
-            Cache.getProperty("LAST_DIRECTORY"), "fc",
-            "Sequence Feature Colours", "Sequence Feature Colours");
-    chooser.setFileView(new jalview.io.JalviewFileView());
-    chooser.setDialogTitle(MessageManager
-            .getString("label.save_feature_colours"));
+    JalviewFileChooser chooser = new JalviewFileChooser("fc",
+            "Sequence Feature Colours");
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(
+            MessageManager.getString("label.save_feature_colours"));
     chooser.setToolTipText(MessageManager.getString("action.save"));
 
     int value = chooser.showSaveDialog(this);
@@ -888,13 +938,13 @@ public class FeatureSettings extends JPanel implements
             col.setRGB(Format.getHexString(fcol.getMaxColour()));
             col.setMin(fcol.getMin());
             col.setMax(fcol.getMax());
-            col.setMinRGB(jalview.util.Format.getHexString(fcol
-                    .getMinColour()));
+            col.setMinRGB(
+                    jalview.util.Format.getHexString(fcol.getMinColour()));
             col.setAutoScale(fcol.isAutoScaled());
             col.setThreshold(fcol.getThreshold());
             col.setColourByLabel(fcol.isColourByLabel());
-            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE" : (fcol
-                    .isBelowThreshold() ? "BELOW" : "NONE"));
+            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
+                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
           }
           ucs.addColour(col);
         }
@@ -1061,6 +1111,10 @@ public class FeatureSettings extends JPanel implements
     settingsPane.setLayout(borderLayout2);
     dasSettingsPane.setLayout(borderLayout3);
     bigPanel.setLayout(borderLayout4);
+
+    groupPanel = new JPanel();
+    bigPanel.add(groupPanel, BorderLayout.NORTH);
+
     invert.setFont(JvSwingUtils.getLabelFont());
     invert.setText(MessageManager.getString("label.invert_selection"));
     invert.addActionListener(new ActionListener()
@@ -1093,8 +1147,8 @@ public class FeatureSettings extends JPanel implements
       }
     });
     sortByDens.setFont(JvSwingUtils.getLabelFont());
-    sortByDens.setText(MessageManager
-            .getString("label.sequence_sort_by_density"));
+    sortByDens.setText(
+            MessageManager.getString("label.sequence_sort_by_density"));
     sortByDens.addActionListener(new ActionListener()
     {
       @Override
@@ -1188,8 +1242,8 @@ public class FeatureSettings extends JPanel implements
     });
 
     transparency.setMaximum(70);
-    transparency.setToolTipText(MessageManager
-            .getString("label.transparency_tip"));
+    transparency.setToolTipText(
+            MessageManager.getString("label.transparency_tip"));
     fetchDAS.setText(MessageManager.getString("label.fetch_das_features"));
     fetchDAS.addActionListener(new ActionListener()
     {
@@ -1399,15 +1453,10 @@ public class FeatureSettings extends JPanel implements
   public void noDasSourceActive()
   {
     complete();
-    JvOptionPane
-            .showInternalConfirmDialog(
-                    Desktop.desktop,
-                    MessageManager
-                            .getString("label.no_das_sources_selected_warn"),
-                    MessageManager
-                            .getString("label.no_das_sources_selected_title"),
-                    JvOptionPane.DEFAULT_OPTION,
-                    JvOptionPane.INFORMATION_MESSAGE);
+    JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
+            MessageManager.getString("label.no_das_sources_selected_warn"),
+            MessageManager.getString("label.no_das_sources_selected_title"),
+            JvOptionPane.DEFAULT_OPTION, JvOptionPane.INFORMATION_MESSAGE);
   }
 
   // ///////////////////////////////////////////////////////////////////////
@@ -1504,9 +1553,8 @@ public class FeatureSettings extends JPanel implements
     }
 
     @Override
-    public Component getTableCellRendererComponent(JTable tbl,
-            Object color, boolean isSelected, boolean hasFocus, int row,
-            int column)
+    public Component getTableCellRendererComponent(JTable tbl, Object color,
+            boolean isSelected, boolean hasFocus, int row, int column)
     {
       FeatureColourI cellColour = (FeatureColourI) color;
       // JLabel comp = new JLabel();
@@ -1714,8 +1762,8 @@ class FeatureIcon implements Icon
   }
 }
 
-class ColorEditor extends AbstractCellEditor implements TableCellEditor,
-        ActionListener
+class ColorEditor extends AbstractCellEditor
+        implements TableCellEditor, ActionListener
 {
   FeatureSettings me;