Merge branch 'develop' into features/JAL-3010ontologyFeatureSettings features/JAL-3010ontologyFeatureSettings
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 12 Jul 2019 13:38:34 +0000 (14:38 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Fri, 12 Jul 2019 13:38:34 +0000 (14:38 +0100)
Conflicts:
test/jalview/gui/AlignFrameTest.java

22 files changed:
resources/lang/Messages.properties
resources/lang/Messages_es.properties
resources/so-simple.obo.zip [new file with mode: 0644]
resources/so-xp-simple.obo.zip [deleted file]
src/jalview/api/AlignViewControllerI.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/ontology/OntologyBase.java [new file with mode: 0644]
src/jalview/datamodel/ontology/OntologyI.java [new file with mode: 0644]
src/jalview/ext/so/SequenceOntology.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/gui/SequenceFetcher.java
src/jalview/io/gff/SequenceOntologyI.java
src/jalview/io/gff/SequenceOntologyLite.java
src/org/biojava/nbio/ontology/Synonym.java [new file with mode: 0644]
test/jalview/controller/AlignViewControllerTest.java
test/jalview/datamodel/ontology/OntologyBaseTest.java [new file with mode: 0644]
test/jalview/ext/so/SequenceOntologyTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/io/gff/SequenceOntologyLiteTest.java

index d2d8e88..205da8e 100644 (file)
@@ -801,6 +801,7 @@ label.rest_client_submit = {0} using {1}
 label.fetch_retrieve_from =Retrieve from {0}</html>
 label.fetch_retrieve_from_all_sources = Retrieve from all {0} sources in {1}<br>First is :{2}<html> 
 label.feature_settings_click_drag = Drag up or down to change render order.<br/>Double click to select columns containing feature.
+label.feature_settings_select_columns = Double click to select columns containing feature
 label.transparency_tip = Adjust transparency to 'see through' feature colours.
 label.opt_and_params_further_details = see further details by right-clicking
 label.opt_and_params_show_brief_desc_image_link = <html>Click to show brief description<br><img src="{0}"/> Right click for further information.</html> 
@@ -1334,6 +1335,10 @@ label.most_bound_molecules = Most Bound Molecules
 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.group_by_so = Group features by Sequence Ontology
+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)
 label.backupfiles_confirm_save_file = Confirm save file
index e9e18ce..b62a8c5 100644 (file)
@@ -726,7 +726,8 @@ label.services_at = Servicios en {0}
 label.rest_client_submit = {0} utilizando {1}
 label.fetch_retrieve_from =Recuperar de {0}
 label.fetch_retrieve_from_all_sources = Recuperar de todas las fuentes {0} en {1}<br>La primera es :{2}
-label.feature_settings_click_drag = Haga clic o arrastre los tipos de las características hacia arriba o hacia abajo para cambiar el orden de visualización.<br/>Haga doble clic para seleccionar las columnas que contienen las características del alineamiento/selección actual.<br/>
+label.feature_settings_click_drag = Haga clic o arrastre los tipos de las características hacia arriba o hacia abajo para cambiar el orden de visualización.<br/>Haga doble clic para seleccionar las columnas que contienen las características del alineamiento/selección actual.
+label.feature_settings_select_columns =Haga doble clic para seleccionar las columnas que contienen las características del alineamiento/selección actual
 label.opt_and_params_further_details = ver los detalles adicionales haciendo clic en el botón derecho
 label.opt_and_params_show_brief_desc_image_link = Haga clic para ver una descripción breve<br><img src="{0}"/>Haga clic en el botón derecho para obtener información adicional.
 label.opt_and_params_show_brief_desc = Haga clic para ver una descripción breve<br>
@@ -1335,6 +1336,10 @@ label.most_bound_molecules = M
 label.most_polymer_residues = Más Residuos de Polímeros
 label.cached_structures = Estructuras en Caché
 label.free_text_search = Búsqueda de texto libre
+label.summary_view = Vista Resumida
+label.group_by_so = Agrupar por términos de la Sequence Ontology
+label.apply_to_subtypes = Aplicar 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)
 label.backupfiles_confirm_save_file = Confirmar guardar archivo
diff --git a/resources/so-simple.obo.zip b/resources/so-simple.obo.zip
new file mode 100644 (file)
index 0000000..5f2c6d5
Binary files /dev/null and b/resources/so-simple.obo.zip differ
diff --git a/resources/so-xp-simple.obo.zip b/resources/so-xp-simple.obo.zip
deleted file mode 100644 (file)
index d150da0..0000000
Binary files a/resources/so-xp-simple.obo.zip and /dev/null differ
index a7ec69e..f7575c3 100644 (file)
@@ -62,11 +62,11 @@ public interface AlignViewControllerI
    * @param toggle
    *          - rather than explicitly set, toggle selection state
    * @param featureType
-   *          - feature type string
+   *          - one or more feature types to match
    * @return true if operation affected state
    */
   boolean markColumnsContainingFeatures(boolean invert,
-          boolean extendCurrent, boolean toggle, String featureType);
+          boolean extendCurrent, boolean toggle, String... featureType);
 
   /**
    * sort the alignment or current selection by average score over the given set
index f45afa5..078a820 100644 (file)
@@ -161,7 +161,7 @@ public class AlignViewController implements AlignViewControllerI
 
   @Override
   public boolean markColumnsContainingFeatures(boolean invert,
-          boolean extendCurrent, boolean toggle, String featureType)
+          boolean extendCurrent, boolean toggle, String... featureType)
   {
     // JBPNote this routine could also mark rows, not just columns.
     // need a decent query structure to allow all types of feature searches
@@ -171,7 +171,7 @@ public class AlignViewController implements AlignViewControllerI
     SequenceCollectionI sqcol = searchSelection ? viewport
             .getSelectionGroup() : viewport.getAlignment();
 
-    int nseq = findColumnsWithFeature(featureType, sqcol, bs);
+    int nseq = findColumnsWithFeature(sqcol, bs, featureType);
 
     ColumnSelection cs = viewport.getColumnSelection();
     if (cs == null)
@@ -179,6 +179,16 @@ public class AlignViewController implements AlignViewControllerI
       cs = new ColumnSelection();
     }
 
+    String featureTypeString = featureType[0];
+    if (featureType.length > 1)
+    {
+      /*
+       * ellipsis if multiple feature types selected
+       * (from Summary View in Feature Settings)
+       */
+      featureTypeString += "...";
+    }
+
     if (bs.cardinality() > 0 || invert)
     {
       boolean changed = cs.markColumns(bs, sqcol.getStartRes(),
@@ -199,7 +209,7 @@ public class AlignViewController implements AlignViewControllerI
                     invert ? MessageManager
                             .getString("label.not_containing")
                             : MessageManager.getString("label.containing"),
-                    featureType, Integer.valueOf(nseq).toString() }));
+                    featureTypeString, Integer.valueOf(nseq).toString() }));
         return true;
       }
     }
