Merge branch 'develop' into trialMerge
[jalview.git] / src / jalview / gui / FeatureTypeSettings.java
index 7456e18..50efffc 100644 (file)
@@ -30,6 +30,8 @@ import jalview.datamodel.features.FeatureMatcher;
 import jalview.datamodel.features.FeatureMatcherI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.FeatureColour;
 import jalview.util.ColorUtils;
 import jalview.util.MessageManager;
@@ -50,7 +52,12 @@ import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 import javax.swing.BorderFactory;
 import javax.swing.BoxLayout;
@@ -118,10 +125,11 @@ public class FeatureTypeSettings extends JalviewDialog
 
   /*
    * the colour and filters to reset to on Cancel
+   * (including feature sub-types if modified)
    */
-  private final FeatureColourI originalColour;
+  private Map<String, FeatureColourI> originalColours;
 
-  private final FeatureMatcherSetI originalFilter;
+  private Map<String, FeatureMatcherSetI> originalFilters;
 
   /*
    * set flag to true when setting values programmatically,
@@ -206,6 +214,31 @@ public class FeatureTypeSettings extends JalviewDialog
 
   private JPanel chooseFiltersPanel;
 
+  /*
+   * the root Sequence Ontology terms (if any) that is a parent of
+   * the current feature type
+   */
+  private String rootSOTerm;
+
+  /*
+   * a map whose keys are Sequence Ontology terms - selected from the
+   * current term and its parents in the SO - whose subterms include
+   * additional feature types; the map entry is the list of additional
+   * feature types that match the key or have it as a parent term; in
+   * other words, distinct 'aggregations' that include the current feature type
+   */
+  private final Map<String, List<String>> relatedSoTerms;
+
+  /*
+   * if true, filter or colour settings are also applied to 
+   * any sub-types of parentTerm in the Sequence Ontology
+   */
+  private boolean applyFiltersToSubtypes;
+
+  private boolean applyColourToSubtypes;
+
+  private String parentSOTerm;
+
   /**
    * Constructor
    * 
@@ -217,11 +250,30 @@ public class FeatureTypeSettings extends JalviewDialog
     this.fr = frender;
     this.featureType = theType;
     ap = fr.ap;
-    originalFilter = fr.getFeatureFilter(theType);
-    originalColour = fr.getFeatureColours().get(theType);
-    
+
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    relatedSoTerms = so.findSequenceOntologyGroupings(
+            this.featureType, fr.getRenderOrder());
+
+    /*
+     * save original colours and filters for this feature type,
+     * and any related types, to restore on Cancel
+     */
+    originalFilters = new HashMap<>();
+    originalFilters.put(theType, fr.getFeatureFilter(theType));
+    originalColours = new HashMap<>();
+    originalColours.put(theType, fr.getFeatureColours().get(theType));
+    for (List<String> related : relatedSoTerms.values())
+    {
+      for (String type : related)
+      {
+        originalFilters.put(type, fr.getFeatureFilter(type));
+        originalColours.put(type, fr.getFeatureColours().get(type));
+      }
+    }
+
     adjusting = true;
-    
+
     try
     {
       initialise();
@@ -230,15 +282,15 @@ public class FeatureTypeSettings extends JalviewDialog
       ex.printStackTrace();
       return;
     }
-    
+
     updateColoursTab();
-    
+
     updateFiltersTab();
-    
+
     adjusting = false;
-    
+
     colourChanged(false);
-    
+
     String title = MessageManager
             .formatMessage("label.display_settings_for", new String[]
             { theType });
