JAL-3010 choose feature types to apply changes to by parent SO term
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 22 Apr 2019 14:13:52 +0000 (15:13 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Mon, 22 Apr 2019 14:13:52 +0000 (15:13 +0100)
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/datamodel/ontology/OntologyI.java
src/jalview/ext/so/SequenceOntology.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/io/gff/SequenceOntologyLite.java
test/jalview/ext/so/SequenceOntologyTest.java
test/jalview/gui/FeatureTypeSettingsTest.java [new file with mode: 0644]

index d091426..1492fcf 100644 (file)
@@ -1339,8 +1339,8 @@ label.most_polymer_residues = Most Polymer Residues
 label.cached_structures = Cached Structures
 label.free_text_search = Free Text Search
 label.summary_view = Summary View
-label.summary_view_tip = Show only top level ontology terms
-label.apply_to_subtypes = Apply changes to all ''{0}'' features
+label.summary_view_tip = Group Sequence Ontology terms
+label.apply_to_subtypes = Apply to features and sub-types of
 label.apply_also_to = Apply also to:
 label.backupfiles_confirm_delete = Confirm delete
 label.backupfiles_confirm_delete_old_files = Delete the following older backup files? (see the Backups tab in Preferences for more options)
index 4935a3d..6e465a6 100644 (file)
@@ -1340,8 +1340,8 @@ label.most_polymer_residues = M
 label.cached_structures = Estructuras en Caché
 label.free_text_search = Búsqueda de texto libre
 label.summary_view = Vista Resumida
-label.summary_view_tip = Mostrar solo términos de ontología de nivel mayor
-label.apply_to_subtypes = Aplicar cambios también a todas características de tipo ''{0}''
+label.summary_view_tip = Agrupar términos de la Sequence Ontology
+label.apply_to_subtypes = Aplicar cambios también a características y subtipos de
 label.apply_also_to = Aplicar también a:
 label.backupfiles_confirm_delete = Confirmar borrar
 label.backupfiles_confirm_delete_old_files = ¿Borrar los siguientes archivos? (ver la pestaña 'Copias' de la ventana de Preferencias para más opciones)
index 5ac5a97..9ae2ad4 100644 (file)
@@ -38,6 +38,15 @@ public interface OntologyI
   List<String> getChildTerms(String parent, List<String> terms);
 
   /**
+   * Answers a (possibly empty) list of the immediate parent terms of the given
+   * term
+   * 
+   * @param term
+   * @return
+   */
+  List<String> getParents(String term);
+
+  /**
    * Returns a sorted list of all valid terms queried for (i.e. terms processed
    * which were valid in the SO), using the friendly description.
    * 
index 8a3805d..3b8bad4 100644 (file)
@@ -552,4 +552,20 @@ public class SequenceOntology extends OntologyBase
 
     return result;
   }
+
+  @Override
+  public List<String> getParents(String term)
+  {
+    List<String> parents = new ArrayList<>();
+    Term t = getTerm(term);
+    if (t != null)
+    {
+      for (Triple triple : ontology.getTriples(t, null, isA))
+      {
+        Term parent = triple.getObject();
+        parents.add(parent.getDescription());
+      }
+    }
+    return parents;
+  }
 }
index 0dd0f1f..e7efea9 100644 (file)
@@ -52,6 +52,7 @@ 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;
@@ -220,19 +221,24 @@ public class FeatureTypeSettings extends JalviewDialog
   private String rootSOTerm;
 
   /*
-   * feature types present in Feature Renderer which have the same Sequence
-   * Ontology root parent as the one this editor is acting on
+   * 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 List<String> peerSoTerms;
+  private final Map<String, List<String>> relatedSoTerms;
 
   /*
    * if true, filter or colour settings are also applied to 
-   * any sub-types of rootSOTerm in the Sequence Ontology
+   * any sub-types of parentTerm in the Sequence Ontology
    */
   private boolean applyFiltersToSubtypes;
 
   private boolean applyColourToSubtypes;
 
+  private String parentSOTerm;
+
   /**
    * Constructor
    * 
@@ -245,20 +251,24 @@ public class FeatureTypeSettings extends JalviewDialog
     this.featureType = theType;
     ap = fr.ap;
 
-    peerSoTerms = findSequenceOntologyPeers(this.featureType);
+    relatedSoTerms = findSequenceOntologyGroupings(this.featureType,
+            fr.getRenderOrder());
 
     /*
-     * save original colours and filters for this feature type
-     * and any sub-types, to restore on Cancel
+     * 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 (String child : peerSoTerms)
+    for (List<String> related : relatedSoTerms.values())
     {
-      originalFilters.put(child, fr.getFeatureFilter(child));
-      originalColours.put(child, fr.getFeatureColours().get(child));
+      for (String type : related)
+      {
+        originalFilters.put(type, fr.getFeatureFilter(type));
+        originalColours.put(type, fr.getFeatureColours().get(type));
+      }
     }
 
     adjusting = true;
@@ -288,42 +298,57 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
-   * Answers a (possibly empty) list of feature types known to the Feature
-   * Renderer which share a top level Sequence Ontology parent with the current
-   * feature type. The current type is not included.
+   * 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 List<String> findSequenceOntologyPeers(String featureType)
+  protected static Map<String, List<String>> findSequenceOntologyGroupings(
+          String featureType, List<String> featureTypes)
   {
-    List<String> peers = new ArrayList<>();
+    List<String> sortedTypes = new ArrayList<>(featureTypes);
+    Collections.sort(sortedTypes);
+
+    Map<String, List<String>> parents = new HashMap<>();
 
     /*
-     * first find the SO term (if any) that is the root
-     * parent of the current type
+     * 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();
-    List<String> roots = so.getRootParents(featureType);
-    if (roots == null || roots.size() > 1)
+    candidates.add(featureType);
+    while (!candidates.isEmpty())
     {
-      /*
-       * feature type is not an SO term, or has ambiguous root
-       */
-      return peers;
-    }
-    rootSOTerm = roots.get(0);
-
-    List<String> types = fr.getRenderOrder();
-    for (String type : types)
-    {
-      if (!type.equals(featureType) && so.isA(type, rootSOTerm))
+      String term = candidates.remove(0);
+      List<String> includedFeatures = new ArrayList<>();
+      for (String type : sortedTypes)
       {
-        peers.add(type);
+        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));
     }