@@ -207,8 +217,9 @@ public class AlignViewController implements AlignViewControllerI
     {
       String key = searchSelection ? "label.no_feature_found_selection"
               : "label.no_feature_of_type_found";
-      avcg.setStatus(MessageManager.formatMessage(key,
-              new String[] { featureType }));
+      avcg.setStatus(
+              MessageManager.formatMessage(key, new String[]
+              { featureTypeString }));
       if (!extendCurrent)
       {
         cs.clear();
@@ -220,17 +231,18 @@ public class AlignViewController implements AlignViewControllerI
 
   /**
    * Sets a bit in the BitSet for each column (base 0) in the sequence
-   * collection which includes a visible feature of the specified feature type.
-   * Returns the number of sequences which have the feature visible in the
-   * selected range.
+   * collection which includes a visible feature of the specified feature
+   * type(s). Returns the number of sequences which have the feature(s) visible
+   * in the selected range.
    * 
-   * @param featureType
    * @param sqcol
    * @param bs
+   * @param featureType
+   * 
    * @return
    */
-  int findColumnsWithFeature(String featureType,
-          SequenceCollectionI sqcol, BitSet bs)
+  int findColumnsWithFeature(SequenceCollectionI sqcol,
+          BitSet bs, String... featureType)
   {
     FeatureRenderer fr = alignPanel == null ? null : alignPanel
             .getFeatureRenderer();
diff --git a/src/jalview/datamodel/ontology/OntologyBase.java b/src/jalview/datamodel/ontology/OntologyBase.java
new file mode 100644 (file)
index 0000000..b19d9bc
--- /dev/null
@@ -0,0 +1,142 @@
+package jalview.datamodel.ontology;
+
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A base class for models of Sequence Ontology and others
+ * 
+ * @author gmcarstairs
+ *
+ */
+public abstract class OntologyBase implements OntologyI
+{
+  protected Map<String, List<String>> rootParents = new HashMap<>();
+
+  @Override
+  public Set<String> getParentTerms(Set<String> terms)
+  {
+    Set<String> parents = new HashSet<>(terms);
+
+    boolean childRemoved = true;
+    while (childRemoved)
+    {
+      childRemoved = removeChild(parents);
+    }
+    return parents;
+  }
+
+  /**
+   * Removes the first term in the given set found which is a child of another
+   * term in the set. Answers true if a child was found and removed, else false.
+   * 
+   * @param terms
+   * @return
+   */
+  boolean removeChild(Set<String> terms)
+  {
+    for (String t1 : terms)
+    {
+      for (String t2 : terms)
+      {
+        if (t1 != t2)
+        {
+          if (isA(t1, t2))
+          {
+            terms.remove(t1);
+            return true;
+          }
+          if (isA(t2, t1))
+          {
+            terms.remove(t2);
+            return true;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public List<String> getChildTerms(String parent, List<String> terms)
+  {
+    List<String> children = new ArrayList<>();
+    for (String term : terms)
+    {
+      if (!term.equals(parent) && isA(term, parent))
+      {
+        children.add(term);
+      }
+    }
+    return children;
+  }
+
+  /**
+   * Answers a (possibly empty) map of any Ontology terms (from the given term
+   * and its parents) which subsume one or more of the target terms. The map key
+   * is an ontology term, and the entry is the list of target terms that are
+   * sub-terms of the key.
+   * <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 givenTerm
+   *          the term to search from
+   * @param targetTerms
+   *          candidate terms to 'capture' in ontology groupings
+   * @return
+   */
+  @Override
+  public Map<String, List<String>> findSequenceOntologyGroupings(
+          String givenTerm, List<String> targetTerms)
+  {
+    List<String> sortedTypes = new ArrayList<>(targetTerms);
+    Collections.sort(sortedTypes);
+  
+    Map<String, List<String>> parents = new HashMap<>();
+    if (!isValidTerm(givenTerm))
+    {
+      return parents;
+    }
+
+    /*
+     * 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(givenTerm);
+    while (!candidates.isEmpty())
+    {
+      String term = candidates.remove(0);
+      List<String> includedFeatures = new ArrayList<>();
+      for (String type : sortedTypes)
+      {
+        if (!type.equals(givenTerm) && so.isA(type, term))
+        {
+          includedFeatures.add(type);
+        }
+      }
+      if (!includedFeatures.isEmpty()
+              && !parents.containsValue(includedFeatures))
+      {
+        parents.put(term, includedFeatures);
+      }
+      candidates.addAll(so.getParents(term));
+    }
+  
+    return parents;
+  }
+}
diff --git a/src/jalview/datamodel/ontology/OntologyI.java b/src/jalview/datamodel/ontology/OntologyI.java
new file mode 100644 (file)
index 0000000..b449982
--- /dev/null
@@ -0,0 +1,109 @@
+package jalview.datamodel.ontology;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public interface OntologyI
+{
+  /**
+   * Answers true if the term can be identified in the ontology (possibly by id,
+   * description or alias), else false
+   * 
+   * @param term
+   * @return
+   */
+  boolean isValidTerm(String term);
+
+  /**
+   * Answers true if <code>childTerm</code> is the same as, or a sub-type
+   * (specialisation of) <code>parentTerm</code>, else false
+   * 
+   * @param childTerm
+   * @param parentTerm
+   * @return
+   */
+  boolean isA(String childTerm, String parentTerm);
+
+  /**
+   * Answers those terms in the given set which are not child terms of some
+   * other term in the set. That is, returns a set of parent terms. The input
+   * set is not modified.
+   * 
+   * @param terms
+   * @return
+   */
+  Set<String> getParentTerms(Set<String> terms);
+
+  /**
+   * Answers a (possibly empty) list of those terms in the supplied list which
+   * are a child (directly or indirectly) of <code>parent</code>. The parent
+   * term itself is not included (even if in the input list)
+   * 
+   * @param parent
+   * @param terms
+   * @return
+   */
+  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.
+   * 
+   * This can be used to check that any hard-coded stand-in for the full SO
+   * includes all the terms needed for correct processing.
+   * 
+   * @return
+   */
+  List<String> termsFound();
+
+  /**
+   * Returns a sorted list of all invalid terms queried for (i.e. terms
+   * processed which were not found in the SO), using the friendly description.
+   * 
+   * This can be used to report any 'non-compliance' in data, and/or to report
+   * valid terms missing from any hard-coded stand-in for the full SO.
+   * 
+   * @return
+   */
+  List<String> termsNotFound();
+
+  /**
+   * Answers the top level parent terms (normally only one) for the given term,
+   * that is, those that have no parent themselves. Answers null if {@code term}
+   * is not a sequence ontology term. Answers a list just containing
+   * {@code term} if it is a valid term with no parent.
+   * 
+   * @param term
+   * @return
+   */
+  List<String> getRootParents(String term);
+
+  /**
+   * Answers a (possibly empty) map of any Ontology terms (from the given term
+   * and its parents) which subsume one or more of the target terms. The map key
+   * is an ontology term, and the entry is the list of target terms that are
+   * sub-terms of the key.
+   * <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 givenTerm
+   *          the term to search from
+   * @param targetTerms
+   *          candidate terms to 'capture' in ontology groupings
+   * @return
+   */
+  Map<String, List<String>> findSequenceOntologyGroupings(String givenTerm,
+          List<String> targetTerms);
+}
\ No newline at end of file
index 0d631e6..34e794e 100644 (file)
@@ -20,6 +20,8 @@
  */
 package jalview.ext.so;
 
+import jalview.bin.Cache;
+import jalview.datamodel.ontology.OntologyBase;
 import jalview.io.gff.SequenceOntologyI;
 
 import java.io.BufferedInputStream;
@@ -31,13 +33,16 @@ import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
 import org.biojava.nbio.ontology.Ontology;
+import org.biojava.nbio.ontology.Synonym;
 import org.biojava.nbio.ontology.Term;
 import org.biojava.nbio.ontology.Term.Impl;
 import org.biojava.nbio.ontology.Triple;
@@ -48,7 +53,8 @@ import org.biojava.nbio.ontology.utils.Annotation;
  * A wrapper class that parses the Sequence Ontology and exposes useful access
  * methods. This version uses the BioJava parser.
  */
-public class SequenceOntology implements SequenceOntologyI
+public class SequenceOntology extends OntologyBase
+        implements SequenceOntologyI
 {
   /*
    * the parsed Ontology data as modelled by BioJava
@@ -63,7 +69,7 @@ public class SequenceOntology implements SequenceOntologyI
   /*
    * lookup of terms by user readable name (NB not guaranteed unique)
    */
-  private Map<String, Term> termsByDescription;
+  private Map<String, Term> aliases;
 
   /*
    * Map where key is a Term and value is a (possibly empty) list of 
@@ -82,12 +88,12 @@ public class SequenceOntology implements SequenceOntologyI
    */
   public SequenceOntology()
   {
-    termsFound = new ArrayList<String>();
-    termsNotFound = new ArrayList<String>();
-    termsByDescription = new HashMap<String, Term>();
-    termIsA = new HashMap<Term, List<Term>>();
+    termsFound = new ArrayList<>();
+    termsNotFound = new ArrayList<>();
+    aliases = new HashMap<>();
+    termIsA = new HashMap<>();
 
-    loadOntologyZipFile("so-xp-simple.obo");
+    loadOntologyZipFile("so-simple.obo");
   }
 
   /**
@@ -158,55 +164,182 @@ public class SequenceOntology implements SequenceOntologyI
     OboParser parser = new OboParser();
     ontology = parser.parseOBO(oboFile, "SO", "the SO ontology");
     isA = ontology.getTerm("is_a");
-    storeTermNames();
+    storeTermAliases();
   }
 
   /**
-   * Stores a lookup table of terms by description. Note that description is not
-   * guaranteed unique. Where duplicate descriptions are found, try to discard
-   * the term that is flagged as obsolete. However we do store obsolete terms
-   * where there is no duplication of description.
+   * Stores a lookup table of terms by description or synonym. Note that
+   * description is not guaranteed unique. Where duplicate descriptions are
+   * found, try to discard the term that is flagged as obsolete. However we do
+   * store obsolete terms where there is no duplication of description.
    */
-  protected void storeTermNames()
+  protected void storeTermAliases()
   {
+    Set<String> ambiguous = new HashSet<>();
+
     for (Term term : ontology.getTerms())
     {
       if (term instanceof Impl)
       {
+        boolean newTermIsObsolete = isObsolete(term);
         String description = term.getDescription();
         if (description != null)
         {
-          Term replaced = termsByDescription.get(description);
+          description = canonicalise(description);
+          Term replaced = aliases.get(description);
           if (replaced != null)
           {
-            boolean newTermIsObsolete = isObsolete(term);
             boolean oldTermIsObsolete = isObsolete(replaced);
             if (newTermIsObsolete && !oldTermIsObsolete)
             {
-              System.err.println("Ignoring " + term.getName()
+              Cache.log.debug("SequenceOntology ignoring " + term.getName()
                       + " as obsolete and duplicated by "
                       + replaced.getName());
               term = replaced;
             }
             else if (!newTermIsObsolete && oldTermIsObsolete)
             {
-              System.err.println("Ignoring " + replaced.getName()
+              Cache.log.debug("SequenceOntology ignoring "
+                      + replaced.getName()
                       + " as obsolete and duplicated by " + term.getName());
             }
             else
             {
-              System.err.println("Warning: " + term.getName()
+              Cache.log.debug("SequenceOntology warning: " + term.getName()
                       + " has replaced " + replaced.getName()
                       + " for lookup of '" + description + "'");
             }
           }
-          termsByDescription.put(description, term);
+          aliases.put(description, term);
+
+          /*
+           * also store synonyms if not ambiguous
+           */
+          if (!newTermIsObsolete)
+          {
+            storeSynonymsForTerm(term, ambiguous);
+          }
+        }
+      }
+    }
+
+    /*
+     * remove ambiguous synonyms for safety;
+     * problem: what if a synonym matches a description?
+     * only one case found:
+     * nmd_transcript is synonym for SO:0001621:NMD_transcript_variant 
+     * and also the description for SO:0002114:NMD_transcript
+     */
+    for (String syn : ambiguous)
+    {
+      aliases.remove(syn);
+    }
+  }
+
+  /**
+   * Stores any synonyms as an alternative lookup for the term, canonicalised
+   * for case/hyphen/space insensitivity on lookup.
+   * <p>
+   * Some synonyms may be ambiguous (present for more than one term), and these
+   * are handled as follows:
+   * <ul>
+   * <li>if a synonym matches the <em>description</em> of another term, it is
+   * not saved, so that a term can always be found by description
+   * <ul>
+   * <li>Example: {@code nmd_transcript} is the description for
+   * {@code NMD_transcript} and also a synonym for
+   * {@code NMD_transcript_variant} - the synonym is ignored</li>
+   * </ul>
+   * </li>
+   * <li>if one term is a sub-term (directly or indirectly) of the other, the
+   * synonym is retained for the more general term
+   * <ul>
+   * <li>Example: {@code helix} is a synonym for
+   * {@code alpha_helix, right_handed_peptide_helix, peptide_helix} - it is kept
+   * for {@code peptide_helix} as this is a parent of the other terms</li>
+   * </ul>
+   * </li>
+   * <li>otherwise the synonym is added to the {@code ambiguous} list for
+   * removal
+   * <ul>
+   * <li>Example: {@code sequence variation} is a synonym for
+   * {@code sequence_alteration} and {@code alternate_sequence_site} but these
+   * have no {@code isA} relationship - the synonym is ignored as ambiguous</li>
+   * </ul>
+   * </ul>
+   * 
+   * @param term
+   * @param ambiguous
+   */
+  void storeSynonymsForTerm(Term term, Set<String> ambiguous)
+  {
+    for (Object syn : term.getSynonyms())
+    {
+      String name = ((Synonym) syn).getName();
+      String synonym = canonicalise(name);
+      if (aliases.containsKey(synonym))
+      {
+        final Term found = aliases.get(synonym);
+        if (found != term)
+        {
+          /*
+           * this alias is ambiguous - matches description,
+           * or an alias, of another term
+           */
+          String msg = String.format(
+                  "SequenceOntology ambiguous synonym %s for '%s:%s' and '%s:%s'",
+                  synonym, term.getName(), term.getDescription(),
+                  found.getName(), found.getDescription());
+          Cache.log.debug(msg);
+
+          /*
+           * preserve any entry whose canonical description happens to match
+           * a synonym (NMD_transcript is a valid description, and also
+           * a synonym for NMD_transcript_variant)
+           * also preserve a parent (more general) term
+           */
+          if (synonym.equals(canonicalise(found.getDescription()))
+                  || termIsA(term, found))
+          {
+            // leave it alone
+          }
+          /*
+           * replace a specialised term with a more general one
+           * with the same alias
+           */
+          // else if
+          // (synonym.equals(canonicalise(term.getDescription())))
+          else if (termIsA(found, term))
+          {
+            aliases.put(synonym, term);
+          }
+          else
+          {
+            ambiguous.add(synonym);
+          }
         }
       }
+      else
+      {
+        aliases.put(synonym, term);
+      }
     }
   }
 
   /**
+   * Converts a string to lower case and changes hyphens and spaces to
+   * underscores
+   * 
+   * @param s
+   * @return
+   */
+  static String canonicalise(String s)
+  {
+    return s == null ? null
+            : s.toLowerCase().replace('-', '_').replace(' ', '_');
+  }
+
+  /**
    * Answers true if the term has property "is_obsolete" with value true, else
    * false
    * 
@@ -336,7 +469,7 @@ public class SequenceOntology implements SequenceOntologyI
     {
       if (!termsNotFound.contains(term))
       {
-        System.err.println("SO term " + term + " invalid");
+        Cache.log.debug("SequenceOntology term " + term + " invalid");
         termsNotFound.add(term);
       }
     }
@@ -404,7 +537,7 @@ public class SequenceOntology implements SequenceOntologyI
    */
   protected synchronized void findParents(Term childTerm)
   {
-    List<Term> result = new ArrayList<Term>();
+    List<Term> result = new ArrayList<>();
     for (Triple triple : ontology.getTriples(childTerm, null, isA))
     {
       Term parent = triple.getObject();
@@ -420,14 +553,18 @@ public class SequenceOntology implements SequenceOntologyI
 
   /**
    * Returns the Term for a given name (e.g. "SO:0000735") or description (e.g.
-   * "sequence_location"), or null if not found.
+   * "sequence_location"), or alias, or null if not found
    * 
    * @param child
    * @return
    */
-  protected Term getTerm(String nameOrDescription)
+  protected Term getTerm(final String nameOrDescription)
   {
-    Term t = termsByDescription.get(nameOrDescription);
+    if (nameOrDescription == null)
+    {
+      return null;
+    }
+    Term t = aliases.get(canonicalise(nameOrDescription));
     if (t == null)
     {
       try
@@ -471,4 +608,104 @@ public class SequenceOntology implements SequenceOntologyI
       return termsNotFound;
     }
   }
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @throws IllegalStateException
+   *           if a loop is detected in the ontology
+   */
+  @Override
+  public List<String> getRootParents(final String term)
+  {
+    /*
+     * check in cache first
+     */
+    if (rootParents.containsKey(term))
+    {
+      return rootParents.get(term);
+    }
+    Term t = getTerm(term);
+    if (t == null)
+    {
+      return null;
+    }
+
+    /*
+     * todo: check for loops using 'seen', allowing for alternate paths e.g.
+     * stop_gained isA feature_truncation isA feature_variant
+     * " isA nonsynonymous_variant ... isA geneVariant isA feature_variant 
+     */
+    List<Term> seen = new ArrayList<>();
+    List<Term> top = new ArrayList<>();
+    List<Term> query = new ArrayList<>();
+    query.add(t);
+
+    while (!query.isEmpty())
+    {
+      List<Term> nextQuery = new ArrayList<>();
+      for (Term q : query)
+      {
+        Set<Triple> parents = ontology.getTriples(q, null, isA);
+        if (parents.isEmpty())
+        {
+          /*
+           * q has no parents so is a top level term
+           */
+          top.add(q);
+        }
+        else
+        {
+          /*
+           * search all parent terms
+           */
+          for (Triple triple : parents)
+          {
+            Term parent = triple.getObject();
+            nextQuery.add(parent);
+          }
+        }
+      }
+      query = nextQuery;
+    }
+
+    List<String> result = new ArrayList<>();
+    for (Term found : top)
+    {
+      String desc = found.getDescription();
+      if (!result.contains(desc))
+      {
+        result.add(desc);
+      }
+    }
+
+    /*
+     * save result in cache
+     */
+    rootParents.put(term, result);
+
+    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;
+  }
+
+  @Override
+  public boolean isValidTerm(String term)
+  {
+    return getTerm(term) != null;
+  }
 }
index fcb6572..1cdd3a6 100644 (file)
@@ -5564,22 +5564,23 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   }
 
   /**
-   * Hides columns containing (or not containing) a specified feature, provided
-   * that would not leave all columns hidden
+   * Hides columns containing (or not containing) the specified feature(s),
+   * provided that would not leave all columns hidden
    * 
-   * @param featureType
    * @param columnsContaining
+   * @param featureTypes
+   * 
    * @return
    */
-  public boolean hideFeatureColumns(String featureType,
-          boolean columnsContaining)
+  public boolean hideFeatureColumns(boolean columnsContaining,
+          String... featureTypes)
   {
     boolean notForHiding = avc.markColumnsContainingFeatures(
-            columnsContaining, false, false, featureType);
+            columnsContaining, false, false, featureTypes);
     if (notForHiding)
     {
       if (avc.markColumnsContainingFeatures(!columnsContaining, false,
-              false, featureType))
+              false, featureTypes))
       {
         getViewport().hideSelectedColumns();
         return true;
index 9ad6596..16f2b5d 100644 (file)
@@ -150,7 +150,7 @@ public class CrossRefAction implements Runnable
               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
       if (Cache.getDefault("HIDE_INTRONS", true))
       {
-        newFrame.hideFeatureColumns(SequenceOntologyI.EXON, false);
+        newFrame.hideFeatureColumns(false, SequenceOntologyI.EXON);
       }
       String newtitle = String.format("%s %s %s",
               dna ? MessageManager.getString("label.proteins")
index 9ca409b..58f9187 100644 (file)
@@ -28,9 +28,11 @@ import jalview.datamodel.features.FeatureMatcher;
 import jalview.datamodel.features.FeatureMatcherI;
 import jalview.datamodel.features.FeatureMatcherSet;
 import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.datamodel.ontology.OntologyI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.io.gff.SequenceOntologyFactory;
 import jalview.schemes.FeatureColour;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
@@ -64,6 +66,7 @@ import java.io.FileOutputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -92,6 +95,7 @@ import javax.swing.JScrollPane;
 import javax.swing.JSlider;
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
 import javax.swing.SwingConstants;
 import javax.swing.border.Border;
 import javax.swing.event.ChangeEvent;
@@ -101,6 +105,7 @@ import javax.swing.table.JTableHeader;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
 import javax.swing.table.TableColumn;
+import javax.swing.table.TableRowSorter;
 import javax.xml.bind.JAXBContext;
 import javax.xml.bind.JAXBElement;
 import javax.xml.bind.Marshaller;
@@ -110,6 +115,8 @@ import javax.xml.stream.XMLStreamReader;
 public class FeatureSettings extends JPanel
         implements FeatureSettingsControllerI
 {
+  private static final Font VERDANA_12 = new Font("Verdana", Font.PLAIN, 12);
+
   private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
           .getString("label.sequence_feature_colours");
 
@@ -174,6 +181,17 @@ public class FeatureSettings extends JPanel
    */
   Map<String, float[]> typeWidth = null;
 
+  /*
+   * if true, 'child' feature types are not displayed
+   */
+  JCheckBox summaryView;
+
+  /*
+   * those feature types that do not have a parent feature type present
+   * (as determined by an Ontology relationship)
+   */
+  List<String> topLevelTypes;
+
   /**
    * Constructor
    * 
@@ -191,6 +209,8 @@ public class FeatureSettings extends JPanel
 
     originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
 
+    topLevelTypes = new ArrayList<>();
+
     try
     {
       jbInit();
@@ -199,6 +219,69 @@ public class FeatureSettings extends JPanel
       ex.printStackTrace();
     }
 
+    initTable();
+
+    scrollPane.setViewportView(table);
+
+    if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
+    {
+      fr.findAllFeatures(true); // display everything!
+    }
+
+    discoverAllFeatureData();
+    final PropertyChangeListener change;
+    final FeatureSettings fs = this;
+    fr.addPropertyChangeListener(change = new PropertyChangeListener()
+    {
+      @Override
+      public void propertyChange(PropertyChangeEvent evt)
+      {
+        if (!fs.resettingTable && !fs.handlingUpdate)
+        {
+          fs.handlingUpdate = true;
+          fs.resetTable(null);
+          // new groups may be added with new sequence feature types only
+          fs.handlingUpdate = false;
+        }
+      }
+    });
+
+    frame = new JInternalFrame();
+    frame.setContentPane(this);
+    if (Platform.isAMac())
+    {
+      Desktop.addInternalFrame(frame,
+              MessageManager.getString("label.sequence_feature_settings"),
+              600, 480);
+    }
+    else
+    {
+      Desktop.addInternalFrame(frame,
+              MessageManager.getString("label.sequence_feature_settings"),
+              600, 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);
+              };
+            });
+    frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    inConstruction = false;
+  }
+
+  /**
+   * Constructs and configures the JTable which displays columns of data for
+   * each feature type
+   */
+  protected void initTable()
+  {
     table = new JTable()
     {
       @Override
@@ -207,11 +290,16 @@ public class FeatureSettings extends JPanel
         String tip = null;
         int column = table.columnAtPoint(e.getPoint());
         int row = table.rowAtPoint(e.getPoint());
-
         switch (column)
         {
         case TYPE_COLUMN:
-          tip = JvSwingUtils.wrapTooltip(true, MessageManager
+          /*
+           * drag to reorder not enabled in Summary View
+           */
+          tip = summaryView.isSelected()
+                  ? MessageManager.getString(
+                          "label.feature_settings_select_columns")
+                  : JvSwingUtils.wrapTooltip(true, MessageManager
                   .getString("label.feature_settings_click_drag"));
           break;
         case COLOUR_COLUMN:
@@ -230,7 +318,6 @@ public class FeatureSettings extends JPanel
         default:
           break;
         }
-        
         return tip;
       }
 
@@ -249,10 +336,11 @@ public class FeatureSettings extends JPanel
         return loc;
       }
     };
+
     JTableHeader tableHeader = table.getTableHeader();
-    tableHeader.setFont(new Font("Verdana", Font.PLAIN, 12));
+    tableHeader.setFont(VERDANA_12);
     tableHeader.setReorderingAllowed(false);
-    table.setFont(new Font("Verdana", Font.PLAIN, 12));
+    table.setFont(VERDANA_12);
 
     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
@@ -279,17 +367,16 @@ public class FeatureSettings extends JPanel
         String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
         if (evt.isPopupTrigger())
         {
-          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
-          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
-                  evt.getY());
+          popupMenu(selectedRow, type, evt.getX(), evt.getY());
         }
         else if (evt.getClickCount() == 2)
         {
           boolean invertSelection = evt.isAltDown();
           boolean toggleSelection = Platform.isControlDown(evt);
           boolean extendSelection = evt.isShiftDown();
+          String[] terms = getTermsInScope(type);
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
-                  invertSelection, extendSelection, toggleSelection, type);
+                  invertSelection, extendSelection, toggleSelection, terms);
         }
       }
 
@@ -301,9 +388,7 @@ public class FeatureSettings extends JPanel
         if (evt.isPopupTrigger())
         {
           String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
-          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
-          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
-                  evt.getY());
+          popupMenu(selectedRow, type, evt.getX(), evt.getY());
         }
       }
     });
@@ -314,92 +399,46 @@ public class FeatureSettings extends JPanel
       public void mouseDragged(MouseEvent evt)
       {
         int newRow = table.rowAtPoint(evt.getPoint());
-        if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
-        {
-          /*
-           * reposition 'selectedRow' to 'newRow' (the dragged to location)
-           * this could be more than one row away for a very fast drag action
-           * so just swap it with adjacent rows until we get it there
-           */
-          Object[][] data = ((FeatureTableModel) table.getModel())
-                  .getData();
-          int direction = newRow < selectedRow ? -1 : 1;
-          for (int i = selectedRow; i != newRow; i += direction)
-          {
-            Object[] temp = data[i];
-            data[i] = data[i + direction];
-            data[i + direction] = temp;
-          }
-          updateFeatureRenderer(data);
-          table.repaint();
-          selectedRow = newRow;
-        }
+        dragRow(newRow);
       }
     });
-    // table.setToolTipText(JvSwingUtils.wrapTooltip(true,
-    // MessageManager.getString("label.feature_settings_click_drag")));
-    scrollPane.setViewportView(table);
+  }
 
-    if (af.getViewport().isShowSequenceFeatures() || !fr.hasRenderOrder())
+  /**
+   * Answers an array consisting of the given type, and also (if 'Summary View'
+   * is selected), any feature types which are child terms of it in the Sequence
+   * Ontology
+   * 
+   * @param type
+   * @return
+   */
+  protected String[] getTermsInScope(String type)
+  {
+    if (!summaryView.isSelected())
     {
-      fr.findAllFeatures(true); // display everything!
+      return new String[] { type };
     }
 
-    discoverAllFeatureData();
-    final PropertyChangeListener change;
-    final FeatureSettings fs = this;
-    fr.addPropertyChangeListener(change = new PropertyChangeListener()
-    {
-      @Override
-      public void propertyChange(PropertyChangeEvent evt)
-      {
-        if (!fs.resettingTable && !fs.handlingUpdate)
-        {
-          fs.handlingUpdate = true;
-          fs.resetTable(null);
-          // new groups may be added with new sequence feature types only
-          fs.handlingUpdate = false;
-        }
-      }
+    List<String> terms = new ArrayList<>();
+    terms.add(type);
 
-    });
+    OntologyI so = SequenceOntologyFactory.getInstance();
 
-    frame = new JInternalFrame();
-    frame.setContentPane(this);
-    if (Platform.isAMac())
-    {
-      Desktop.addInternalFrame(frame,
-              MessageManager.getString("label.sequence_feature_settings"),
-              600, 480);
-    }
-    else
+    Object[][] data = ((FeatureTableModel) table.getModel()).getData();
+    for (Object[] row : data)
     {
-      Desktop.addInternalFrame(frame,
-              MessageManager.getString("label.sequence_feature_settings"),
-              600, 450);
+      String type2 = (String) row[TYPE_COLUMN];
+      if (!type2.equals(type) && so.isA(type2, type))
+      {
+        terms.add(type2);
+      }
     }
-    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);
-              };
-            });
-    frame.setLayer(JLayeredPane.PALETTE_LAYER);
-    inConstruction = false;
+    return terms.toArray(new String[terms.size()]);
   }
 