@@ -247,6 +299,60 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
+   * Answers a (possibly empty) map of any Sequence Ontology terms (the current
+   * feature type and its parents) which incorporate additional known feature
+   * types (the map entry).
+   * <p>
+   * For example if {@code stop_gained} and {@code stop_lost} are known feature
+   * types, then SO term {@ nonsynonymous_variant} is the first common parent of
+   * both terms
+   * 
+   * @param featureType
+   *          the current feature type being configured
+   * @param featureTypes
+   *          all known feature types on the alignment
+   * @return
+   */
+  protected static Map<String, List<String>> findSequenceOntologyGroupings(
+          String featureType, List<String> featureTypes)
+  {
+    List<String> sortedTypes = new ArrayList<>(featureTypes);
+    Collections.sort(sortedTypes);
+
+    Map<String, List<String>> parents = new HashMap<>();
+
+    /*
+     * method: 
+     * walk up featureType and all of its parents
+     * find other feature types which are subsumed by each term
+     * add each distinct aggregation of included feature types to the map
+     */
+    List<String> candidates = new ArrayList<>();
+    SequenceOntologyI so = SequenceOntologyFactory.getInstance();
+    candidates.add(featureType);
+    while (!candidates.isEmpty())
+    {
+      String term = candidates.remove(0);
+      List<String> includedFeatures = new ArrayList<>();
+      for (String type : sortedTypes)
+      {
+        if (!type.equals(featureType) && so.isA(type, term))
+        {
+          includedFeatures.add(type);
+        }
+      }
+      if (!includedFeatures.isEmpty()
+              && !parents.containsValue(includedFeatures))
+      {
+        parents.put(term, includedFeatures);
+      }
+      candidates.addAll(so.getParents(term));
+    }
+    
+    return parents;
+  }
+
+  /**
    * Configures the widgets on the Colours tab according to the current feature
    * colour scheme
    */
@@ -565,6 +671,7 @@ public class FeatureTypeSettings extends JalviewDialog
      * if not set, default max colour to last plain colour,
      * and make min colour a pale version of max colour
      */
+    FeatureColourI originalColour = originalColours.get(featureType);
     Color max = originalColour.getMaxColour();
     if (max == null)
     {
@@ -730,6 +837,15 @@ public class FeatureTypeSettings extends JalviewDialog
             MessageManager.getString("action.colour"), true);
 
     /*
+     * option to apply colour to other selected types as well
+     */
+    if (!relatedSoTerms.isEmpty())
+    {
+      applyColourToSubtypes = false;
+      colourByPanel.add(initSubtypesPanel(false));
+    }
+
+    /*
      * simple colour radio button and colour picker
      */
     JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
@@ -754,16 +870,10 @@ public class FeatureTypeSettings extends JalviewDialog
     singleColour.setFont(JvSwingUtils.getLabelFont());
     singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
     singleColour.setPreferredSize(new Dimension(40, 20));