-    Collections.sort(peers); // sort for ease of reading in tooltip
-    return peers;
+    
+    return parents;
   }
 
   /**
@@ -811,9 +836,9 @@ public class FeatureTypeSettings extends JalviewDialog
             MessageManager.getString("action.colour"), true);
 
     /*
-     * option to apply colour to peer types as well (if there are any)
+     * option to apply colour to other selected types as well
      */
-    if (!peerSoTerms.isEmpty())
+    if (!relatedSoTerms.isEmpty())
     {
       applyColourToSubtypes = false;
       colourByPanel.add(initSubtypesPanel(false));
@@ -918,8 +943,8 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
-   * Constructs and returns a panel with a checkbox for the option to apply any
-   * changes also to sub-types of the feature type
+   * 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
    */
@@ -927,10 +952,46 @@ public class FeatureTypeSettings extends JalviewDialog
   {
     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));
-    applyToSubtypesCB.setToolTipText(getSubtypesTooltip());
-    applyToSubtypesCB.addActionListener(new ActionListener()
+    toSubtypes.add(applyToSubtypesCB);
+
+    /*
+     * 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
@@ -938,6 +999,7 @@ public class FeatureTypeSettings extends JalviewDialog
       @Override
       public void actionPerformed(ActionEvent e)
       {
+        parentSOTerm = (String) parentType.getSelectedItem();
         if (forFilters)
         {
           applyFiltersToSubtypes = applyToSubtypesCB.isSelected();
@@ -951,8 +1013,9 @@ public class FeatureTypeSettings extends JalviewDialog
           colourChanged(true);
         }
       }
-    });
-    toSubtypes.add(applyToSubtypesCB);
+    };
+    applyToSubtypesCB.addActionListener(action);
+    parentType.addActionListener(action);
 
     return toSubtypes;
   }
@@ -1001,7 +1064,7 @@ public class FeatureTypeSettings extends JalviewDialog
     fr.setColour(featureType, acg);
     if (applyColourToSubtypes)
     {
-      for (String child : peerSoTerms)
+      for (String child : relatedSoTerms.get(parentSOTerm))
       {
         fr.setColour(child, acg);
       }
@@ -1155,6 +1218,10 @@ public class FeatureTypeSettings extends JalviewDialog
     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
@@ -1164,6 +1231,10 @@ public class FeatureTypeSettings extends JalviewDialog
     }
   }
 
+  /**
+   * Restores colours for all feature types to their values when the dialog was
+   * opened
+   */
   protected void restoreOriginalColours()
   {
     for (Entry<String, FeatureColourI> entry : originalColours.entrySet())
@@ -1317,9 +1388,9 @@ public class FeatureTypeSettings extends JalviewDialog
     outerPanel.setBackground(Color.white);
 
     /*
-     * option to apply colour to peer types as well (if there are any)
+     * option to apply colour to other selected types as well
      */
-    if (!peerSoTerms.isEmpty())
+    if (!relatedSoTerms.isEmpty())
     {
       applyFiltersToSubtypes = false;
       outerPanel.add(initSubtypesPanel(true));
@@ -1381,16 +1452,18 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
-   * Builds a tooltip for the 'Apply to subtypes' checkbox with a list of
-   * subtypes of this feature type
+   * 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 getSubtypesTooltip()
+  protected String getSOTermsTooltip(List<String> list)
   {
-    StringBuilder sb = new StringBuilder(20 * peerSoTerms.size());
+    StringBuilder sb = new StringBuilder(20 * relatedSoTerms.size());
     sb.append(MessageManager.getString("label.apply_also_to"));
-    for (String child : peerSoTerms)
+    for (String child : list)
     {
       sb.append("<br>").append(child);
     }
@@ -1934,7 +2007,7 @@ public class FeatureTypeSettings extends JalviewDialog
     fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
     if (applyFiltersToSubtypes)
     {
-      for (String child : peerSoTerms)
+      for (String child : relatedSoTerms.get(parentSOTerm))
       {
         fr.setFeatureFilter(child, combined.isEmpty() ? null : combined);
       }
index d4c2278..6abb5d6 100644 (file)
@@ -306,4 +306,11 @@ public class SequenceOntologyLite extends OntologyBase
 
     return top.isEmpty() ? null : top;
   }
+
+  @Override
+  public List<String> getParents(String term)
+  {
+    List<String> result = parents.get(term);
+    return result == null ? new ArrayList<>() : result;
+  }
 }
index 9cfbe88..7eb01c9 100644 (file)
@@ -183,6 +183,26 @@ public class SequenceOntologyTest
   }
 
   @Test(groups = "Functional")
+  public void testGetParents()
+  {
+    // invalid term
+    List<String> roots = so.getParents("xyz");
+    assertTrue(roots.isEmpty());
+
+    roots = so.getParents("stop_gained");
+    assertEquals(roots.size(), 2);
+    assertTrue(roots.contains("nonsynonymous_variant"));
+    assertTrue(roots.contains("feature_truncation"));
+
+    // top level term
+    roots = so.getParents("sequence_variant");
+    assertTrue(roots.isEmpty());
+
+    roots = so.getParents(null);
+    assertTrue(roots.isEmpty());
+  }
+
+  @Test(groups = "Functional")
   public void testGetRootParents()
   {
     List<String> roots = so.getRootParents("xyz");
diff --git a/test/jalview/gui/FeatureTypeSettingsTest.java b/test/jalview/gui/FeatureTypeSettingsTest.java
new file mode 100644 (file)
index 0000000..6a32b83
--- /dev/null
@@ -0,0 +1,147 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import jalview.ext.so.SequenceOntology;
+import jalview.io.gff.SequenceOntologyFactory;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class FeatureTypeSettingsTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    SequenceOntologyFactory.setInstance(new SequenceOntology());
+  }
+
+  @AfterClass(alwaysRun = true)
+  public void tearDown()
+  {
+    SequenceOntologyFactory.setInstance(null);
+  }
+
+  @Test(groups="Functional")
+  public void testfindSequenceOntologyGroupings()
+  {
+    /*
+     * typical gnomAD feature types, plus the top level 'sequence_variant' as in dbSNP
+     */
+    List<String> featureTypes = Arrays.asList("sequence_variant",
+            "inframe_insertion", "stop_lost", "stop_gained",
+            "5_prime_UTR_variant", "non_coding_transcript_exon_variant",
+            "synonymous_variant", "inframe_deletion", "frameshift_variant",
+            "upstream_gene_variant", "splice_region_variant",
+            "missense_variant");
+
+    /*
+     * for stop_gained:
+     * transcript_variant further adds 5_prime_UTR_variant, 
+     *     non_coding_transcript_exon_variant, synonymous_variant, splice_region_variant
+     * feature_variant further adds upstream_gene_variant
+     * sequence_variant further adds sequence_variant
+     */
+    Map<String, List<String>> map = FeatureTypeSettings
+            .findSequenceOntologyGroupings("stop_gained", featureTypes);
+    assertEquals(map.size(), 10);
+
+    /*
+     * feature_truncation adds inframe_deletion
+     */
+    List<String> terms = map.get("feature_truncation");
+    assertEquals(terms.size(), 1);
+    assertTrue(terms.contains("inframe_deletion"));
+
+    /*
+     * nonsynonymous_variant adds stop_lost, missense_variant
+     */
+    terms = map.get("nonsynonymous_variant");
+    assertEquals(terms.size(), 2);
+    assertEquals(terms.toString(), "[missense_variant, stop_lost]");
+
+    /*
+     * inframe_variant further adds inframe_deletion, inframe_insertion
+     */
+    terms = map.get("inframe_variant");
+    assertEquals(terms.size(), 4);
+    assertEquals(terms.toString(),
+            "[inframe_deletion, inframe_insertion, missense_variant, stop_lost]");
+
+    /*
+     * protein_altering_variant further adds frameshift_variant
+     */
+    terms = map.get("protein_altering_variant");
+    assertEquals(terms.size(), 5);
+    assertEquals(terms.toString(),
+            "[frameshift_variant, inframe_deletion, inframe_insertion, "
+                    + "missense_variant, stop_lost]");
+
+    /*
+     * coding_sequence_variant further adds synonymous_variant
+     */
+    terms = map.get("coding_sequence_variant");
+    assertEquals(terms.size(), 6);
+    assertEquals(terms.toString(),
+            "[frameshift_variant, inframe_deletion, inframe_insertion, "
+                    + "missense_variant, stop_lost, synonymous_variant]");
+
+    /*
+     * coding_transcript_variant further adds 5_prime_UTR_variant
+     */
+    terms = map.get("coding_transcript_variant");
+    assertEquals(terms.size(), 7);
+    assertEquals(terms.toString(),
+            "[5_prime_UTR_variant, frameshift_variant, inframe_deletion, "
+                    + "inframe_insertion, missense_variant, stop_lost, synonymous_variant]");
+
+    /*
+     * exon_variant further adds non_coding_transcript_exon_variant
+     */
+    terms = map.get("exon_variant");
+    assertEquals(terms.size(), 8);
+    assertEquals(terms.toString(),
+            "[5_prime_UTR_variant, frameshift_variant, inframe_deletion, "
+                    + "inframe_insertion, missense_variant, "
+                    + "non_coding_transcript_exon_variant, stop_lost, synonymous_variant]");
+
+    /*
+     * transcript_variant further adds splice_region_variant
+     */
+    terms = map.get("transcript_variant");
+    assertEquals(terms.size(), 9);
+    assertEquals(terms.toString(),
+            "[5_prime_UTR_variant, frameshift_variant, inframe_deletion, "
+                    + "inframe_insertion, missense_variant, "
+                    + "non_coding_transcript_exon_variant, splice_region_variant, "
+                    + "stop_lost, synonymous_variant]");
+
+    /*
+     * feature_variant further adds upstream_gene_variant
+     */
+    terms = map.get("feature_variant");
+    assertEquals(terms.size(), 10);
+    assertEquals(terms.toString(),
+            "[5_prime_UTR_variant, frameshift_variant, inframe_deletion, "
+                    + "inframe_insertion, missense_variant, "
+                    + "non_coding_transcript_exon_variant, splice_region_variant, "
+                    + "stop_lost, synonymous_variant, upstream_gene_variant]");
+
+    /*
+     * sequence_variant adds itself
+     */
+    terms = map.get("sequence_variant");
+    assertEquals(terms.size(), 11);
+    assertEquals(terms.toString(),
+            "[5_prime_UTR_variant, frameshift_variant, inframe_deletion, "
+                    + "inframe_insertion, missense_variant, "
+                    + "non_coding_transcript_exon_variant, sequence_variant, splice_region_variant, "
+                    + "stop_lost, synonymous_variant, upstream_gene_variant]");
+  }
+}