-  protected void popupSort(final int rowSelected, final String type,
-          final Object typeCol, final Map<String, float[][]> minmax, int x,
+  protected void popupMenu(final int rowSelected, final String type, int x,
           int y)
   {
-    final FeatureColourI featureColour = (FeatureColourI) typeCol;
-
     JPopupMenu men = new JPopupMenu(MessageManager
             .formatMessage("label.settings_for_param", new String[]
             { type }));
@@ -409,29 +448,23 @@ public class FeatureSettings extends JPanel
     final FeatureSettings me = this;
     scr.addActionListener(new ActionListener()
     {
-
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.af.avc
-                .sortAlignmentByFeatureScore(Arrays.asList(new String[]
-                { type }));
+        String[] types = getTermsInScope(type);
+        me.af.avc.sortAlignmentByFeatureScore(Arrays.asList(types));
       }
-
     });
     JMenuItem dens = new JMenuItem(
             MessageManager.getString("label.sort_by_density"));
     dens.addActionListener(new ActionListener()
     {
-
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        me.af.avc
-                .sortAlignmentByFeatureDensity(Arrays.asList(new String[]
-                { type }));
+        String[] types = getTermsInScope(type);
+        me.af.avc.sortAlignmentByFeatureDensity(Arrays.asList(types));
       }
-
     });
     men.add(dens);
 