-    // if (originalColour.isGraduatedColour())
-    // {
-    // singleColour.setBackground(originalColour.getMaxColour());
-    // singleColour.setForeground(originalColour.getMaxColour());
-    // }
-    // else
-    // {
-      singleColour.setBackground(originalColour.getColour());
-      singleColour.setForeground(originalColour.getColour());
-    // }
+    FeatureColourI originalColour = originalColours.get(featureType);
+    singleColour.setBackground(originalColour.getColour());
+    singleColour.setForeground(originalColour.getColour());
+
     singleColour.addMouseListener(new MouseAdapter()
     {
       @Override
@@ -833,6 +943,86 @@ public class FeatureTypeSettings extends JalviewDialog
     return colourByPanel;
   }
 
+  /**
+   * Constructs and returns a panel with the option to apply any changes also to
+   * sub-types of SO terms at or above the feature type
+   * 
+   * @return
+   */
+  protected JPanel initSubtypesPanel(final boolean forFilters)
+  {
+    JPanel toSubtypes = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    toSubtypes.setBackground(Color.WHITE);
+
+    /*
+     * checkbox 'apply to sub-types of...'
+     */
+    JCheckBox applyToSubtypesCB = new JCheckBox(MessageManager
+            .formatMessage("label.apply_to_subtypes", rootSOTerm));
+    toSubtypes.add(applyToSubtypesCB);
+    toSubtypes
+            .setToolTipText(MessageManager.getString("label.group_by_so"));
+
+    /*
+     * combobox to choose 'parent' of sub-types
+     */
+    List<String> soTerms = new ArrayList<>();
+    for (String term : relatedSoTerms.keySet())
+    {
+      soTerms.add(term);
+    }
+    // sort from most restrictive to most inclusive
+    Collections.sort(soTerms, new Comparator<String>()
+    {
+      @Override
+      public int compare(String o1, String o2)
+      {
+        return Integer.compare(relatedSoTerms.get(o1).size(),
+                relatedSoTerms.get(o2).size());
+      }
+    });
+    List<String> tooltips = new ArrayList<>();
+    for (String term : soTerms)
+    {
+      tooltips.add(getSOTermsTooltip(relatedSoTerms.get(term)));
+    }
+    JComboBox<String> parentType = JvSwingUtils
+            .buildComboWithTooltips(soTerms, tooltips);
+    toSubtypes.add(parentType);
+
+    /*
+     * on toggle of checkbox, or change of parent SO term,
+     * reset and then reapply filters to the selected scope
+     */
+    final ActionListener action = new ActionListener()
+    {
+      /*
+       * reset and reapply settings on toggle of checkbox
+       */
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        parentSOTerm = (String) parentType.getSelectedItem();
+        if (forFilters)
+        {
+          applyFiltersToSubtypes = applyToSubtypesCB.isSelected();
+          restoreOriginalFilters();
+          filtersChanged();
+        }
+        else
+        {
+          applyColourToSubtypes = applyToSubtypesCB.isSelected();
+          restoreOriginalColours();
+          colourChanged(true);
+        }
+      }
+    };
+    applyToSubtypesCB.addActionListener(action);
+    parentType.addActionListener(action);
+
+    return toSubtypes;
+  }
+
   private void showColourChooser(JPanel colourPanel, String key)
   {
     Color col = JColorChooser.showDialog(this,
@@ -872,9 +1062,17 @@ public class FeatureTypeSettings extends JalviewDialog
     FeatureColourI acg = makeColourFromInputs();
 
     /*
-     * save the colour, and repaint stuff
+     * save the colour, and set on subtypes if selected
      */
     fr.setColour(featureType, acg);
+    if (applyColourToSubtypes)
+    {
+      for (String child : relatedSoTerms.get(parentSOTerm))
+      {
+        fr.setColour(child, acg);
+      }
+    }
+    refreshFeatureSettings();
     ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
 
     updateColoursTab();
@@ -990,9 +1188,14 @@ public class FeatureTypeSettings extends JalviewDialog
   @Override
   protected void raiseClosed()
   {
+    refreshFeatureSettings();
+  }
+
+  protected void refreshFeatureSettings()
+  {
     if (this.featureSettings != null)
     {
-      featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
+      featureSettings.actionPerformed(new ActionEvent(this, 0, "REFRESH"));
     }
   }
 
@@ -1007,17 +1210,43 @@ public class FeatureTypeSettings extends JalviewDialog
 
   /**
    * Action on Cancel is to restore colour scheme and filters as they were when
-   * the dialog was opened
+   * the dialog was opened (including any feature sub-types that may have been
+   * changed)
    */
   @Override
   public void cancelPressed()
   {
-    fr.setColour(featureType, originalColour);
-    fr.setFeatureFilter(featureType, originalFilter);
+    restoreOriginalColours();
+    restoreOriginalFilters();
     ap.paintAlignment(true, true);
   }
 
   /**
+   * Restores filters for all feature types to their values when the dialog was
+   * opened
+   */
+  protected void restoreOriginalFilters()
+  {
+    for (Entry<String, FeatureMatcherSetI> entry : originalFilters
+            .entrySet())
+    {
+      fr.setFeatureFilter(entry.getKey(), entry.getValue());
+    }
+  }
+
+  /**
+   * Restores colours for all feature types to their values when the dialog was
+   * opened
+   */
+  protected void restoreOriginalColours()
+  {
+    for (Entry<String, FeatureColourI> entry : originalColours.entrySet())
+    {
+      fr.setColour(entry.getKey(), entry.getValue());
+    }
+  }
+
+  /**
    * Action on text entry of a threshold value
    */
   protected void thresholdValue_actionPerformed()
@@ -1030,7 +1259,7 @@ public class FeatureTypeSettings extends JalviewDialog
        */
       adjusting = true;
       float f = Float.parseFloat(thresholdValue.getText());
-      f = Float.max(f,  this.min);
+      f = Float.max(f, this.min);
       f = Float.min(f, this.max);
       thresholdValue.setText(String.valueOf(f));
       slider.setValue((int) (f * scaleFactor));
@@ -1159,11 +1388,25 @@ public class FeatureTypeSettings extends JalviewDialog
   {
     filters = new ArrayList<>();
 
+    JPanel outerPanel = new JPanel();
+    outerPanel.setLayout(new BoxLayout(outerPanel, BoxLayout.Y_AXIS));
+    outerPanel.setBackground(Color.white);
+
+    /*
+     * option to apply colour to other selected types as well
+     */
+    if (!relatedSoTerms.isEmpty())
+    {
+      applyFiltersToSubtypes = false;
+      outerPanel.add(initSubtypesPanel(true));
+    }
+
     JPanel filtersPanel = new JPanel();
     filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
     filtersPanel.setBackground(Color.white);
     JvSwingUtils.createTitledBorder(filtersPanel,
             MessageManager.getString("label.filters"), true);
+    outerPanel.add(filtersPanel);
 
     JPanel andOrPanel = initialiseAndOrPanel();
     filtersPanel.add(andOrPanel);
@@ -1176,7 +1419,7 @@ public class FeatureTypeSettings extends JalviewDialog
     chooseFiltersPanel.setBackground(Color.white);
     filtersPanel.add(chooseFiltersPanel);
 
-    return filtersPanel;
+    return outerPanel;
   }
 
   /**
@@ -1186,8 +1429,9 @@ public class FeatureTypeSettings extends JalviewDialog
    */
   private JPanel initialiseAndOrPanel()
   {
-    JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    JPanel andOrPanel = new JPanel(new BorderLayout());
     andOrPanel.setBackground(Color.white);
+
     andFilters = new JRadioButton(MessageManager.getString("label.and"));
     orFilters = new JRadioButton(MessageManager.getString("label.or"));
     ActionListener actionListener = new ActionListener()
@@ -1208,10 +1452,31 @@ public class FeatureTypeSettings extends JalviewDialog
             new JLabel(MessageManager.getString("label.join_conditions")));
     andOrPanel.add(andFilters);
     andOrPanel.add(orFilters);
+
     return andOrPanel;
   }
 
   /**
+   * Builds a tooltip for the 'Apply also to...' combobox with a list of known
+   * feature types (excluding the current type) which are sub-types of the
+   * selected Sequence Ontology term
+   * 
+   * @param
+   * @return
+   */
+  protected String getSOTermsTooltip(List<String> list)
+  {
+    StringBuilder sb = new StringBuilder(20 * relatedSoTerms.size());
+    sb.append(MessageManager.getString("label.apply_also_to"));
+    for (String child : list)
+    {
+      sb.append("<br>").append(child);
+    }
+    String tooltip = JvSwingUtils.wrapTooltip(true, sb.toString());
+    return tooltip;
+  }
+
+  /**
    * Refreshes the display to show any filters currently configured for the
    * selected feature type (editable, with 'remove' option), plus one extra row
    * for adding a condition. This should be called after a filter has been
@@ -1753,6 +2018,15 @@ public class FeatureTypeSettings extends JalviewDialog
      * (note this might now be an empty filter with no conditions)
      */
     fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
+    if (applyFiltersToSubtypes)
+    {
+      for (String child : relatedSoTerms.get(parentSOTerm))
+      {
+        fr.setFeatureFilter(child, combined.isEmpty() ? null : combined);
+      }
+    }
+
+    refreshFeatureSettings();
     ap.paintAlignment(true, true);
 
     updateFiltersTab();