@@ -442,8 +475,9 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
+        String[] types = getTermsInScope(type);
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(false, false,
-                false, type);
+                false, types);
       }
     });
     JMenuItem clearCols = new JMenuItem(MessageManager
@@ -453,8 +487,9 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
+        String[] types = getTermsInScope(type);
         fr.ap.alignFrame.avc.markColumnsContainingFeatures(true, false,
-                false, type);
+                false, types);
       }
     });
     JMenuItem hideCols = new JMenuItem(
@@ -464,7 +499,8 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
-        fr.ap.alignFrame.hideFeatureColumns(type, true);
+        String[] types = getTermsInScope(type);
+        fr.ap.alignFrame.hideFeatureColumns(true, types);
       }
     });
     JMenuItem hideOtherCols = new JMenuItem(
@@ -474,7 +510,8 @@ public class FeatureSettings extends JPanel
       @Override
       public void actionPerformed(ActionEvent arg0)
       {
-        fr.ap.alignFrame.hideFeatureColumns(type, false);
+        String[] types = getTermsInScope(type);
+        fr.ap.alignFrame.hideFeatureColumns(false, types);
       }
     });
     men.add(selCols);
@@ -589,6 +626,7 @@ public class FeatureSettings extends JPanel
        */
       Set<String> types = seq.getFeatures().getFeatureTypesForGroups(true,
               visibleGroups.toArray(new String[visibleGroups.size()]));
+
       for (String type : types)
       {
         displayableTypes.add(type);
@@ -607,6 +645,13 @@ public class FeatureSettings extends JPanel
       }
     }
 
+    /*
+     * enable 'Summary View' if some types are sub-types of others
+     */
+    Set<String> parents = SequenceOntologyFactory.getInstance()
+            .getParentTerms(displayableTypes);
+    summaryView.setEnabled(parents.size() < displayableTypes.size());
+
     Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
     int dataIndex = 0;
 
@@ -629,7 +674,6 @@ public class FeatureSettings extends JPanel
         {
           continue;
         }
-
         data[dataIndex][TYPE_COLUMN] = type;
         data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
         FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
@@ -681,7 +725,43 @@ public class FeatureSettings extends JPanel
       updateOriginalData(data);
     }
 
-    table.setModel(new FeatureTableModel(data));
+    /*
+     * recreate the table model
+     */
+    FeatureTableModel dataModel = new FeatureTableModel(data);
+    table.setModel(dataModel);
+
+    /*
+     * we want to be able to filter out rows for sub-types, but not to sort 
+     * rows, so have to add a RowFilter to a disabled TableRowSorter (!)
+     */
+    final TableRowSorter<FeatureTableModel> sorter = new TableRowSorter<>(
+            dataModel);
+    for (int i = 0; i < table.getColumnCount(); i++)
+    {
+      sorter.setSortable(i, false);
+    }
+
+    /*
+     * filter rows to only top-level Ontology types if requested
+     */
+    sorter.setRowFilter(new RowFilter<FeatureTableModel, Integer>()
+    {
+      @Override
+      public boolean include(
+              Entry<? extends FeatureTableModel, ? extends Integer> entry)
+      {
+        if (!summaryView.isSelected())
+        {
+          return true;
+        }
+        int row = entry.getIdentifier(); // this is model, not view, row number
+        String featureType = (String) entry.getModel().getData()[row][TYPE_COLUMN];
+        return parents.contains(featureType);
+      }
+    });
+    table.setRowSorter(sorter);
+
     table.getColumnModel().getColumn(0).setPreferredWidth(200);
 
     groupPanel.setLayout(
@@ -1260,12 +1340,26 @@ public class FeatureSettings extends JPanel
       }
     });
 
+    summaryView = new JCheckBox(
+            MessageManager.getString("label.summary_view"));
+    summaryView
+            .setToolTipText(
+                    MessageManager.getString("label.group_by_so"));
+    summaryView.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        resetTable(null);
+      }
+    });
+
     transparency.setMaximum(70);
     transparency.setToolTipText(
             MessageManager.getString("label.transparency_tip"));
 
-    JPanel transPanel = new JPanel(new GridLayout(1, 2));
-    bigPanel.add(transPanel, BorderLayout.SOUTH);
+    JPanel lowerPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(lowerPanel, BorderLayout.SOUTH);
 
     JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
@@ -1273,8 +1367,12 @@ public class FeatureSettings extends JPanel
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
+    JPanel transPanel = new JPanel(new GridLayout(3, 1));
+    transPanel.add(summaryView);
+    transPanel.add(new JLabel(" Colour transparency" + ":"));
     transPanel.add(transparency);
-    transPanel.add(transbuttons);
+    lowerPanel.add(transPanel);
+    lowerPanel.add(transbuttons);
 
     JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
@@ -1288,6 +1386,59 @@ public class FeatureSettings extends JPanel
   }
 
   /**
+   * Reorders features by 'dragging' selectedRow to 'newRow'
+   * 
+   * @param newRow
+   */
+  protected void dragRow(int newRow)
+  {
+    if (summaryView.isSelected())
+    {
+      // no drag while in summary view
+      return;
+    }
+
+    if (newRow != selectedRow && selectedRow != -1 && newRow != -1)
+    {
+      /*
+       * reposition 'selectedRow' to 'newRow' (the dragged to location)
+       * this could be more than one row away for a very fast drag action
+       * so just swap it with adjacent rows until we get it there
+       */
+      Object[][] data = ((FeatureTableModel) table.getModel())
+              .getData();
+      int direction = newRow < selectedRow ? -1 : 1;
+      for (int i = selectedRow; i != newRow; i += direction)
+      {
+        Object[] temp = data[i];
+        data[i] = data[i + direction];
+        data[i + direction] = temp;
+      }
+      updateFeatureRenderer(data);
+      table.repaint();
+      selectedRow = newRow;
+    }
+  }
+
+  protected void refreshTable()
+  {
+    Object[][] data = ((FeatureTableModel) table.getModel()).getData();
+    for (Object[] row : data)
+    {
+      String type = (String) row[TYPE_COLUMN];
+      FeatureColourI colour = fr.getFeatureColours().get(type);
+      FeatureMatcherSetI filter = fr.getFeatureFilter(type);
+      if (filter == null)
+      {
+        filter = new FeatureMatcherSet();
+      }
+      row[COLOUR_COLUMN] = colour;
+      row[FILTER_COLUMN] = filter;
+    }
+    repaint();
+  }
+
+  /*
    * Answers a suitable tooltip to show on the colour cell of the table
    * 
    * @param fcol
@@ -1429,29 +1580,75 @@ public class FeatureSettings extends JPanel
     }
 
     /**
-     * Answers the class of the object in column c of the first row of the table
+     * Answers the class of column c of the table
      */
     @Override
     public Class<?> getColumnClass(int c)
     {
-      Object v = getValueAt(0, c);
-      return v == null ? null : v.getClass();
+      switch (c)
+      {
+      case TYPE_COLUMN:
+        return String.class;
+      case COLOUR_COLUMN:
+        return FeatureColour.class;
+      case FILTER_COLUMN:
+        return FeatureMatcherSet.class;
+      default:
+        return Boolean.class;
+      }
     }
 
+    /**
+     * Answers true for all columns except Feature Type
+     */
     @Override
     public boolean isCellEditable(int row, int col)
     {
-      return col == 0 ? false : true;
+      return col != TYPE_COLUMN;
     }
 
+    /**
+     * Sets the value in the model for a given row and column. If Visibility
+     * (Show/Hide) is being set, and the table is in Summary View, then it is
+     * set also on any sub-types of the row's feature type.
+     */
     @Override
     public void setValueAt(Object value, int row, int col)
     {
       data[row][col] = value;
       fireTableCellUpdated(row, col);
+      if (summaryView.isSelected() && col == SHOW_COLUMN)
+      {
+        setSubtypesVisibility(row, (Boolean) value);
+      }
       updateFeatureRenderer(data);
     }
 
+    /**
+     * Sets the visibility of any feature types which are sub-types of the type
+     * in the given row of the table
+     * 
+     * @param row
+     * @param value
+     */
+    protected void setSubtypesVisibility(int row, Boolean value)
+    {
+      String type = (String) data[row][TYPE_COLUMN];
+      OntologyI so = SequenceOntologyFactory.getInstance();
+
+      for (int r = 0; r < data.length; r++)
+      {
+        if (r != row)
+        {
+          String type2 = (String) data[r][TYPE_COLUMN];
+          if (so.isA(type2, type))
+          {
+            data[r][SHOW_COLUMN] = value;
+            fireTableCellUpdated(r, SHOW_COLUMN);
+          }
+        }
+      }
+    }
   }
 
   class ColorRenderer extends JLabel implements TableCellRenderer
@@ -1590,7 +1787,7 @@ public class FeatureSettings extends JPanel
 
     String type;
 
-    JButton button;
+    JButton colourButton;
 
     JColorChooser colorChooser;
 
@@ -1607,13 +1804,13 @@ public class FeatureSettings extends JPanel
       // which is a button.
       // This button brings up the color chooser dialog,
       // which is the editor from the user's point of view.
-      button = new JButton();
-      button.setActionCommand(EDIT);
-      button.addActionListener(this);
-      button.setBorderPainted(false);
+      colourButton = new JButton();
+      colourButton.setActionCommand(EDIT);
+      colourButton.addActionListener(this);
+      colourButton.setBorderPainted(false);
       // Set up the dialog that the button brings up.
       colorChooser = new JColorChooser();
-      dialog = JColorChooser.createDialog(button,
+      dialog = JColorChooser.createDialog(colourButton,
               MessageManager.getString("label.select_colour"), true, // modal
               colorChooser, this, // OK button handler
               null); // no CANCEL button handler
@@ -1633,7 +1830,7 @@ public class FeatureSettings extends JPanel
         if (currentColor.isSimpleColour())
         {
           // bring up simple color chooser
-          button.setBackground(currentColor.getColour());
+          colourButton.setBackground(currentColor.getColour());
           colorChooser.setColor(currentColor.getColour());
           dialog.setVisible(true);
         }
@@ -1641,13 +1838,8 @@ public class FeatureSettings extends JPanel
         {
           // bring up graduated chooser.
           chooser = new FeatureTypeSettings(me.fr, type);
-          /**
-           * @j2sNative
-           */
-          {
-            chooser.setRequestFocusEnabled(true);
-            chooser.requestFocus();
-          }
+          chooser.setRequestFocusEnabled(true);
+          chooser.requestFocus();
           chooser.addActionListener(this);
           // Make the renderer reappear.
           fireEditingStopped();
@@ -1670,16 +1862,7 @@ public class FeatureSettings extends JPanel
            * (or filters!) are already set in FeatureRenderer, so just
            * update table data without triggering updateFeatureRenderer
            */
-          currentColor = fr.getFeatureColours().get(type);
-          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
-          if (currentFilter == null)
-          {
-            currentFilter = new FeatureMatcherSet();
-          }
-          Object[] data = ((FeatureTableModel) table.getModel())
-                  .getData()[rowSelected];
-          data[COLOUR_COLUMN] = currentColor;
-          data[FILTER_COLUMN] = currentFilter;
+          refreshTable();
         }
         fireEditingStopped();
         me.table.validate();
@@ -1701,24 +1884,24 @@ public class FeatureSettings extends JPanel
       currentColor = (FeatureColourI) value;
       this.rowSelected = row;
       type = me.table.getValueAt(row, TYPE_COLUMN).toString();
-      button.setOpaque(true);
-      button.setBackground(me.getBackground());
+      colourButton.setOpaque(true);
+      colourButton.setBackground(me.getBackground());
       if (!currentColor.isSimpleColour())
       {
         JLabel btn = new JLabel();
-        btn.setSize(button.getSize());
+        btn.setSize(colourButton.getSize());
         FeatureSettings.renderGraduatedColor(btn, currentColor);
-        button.setBackground(btn.getBackground());
-        button.setIcon(btn.getIcon());
-        button.setText(btn.getText());
+        colourButton.setBackground(btn.getBackground());
+        colourButton.setIcon(btn.getIcon());
+        colourButton.setText(btn.getText());
       }
       else
       {
-        button.setText("");
-        button.setIcon(null);
-        button.setBackground(currentColor.getColour());
+        colourButton.setText("");
+        colourButton.setIcon(null);
+        colourButton.setBackground(currentColor.getColour());
       }
-      return button;
+      return colourButton;
     }
   }
 
@@ -1739,7 +1922,7 @@ public class FeatureSettings extends JPanel
 
     String type;
 
-    JButton button;
+    JButton filterButton;
 
     protected static final String EDIT = "edit";
 
@@ -1748,10 +1931,10 @@ public class FeatureSettings extends JPanel
     public FilterEditor(FeatureSettings me)
     {
       this.me = me;
-      button = new JButton();
-      button.setActionCommand(EDIT);
-      button.addActionListener(this);
-      button.setBorderPainted(false);
+      filterButton = new JButton();
+      filterButton.setActionCommand(EDIT);
+      filterButton.addActionListener(this);
+      filterButton.setBorderPainted(false);
     }
 
     /**
@@ -1760,7 +1943,7 @@ public class FeatureSettings extends JPanel
     @Override
     public void actionPerformed(ActionEvent e)
     {
-      if (button == e.getSource())
+      if (filterButton == e.getSource())
       {
         FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
         chooser.addActionListener(this);
@@ -1777,22 +1960,12 @@ public class FeatureSettings extends JPanel
       }
       else if (e.getSource() instanceof Component)
       {
-
         /*
          * after OK in variable colour dialog, any changes to filter
          * (or colours!) are already set in FeatureRenderer, so just
          * update table data without triggering updateFeatureRenderer
          */
-        FeatureColourI currentColor = fr.getFeatureColours().get(type);
-        currentFilter = me.fr.getFeatureFilter(type);
-        if (currentFilter == null)
-        {
-          currentFilter = new FeatureMatcherSet();
-        }
-        Object[] data = ((FeatureTableModel) table.getModel())
-                .getData()[rowSelected];
-        data[COLOUR_COLUMN] = currentColor;
-        data[FILTER_COLUMN] = currentFilter;
+        refreshTable();
         fireEditingStopped();
         me.table.validate();
       }
@@ -1811,17 +1984,20 @@ public class FeatureSettings extends JPanel
       currentFilter = (FeatureMatcherSetI) value;
       this.rowSelected = row;
       type = me.table.getValueAt(row, TYPE_COLUMN).toString();
-      button.setOpaque(true);
-      button.setBackground(me.getBackground());
-      button.setText(currentFilter.toString());
-      button.setIcon(null);
-      return button;
+      filterButton.setOpaque(true);
+      filterButton.setBackground(me.getBackground());
+      filterButton.setText(currentFilter.toString());
+      filterButton.setToolTipText(currentFilter.toString());
+      filterButton.setIcon(null);
+      return filterButton;
     }
   }
 }
 
 class FeatureIcon implements Icon
 {
+  private static final Font VERDANA_9 = new Font("Verdana", Font.PLAIN, 9);
+
   FeatureColourI gcol;
 
   Color backg;
@@ -1876,7 +2052,7 @@ class FeatureIcon implements Icon
       // need an icon here.
       g.setColor(gcol.getMaxColour());
 
-      g.setFont(new Font("Verdana", Font.PLAIN, 9));
+      g.setFont(VERDANA_9);
 
       // g.setFont(g.getFont().deriveFont(
       // AffineTransform.getScaleInstance(
index 7456e18..167a63e 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<Object> soTerms = new ArrayList<>();
+    for (String term : relatedSoTerms.keySet())
+    {
+      soTerms.add(term);
+    }
+    // sort from most restrictive to most inclusive
+    Collections.sort(soTerms, new Comparator<>()
+    {
+      @Override
+      public int compare(Object o1, Object o2)
+      {
+        return Integer.compare(relatedSoTerms.get(o1).size(),
+                relatedSoTerms.get(o2).size());
+      }
+    });
+    List<String> tooltips = new ArrayList<>();
+    for (Object term : soTerms)
+    {
+      tooltips.add(getSOTermsTooltip(relatedSoTerms.get(term)));
+    }
+    JComboBox<Object> 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();
index 1c4e6a6..8ea0ba2 100755 (executable)
@@ -1029,7 +1029,7 @@ public class SequenceFetcher extends JPanel implements Runnable
         }
         if (Cache.getDefault("HIDE_INTRONS", true))
         {
-          af.hideFeatureColumns(SequenceOntologyI.EXON, false);
+          af.hideFeatureColumns(false, SequenceOntologyI.EXON);
         }
         if (newAlframes != null)
         {
index 307e1d1..e9b9923 100644 (file)
@@ -20,9 +20,9 @@
  */
 package jalview.io.gff;
 
-import java.util.List;
+import jalview.datamodel.ontology.OntologyI;
 
-public interface SequenceOntologyI
+public interface SequenceOntologyI extends OntologyI
 {
   /*
    * selected commonly used values for quick reference
@@ -62,28 +62,4 @@ public interface SequenceOntologyI
 
   // SO:0000704
   public static final String GENE = "gene";
-
-  public boolean isA(String childTerm, String parentTerm);
-
-  /**
-   * Returns a sorted list of all valid terms queried for (i.e. terms processed
-   * which were valid in the SO), using the friendly description.
-   * 
-   * This can be used to check that any hard-coded stand-in for the full SO
-   * includes all the terms needed for correct processing.
-   * 
-   * @return
-   */
-  public List<String> termsFound();
-
-  /**
-   * Returns a sorted list of all invalid terms queried for (i.e. terms
-   * processed which were not found in the SO), using the friendly description.
-   * 
-   * This can be used to report any 'non-compliance' in data, and/or to report
-   * valid terms missing from any hard-coded stand-in for the full SO.
-   * 
-   * @return
-   */
-  public List<String> termsNotFound();
 }
index 7d354e0..2cbec36 100644 (file)
@@ -20,6 +20,8 @@
  */
 package jalview.io.gff;
 
+import jalview.datamodel.ontology.OntologyBase;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -38,7 +40,8 @@ import java.util.Map;
  * @author gmcarstairs
  *
  */
-public class SequenceOntologyLite implements SequenceOntologyI
+public class SequenceOntologyLite extends OntologyBase
+        implements SequenceOntologyI
 {
   /*
    * initial selection of types of interest when processing Ensembl features
@@ -81,8 +84,11 @@ public class SequenceOntologyLite implements SequenceOntologyI
     { "sequence_variant", "sequence_variant" },
     { "structural_variant", "sequence_variant" },
     { "feature_variant", "sequence_variant" },
+    { "upstream_gene_variant", "sequence_variant" },
     { "gene_variant", "sequence_variant" },
     { "transcript_variant", "sequence_variant" },
+    { "non_coding_transcript_variant", "sequence_variant" },
+    { "non_coding_transcript_exon_variant", "sequence_variant" },
     // NB Ensembl uses NMD_transcript_variant as if a 'transcript'
     // but we model it here correctly as per the SO
     { "NMD_transcript_variant", "sequence_variant" },
@@ -248,4 +254,70 @@ public class SequenceOntologyLite implements SequenceOntologyI
       return termsNotFound;
     }
   }
+
+  @Override
+  public List<String> getRootParents(final String term)
+  {
+    /*
+     * check in cache first
+     */
+    if (rootParents.containsKey(term))
+    {
+      return rootParents.get(term);
+    }
+
+    List<String> top = new ArrayList<>();
+    List<String> query = new ArrayList<>();
+    query.add(term);
+
+    while (!query.isEmpty())
+    {
+      List<String> nextQuery = new ArrayList<>();
+      for (String q : query)
+      {
+        List<String> theParents = parents.get(q);
+        if (theParents != null)
+        {
+          if (theParents.size() == 1 && theParents.get(0).equals(q))
+          {
+            /*
+             * top-level term
+             */
+            if (!top.contains(q))
+            {
+              top.add(q);
+            }
+          }
+          else
+          {
+            for (String p : theParents)
+            {
+              if (!p.equals(q))
+              {
+                nextQuery.add(p);
+              }
+            }
+          }
+        }
+      }
+      query = nextQuery;
+    }
+
+    rootParents.put(term, top);
+
+    return top.isEmpty() ? null : top;
+  }
+
+  @Override
+  public List<String> getParents(String term)
+  {
+    List<String> result = parents.get(term);
+    return result == null ? new ArrayList<>() : result;
+  }
+
+  @Override
+  public boolean isValidTerm(String term)
+  {
+    return parents.containsKey(term);
+  }
 }
diff --git a/src/org/biojava/nbio/ontology/Synonym.java b/src/org/biojava/nbio/ontology/Synonym.java
new file mode 100644 (file)
index 0000000..3902026
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ *                  BioJava development code
+ *
+ * This code may be freely distributed and modified under the
+ * terms of the GNU Lesser General Public Licence.  This should
+ * be distributed with the code.  If you do not have a copy,
+ * see:
+ *
+ *      http://www.gnu.org/copyleft/lesser.html
+ *
+ * Copyright for this code is held jointly by the individual
+ * authors.  These should be listed in @author doc comments.
+ *
+ * For more information on the BioJava project and its aims,
+ * or to join the biojava-l mailing list, visit the home page
+ * at:
+ *
+ *      http://www.biojava.org/
+ *
+ * Created on Jan 24, 2008
+ *
+ */
+
+package org.biojava.nbio.ontology;
+
+import java.util.Comparator;
+
+
+public class Synonym implements Comparable<Synonym>{
+
+
+       public final static int UNKNOWN_SCOPE = -1;
+       public final static int RELATED_SYNONYM = 0;
+       public final static int EXACT_SYNONYM = 1;
+       public final static int NARROW_SYNONYM = 2;
+       public final static int BROAD_SYNONYM = 3;
+
+       int scope;
+       String category;
+       String name;
+
+       @Override
+       public String toString(){
+               String txt = "Synonym: name:"+name+ " category:" + category + " scope: " +scope;
+               return txt;
+       }
+
+  @Override
+  public int hashCode()
+  {
+    return toString().hashCode();
+  }
+
+  @Override
+        public boolean equals(Object obj)
+        {
+    return obj instanceof Synonym
+            && ((Synonym) obj).toString().equals(this.toString());
+        }
+
+  public final static Comparator<Synonym> COMPARATOR = new Comparator<Synonym>()
+  {
+               @Override
+               public int compare(Synonym a, Synonym b) {
+                       if (a == null && b == null)
+      {
+        return 0;
+      }
+      if (a == null)
+      {
+        return -1;
+      }
+      if (b == null)
+      {
+        return 1;
+      }
+      if (a.equals(b))
+      {
+        return 0;
+      }
+      String catA = a.getCategory();
+      String catB = b.getCategory();
+      if (catA == null && catB != null)
+      {
+        return 1;
+      }
+      if (catA != null && catB == null)
+      {
+        return -1;
+      }
+      if (catA != null && catB != null)
+      {
+        int comp = catA.compareToIgnoreCase(catB);
+        if (comp != 0)
+        {
+          return comp;
+        }
+      }
+      // todo check for null name
+      return a.getName().compareToIgnoreCase(b.getName());
+    }
+       };
+
+       public Synonym() {
+       }
+       public String getName() {
+               return name;
+       }
+       public void setName(String name) {
+               this.name = name;
+       }
+       public String getCategory() {
+               return category;
+       }
+       public void setCategory(String category) {
+               this.category = category;
+       }
+       public int getScope() {
+               return scope;
+       }
+       public void setScope(int scope) {
+               this.scope = scope;
+       }
+       @Override
+       public int compareTo(Synonym o) {
+               return COMPARATOR.compare(this, o);
+       }
+}
index 7990d21..d9bd991 100644 (file)
@@ -61,25 +61,24 @@ public class AlignViewControllerTest
   public void testFindColumnsWithFeature()
   {
     SequenceI seq1 = new Sequence("seq1", "-a-MMMaaaaaaaaaaaaaaaa");
-    SequenceI seq2 = new Sequence("seq2", "aa--aMM-MMMMMaaaaaaaaaa");
+    SequenceI seq2 = new Sequence("seq2/11-30", "aa--aMM-MMMMMaaaaaaaaaa");
     SequenceI seq3 = new Sequence("seq3", "abcab-caD-aaMMMMMaaaaa");
     SequenceI seq4 = new Sequence("seq4", "abc--abcaaaaaaaaaaaaaa");
 
     /*
-     * features start/end are base 1
+     * features
      */
-    seq1.addSequenceFeature(new SequenceFeature("Metal", "desc", 2, 4, 0f,
-            null));
-    seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
-            null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10,
-            10f,
-            null));
-    seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
-            10f, null));
+    seq1.addSequenceFeature(
+            new SequenceFeature("Metal", "desc", 2, 4, 0f, null));
+    seq1.addSequenceFeature(
+            new SequenceFeature("Helix", "desc", 1, 15, 0f, null));
+    seq2.addSequenceFeature(
+            new SequenceFeature("Metal", "desc", 14, 20, 10f, null));
+    seq3.addSequenceFeature(
+            new SequenceFeature("Metal", "desc", 11, 15, 10f, null));
     // disulfide bond is a 'contact feature' - only select its 'start' and 'end'
-    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc",
-            8, 12, 0f, null));
+    seq3.addSequenceFeature(
+            new SequenceFeature("disulfide bond", "desc", 8, 12, 0f, null));
 
     /*
      * select the first five columns --> Metal in seq1 cols 4-5
@@ -95,15 +94,17 @@ public class AlignViewControllerTest
     /*
      * set features visible on a viewport as only visible features are selected
      */
-    AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1,
-        seq2, seq3, seq4 }), 100, 100);
+    Alignment al = new Alignment(
+            new SequenceI[]
+            { seq1, seq2, seq3, seq4 });
+    AlignFrame af = new AlignFrame(al, 100, 100);
     af.getFeatureRenderer().findAllFeatures(true);
 
     AlignViewController avc = new AlignViewController(af, af.getViewport(),
             af.alignPanel);
 
     BitSet bs = new BitSet();
-    int seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    int seqCount = avc.findColumnsWithFeature(sg, bs, "Metal");
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(3)); // base 0
@@ -114,7 +115,7 @@ public class AlignViewControllerTest
      */
     sg.setEndRes(6);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "Metal");
     assertEquals(2, seqCount);
     assertEquals(4, bs.cardinality());
     assertTrue(bs.get(3));
@@ -128,7 +129,7 @@ public class AlignViewControllerTest
     sg.setStartRes(13);
     sg.setEndRes(13);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "Metal");
     assertEquals(1, seqCount);
     assertEquals(1, bs.cardinality());
     assertTrue(bs.get(13));
@@ -139,7 +140,7 @@ public class AlignViewControllerTest
     sg.setStartRes(17);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "Metal");
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
@@ -156,7 +157,7 @@ public class AlignViewControllerTest
     sg.setStartRes(0);
     sg.setEndRes(6);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "Metal");
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(5));
@@ -168,7 +169,7 @@ public class AlignViewControllerTest
     sg.setStartRes(10);
     sg.setEndRes(12);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "disulfide bond");
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
@@ -178,19 +179,42 @@ public class AlignViewControllerTest
     sg.setStartRes(5);
     sg.setEndRes(17);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "disulfide bond");
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(8));
     assertTrue(bs.get(13));
 
     /*
+     * look for multiple features; should match 
+     * transcript_variant in seq3 positions 3-6, columns 3-7
+     * sequence_variant in seq2 positions 15-18, columns 7-11
+     * transcript_variant in seq3 positions 8 and 12, columns 9 and 14 
+     */
+    seq3.addSequenceFeature(new SequenceFeature("transcript_variant",
+            "desc", 3, 6, 0f, null));
+    seq2.addSequenceFeature(new SequenceFeature("sequence_variant", "desc",
+            15, 18, 0f, null));
+    sg.setStartRes(0);
+    sg.setEndRes(20);
+    bs.clear();
+    seqCount = avc.findColumnsWithFeature(sg, bs, "transcript_variant",
+            "sequence_variant", "disulfide bond", "junk");
+    assertEquals(2, seqCount);
+    assertEquals(10, bs.cardinality()); // 2-10 and 13, base 0
+    for (int i = 2; i <= 10; i++)
+    {
+      assertTrue(bs.get(i));
+    }
+    assertTrue(bs.get(13));
+
+    /*
      * look for a feature that isn't there
      */
     sg.setStartRes(0);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = avc.findColumnsWithFeature("Pfam", sg, bs);
+    seqCount = avc.findColumnsWithFeature(sg, bs, "Pfam");
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
   }
diff --git a/test/jalview/datamodel/ontology/OntologyBaseTest.java b/test/jalview/datamodel/ontology/OntologyBaseTest.java
new file mode 100644 (file)
index 0000000..64812c3
--- /dev/null
@@ -0,0 +1,164 @@
+package jalview.datamodel.ontology;
+
+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 OntologyBaseTest
+{
+  @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_nucleotide()
+  {
+    /*
+     * 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");
+
+    /*
+     * hierarchy from stop_gained
+     */
+    Map<String, List<String>> map = SequenceOntologyFactory.getInstance()
+            .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]");
+  }
+
+  @Test(groups = "Functional")
+  public void testfindSequenceOntologyGroupings_peptide()
+  {
+    /*
+     * typical Uniprot feature types
+     */
+    List<String> featureTypes = Arrays.asList("BETA-TURN-IR", "NEST-RL",
+            "BETA-BULGE", "ALPHA-BETA-MOTIF", "ASX-TURN-IR",
+            "GAMMA-TURN-CLASSIC", "GAMMA-TURN-INVERSE", "BETA-TURN-IL",
+            "BETA-TURN-IIL");
+  
+    /*
+     * hierarchy from GAMMA-TURN-INVERSE
+     * this is a synonym for 
+     */
+    Map<String, List<String>> map = SequenceOntologyFactory.getInstance()
+            .findSequenceOntologyGroupings("TURN", featureTypes);
+    assertEquals(map.size(), 10);
+  }
+
+}
index 31e1887..d21322f 100644 (file)
  */
 package jalview.ext.so;
 
-import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
 
+import jalview.bin.Cache;
 import jalview.gui.JvOptionPane;
-import jalview.io.gff.SequenceOntologyI;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.biojava.nbio.ontology.Synonym;
+import org.biojava.nbio.ontology.Term;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SequenceOntologyTest
 {
 
@@ -39,11 +54,12 @@ public class SequenceOntologyTest
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  private SequenceOntologyI so;
+  private SequenceOntology so;
 
   @BeforeClass(alwaysRun = true)
   public void setUp()
   {
+    Cache.initLogger();
     long now = System.currentTimeMillis();
     try
     {
@@ -132,4 +148,176 @@ public class SequenceOntologyTest
     assertTrue(so.isA("inframe_deletion", "sequence_variant"));
     assertTrue(so.isA("inframe_insertion", "sequence_variant"));
   }
+
+  @Test(groups = "Functional")
+  public void testGetChildTerms()
+  {
+    List<String> terms = Collections.<String> emptyList();
+    List<String> children = so.getChildTerms("exon", terms);
+    assertTrue(children.isEmpty());
+  
+    terms = Arrays.asList("gene", "transcript", "snRNA", "junk", "mRNA");
+    children = so.getChildTerms("exon", terms);
+    assertTrue(children.isEmpty());
+    children = so.getChildTerms("transcript", terms);
+    assertEquals(children.size(), 2);
+    assertTrue(children.contains("snRNA"));
+    assertTrue(children.contains("mRNA"));
+  
+    terms = Arrays.asList("gene", "transcript", "synonymous_variant",
+            "stop_lost", "chain");
+    children = so.getChildTerms("sequence_variant", terms);
+    assertEquals(children.size(), 2);
+    assertTrue(children.contains("synonymous_variant"));
+    assertTrue(children.contains("stop_lost"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetParentTerms()
+  {
+    Set<String> terms = new HashSet<>();
+    terms.add("sequence_variant");
+    terms.add("NMD_transcript_variant");
+    terms.add("stop_lost");
+    terms.add("chain"); // not an SO term
+  
+    Set<String> parents = so.getParentTerms(terms);
+    assertEquals(parents.size(), 2);
+    assertTrue(parents.contains("sequence_variant"));
+    assertTrue(parents.contains("chain"));
+  }
+
+  @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");
+    assertNull(roots);
+    roots = so.getRootParents(null);
+    assertNull(roots);
+
+    roots = so.getRootParents("stop_gained");
+    assertEquals(roots.size(), 1);
+    assertEquals(roots.get(0), "sequence_variant");
+
+    roots = so.getRootParents("sequence_variant");
+    assertEquals(roots.size(), 1);
+    assertEquals(roots.get(0), "sequence_variant");
+
+    roots = so.getRootParents("alanine");
+    assertEquals(roots.size(), 1);
+    assertEquals(roots.get(0), "sequence_feature");
+
+    /*
+     * verify these are now cached
+     */
+    Map<String, List<String>> cached = (Map<String, List<String>>) PA
+            .getValue(so, "rootParents");
+    List<String> parents = cached.get("stop_gained");
+    assertEquals(parents.size(), 1);
+    parents = cached.get("sequence_variant");
+    assertEquals(parents.size(), 1);
+    assertTrue(parents.contains("sequence_variant"));
+    parents = cached.get("alanine");
+    assertEquals(parents.size(), 1);
+    assertTrue(parents.contains("sequence_feature"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetTerm()
+  {
+    assertNull(so.getTerm(null));
+    assertNull(so.getTerm("!*£&!"));
+
+    Term t = so.getTerm("SO:0000084");
+    assertNotNull(t);
+    assertEquals(t.getDescription(), "micronuclear_sequence");
+    // name lookup is case sensitive
+    assertNull(so.getTerm("so:0000084"));
+
+    t = so.getTerm("alpha_helix");
+    assertNotNull(t);
+    Object[] synonyms = t.getSynonyms();
+    assertEquals(synonyms.length, 2);
+    assertEquals(((Synonym) synonyms[0]).getName(), "a-helix");
+    assertEquals(((Synonym) synonyms[1]).getName(), "helix");
+    // case-insensitive description lookup
+    Term t2 = so.getTerm("ALPHA_HELIX");
+    assertSame(t, t2);
+    // can also retrieve by synonym
+    t2 = so.getTerm("a-helix");
+    assertSame(t, t2);
+
+    t = so.getTerm("serine_threonine_motif");
+    t2 = so.getTerm("ST-MOTIF"); // synonym is "st_motif"
+    assertNotNull(t);
+    assertSame(t, t2);
+
+    /*
+     * if a synonym is ambiguous within a hierarchy,
+     * we keep it for the most general term (always valid)
+     * helix is a synonym for
+     *   alpha_helix (isA) right_handed_peptide_helix (isA) peptide_helix
+     * motif is a synonym for polypeptide_conserved_motif (isA) polypeptide_motif
+     *    
+     */
+    t = so.getTerm("helix");
+    assertNotNull(t);
+    assertEquals(t.getDescription(), "peptide_helix");
+    t = so.getTerm("motif");
+    assertNotNull(t);
+    assertEquals(t.getDescription(), "polypeptide_motif");
+
+    /*
+     * ambiguous synonyms with no mutual hierarchy are not cached
+     * 'sequence variation' is a synonym for 
+     * sequence_alteration SO:0001059
+     * alternate_sequence_site SO:0001149
+     * and these have no 'isA' relationship
+     */
+    assertNull(so.getTerm("sequence_variation"));
+
+    /*
+     * nmd_transcript is synonym for SO:0001621:NMD_transcript_variant 
+     * and also the description for SO:0002114:NMD_transcript
+     * since v3.1 of so-simple.obo
+     */
+    t = so.getTerm("SO:0002114");
+    assertNotNull(t);
+    t2 = so.getTerm("SO:0001621");
+    assertNotNull(t2);
+    assertSame(t, so.getTerm("nmd_transcript"));
+    assertSame(t2, so.getTerm("nmd_transcript_variant"));
+  }
+
+  @Test(groups = "Functional")
+  public void testCanonicalise()
+  {
+    assertNull(SequenceOntology.canonicalise(null));
+    assertEquals(SequenceOntology.canonicalise("A-b_c"), "a_b_c");
+    assertEquals(SequenceOntology.canonicalise("A-b-C"), "a_b_c");
+    assertEquals(SequenceOntology.canonicalise("metal binding site"), "metal_binding_site");
+    String s = "thisought_nottobe_modified!";
+    String s2 = SequenceOntology.canonicalise(s);
+    assertSame(s, s2);
+  }
 }
index 3e82547..3fe56b7 100644 (file)
@@ -117,15 +117,18 @@ public class AlignFrameTest
   @Test(groups = "Functional")
   public void testHideFeatureColumns()
   {
-    SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ");
-    SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
-    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f,
-            null));
-    seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4,
-            Float.NaN, null));
-    seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
-            Float.NaN, null));
+    SequenceI seq1 = new Sequence("Seq1/01-10", "A---BCDEFG-HIJ");
+    SequenceI seq2 = new Sequence("Seq2/11-20", "-AB-CDEF--GHIJ");
+    String METAL = "Metal";
+    String TURN = "Turn";
+    seq1.addSequenceFeature(
+            new SequenceFeature(METAL, "", 1, 5, 0f, null));
+    seq2.addSequenceFeature(
+            new SequenceFeature(METAL, "", 16, 20, 10f, null));
+    seq1.addSequenceFeature(
+            new SequenceFeature(TURN, "", 2, 4, Float.NaN, null));
+    seq2.addSequenceFeature(
+            new SequenceFeature(TURN, "", 17, 19, Float.NaN, null));
     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
     AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
             al.getHeight());
@@ -138,63 +141,92 @@ public class AlignFrameTest
     /*
      * hiding a feature not present does nothing
      */
-    assertFalse(alignFrame.hideFeatureColumns("exon", true));
+    assertFalse(alignFrame.hideFeatureColumns(true, "exon"));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
 
-    assertFalse(alignFrame.hideFeatureColumns("exon", false));
+    assertFalse(alignFrame.hideFeatureColumns(false, "exon"));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
 
     /*
      * hiding a feature in all columns does nothing
      */
-    assertFalse(alignFrame.hideFeatureColumns("Metal", true));
+    assertFalse(alignFrame.hideFeatureColumns(true, METAL));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 0);
 
 
     /*
      * threshold Metal to hide features where score < 5
-     * seq1 feature in columns 1-5 is hidden
-     * seq2 feature in columns 6-10 is shown
+     * seq1 feature in columns 1-8 is hidden
+     * seq2 feature in columns 8-14 is shown
+     * result: columns 8-14 are hidden
+     * note this includes gapped columns spanned by the feature
      */
     FeatureColourI fc = new FeatureColour(null, Color.red, Color.blue, null,
             0f, 10f);
     fc.setAboveThreshold(true);
     fc.setThreshold(5f);
-    alignFrame.getFeatureRenderer().setColour("Metal", fc);
-    assertTrue(alignFrame.hideFeatureColumns("Metal", true));
+    alignFrame.getFeatureRenderer().setColour(METAL, fc);
+    assertTrue(alignFrame.hideFeatureColumns(true, METAL));
     HiddenColumns hidden = alignFrame.getViewport().getAlignment().getHiddenColumns();
     assertEquals(hidden.getNumberOfRegions(), 1);
     Iterator<int[]> regions = hidden.iterator();
-    int[] next = regions.next();
-    assertEquals(next[0], 5);
-    assertEquals(next[1], 9);
+    assertEquals(regions.next(), new int[] { 7, 13 }); // base 0
+    assertFalse(regions.hasNext());
 
     /*
      * hide a feature present in some columns
-     * sequence positions [2-4], [7-9] are column positions
-     * [1-3], [6-8] base zero
+     * seq1 positions [2-4] are column positions [4-6] base zero
+     * seq2 positions [17-19] are column positions [10-12] base zero
      */
     alignFrame.getViewport().showAllHiddenColumns();
-    assertTrue(alignFrame.hideFeatureColumns("Turn", true));
+    assertTrue(alignFrame.hideFeatureColumns(true, TURN));
     regions = alignFrame.getViewport().getAlignment()
             .getHiddenColumns().iterator();
     assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
             .getNumberOfRegions(), 2);
-    next = regions.next();
-    assertEquals(next[0], 1);
-    assertEquals(next[1], 3);
-    next = regions.next();
-    assertEquals(next[0], 6);
-    assertEquals(next[1], 8);
+    assertEquals(regions.next(), new int[] { 4, 6 });
+    assertEquals(regions.next(), new int[] { 10, 12 });
+    assertFalse(regions.hasNext());
+    
+    /*
+     * hiding a contact feature should only hide start and end positions,
+     * not the intermediate columns
+     */
+    String DISULFIDE = "Disulfide Bond";
+    seq1.addSequenceFeature(
+            new SequenceFeature(DISULFIDE, "", 1, 5, 0f, null));
+    alignFrame.getViewport().showAllHiddenColumns();
+    assertTrue(alignFrame.hideFeatureColumns(true, DISULFIDE));
+    regions = alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .iterator();
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 2);
+    assertEquals(regions.next(), new int[] { 0, 0 });
+    assertEquals(regions.next(), new int[] { 7, 7 });
+    assertFalse(regions.hasNext());
+
+    /*
+     * hide multiple feature types:
+     * TURN columns hides 4-6, 10-12
+     * DISULFIDE columns hides 0, 7
+     * combined is { 0-0, 4-7, 10-12 }
+     */
+    alignFrame.getViewport().showAllHiddenColumns();
+    assertTrue(alignFrame.hideFeatureColumns(true, TURN, DISULFIDE));
+    regions = alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .iterator();
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 3);
+    assertEquals(regions.next(), new int[] { 0, 0 });
+    assertEquals(regions.next(), new int[] { 4, 7 });
+    assertEquals(regions.next(), new int[] { 10, 12 });
+    assertFalse(regions.hasNext());
   }
 
   /**
index 0766666..abc9fef 100644 (file)
@@ -1,17 +1,34 @@
 package jalview.io.gff;
 
-import static org.testng.AssertJUnit.assertFalse;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
 
+import jalview.datamodel.ontology.OntologyI;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class SequenceOntologyLiteTest
 {
+  private OntologyI so;
+
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    so = new SequenceOntologyLite();
+  }
+
   @Test(groups = "Functional")
   public void testIsA_sequenceVariant()
   {
-    SequenceOntologyI so = new SequenceOntologyLite();
-
     assertFalse(so.isA("CDS", "sequence_variant"));
     assertTrue(so.isA("sequence_variant", "sequence_variant"));
 
@@ -34,4 +51,55 @@ public class SequenceOntologyLiteTest
     assertTrue(so.isA("inframe_insertion", "sequence_variant"));
     assertTrue(so.isA("splice_region_variant", "sequence_variant"));
   }
+
+  @Test(groups = "Functional")
+  public void testGetParentTerms()
+  {
+    Set<String> terms = new HashSet<>();
+    terms.add("sequence_variant");
+    terms.add("NMD_transcript_variant");
+    terms.add("stop_lost");
+    terms.add("chain"); // not an SO term
+  
+    Set<String> parents = so.getParentTerms(terms);
+    assertEquals(parents.size(), 2);
+    assertTrue(parents.contains("sequence_variant"));
+    assertTrue(parents.contains("chain"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetChildTerms()
+  {
+    List<String> terms = Collections.<String> emptyList();
+    List<String> children = so.getChildTerms("exon", terms);
+    assertTrue(children.isEmpty());
+
+    terms = Arrays.asList("gene", "transcript", "snRNA", "junk", "mRNA");
+    children = so.getChildTerms("exon", terms);
+    assertTrue(children.isEmpty());
+    children = so.getChildTerms("transcript", terms);
+    assertEquals(children.size(), 2);
+    assertTrue(children.contains("snRNA"));
+    assertTrue(children.contains("mRNA"));
+
+    terms = Arrays.asList("gene", "transcript", "synonymous_variant",
+            "stop_lost", "chain");
+    children = so.getChildTerms("sequence_variant", terms);
+    assertEquals(children.size(), 2);
+    assertTrue(children.contains("synonymous_variant"));
+    assertTrue(children.contains("stop_lost"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetRootParents()
+  {
+    List<String> roots = so.getRootParents("xyz");
+    assertNull(roots);
+    roots = so.getRootParents(null);
+    assertNull(roots);
+
+    roots = so.getRootParents("stop_gained");
+    assertEquals(roots.size(), 1);
+    assertEquals(roots.get(0), "sequence_variant");
+  }
 }