JAL-3010 discover root parent SO terms at runtime
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Sat, 20 Apr 2019 06:00:47 +0000 (07:00 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Sat, 20 Apr 2019 06:00:47 +0000 (07:00 +0100)
src/jalview/datamodel/ontology/OntologyBase.java
src/jalview/datamodel/ontology/OntologyI.java
src/jalview/ext/so/SequenceOntology.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/io/gff/SequenceOntologyLite.java
test/jalview/ext/so/SequenceOntologyTest.java
test/jalview/io/gff/SequenceOntologyLiteTest.java

index 25dae22..8bdebbd 100644 (file)
@@ -1,8 +1,10 @@
 package jalview.datamodel.ontology;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -13,6 +15,8 @@ import java.util.Set;
  */
 public abstract class OntologyBase implements OntologyI
 {
+  protected Map<String, List<String>> rootParents = new HashMap<>();
+
   @Override
   public Set<String> getParentTerms(Set<String> terms)
   {
index 545a3c7..5ac5a97 100644 (file)
@@ -58,4 +58,16 @@ public interface OntologyI
    * @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);
+
 }
\ No newline at end of file
index 7842294..8a3805d 100644 (file)
@@ -35,6 +35,7 @@ import java.util.HashMap;
 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;
 
@@ -473,4 +474,82 @@ public class SequenceOntology extends OntologyBase
       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;
+  }
 }
index 538c93c..0dd0f1f 100644 (file)
@@ -22,7 +22,6 @@ package jalview.gui;
 
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureColourI;
-import jalview.bin.Cache;
 import jalview.datamodel.GraphLine;
 import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.FeatureAttributes.Datatype;
@@ -85,11 +84,6 @@ import javax.swing.plaf.basic.BasicArrowButton;
  */
 public class FeatureTypeSettings extends JalviewDialog
 {
-  /*
-   * 'top level' Sequence Ontology terms
-   */
-  private final static String SO_ROOTS = "sequence_variant,sequence_attribute,sequence_collection,sequence_feature";
-
   private final static String LABEL_18N = MessageManager
           .getString("label.label");
 
@@ -309,23 +303,15 @@ public class FeatureTypeSettings extends JalviewDialog
      * parent of the current type
      */
     SequenceOntologyI so = SequenceOntologyFactory.getInstance();
-    String[] roots = Cache.getDefault("SO_ROOTS", SO_ROOTS).split(",");
-    rootSOTerm = null;
-    for (String root : roots)
-    {
-      if (so.isA(featureType, root.trim()))
-      {
-        rootSOTerm = root;
-        break;
-      }
-    }
-    if (rootSOTerm == null)
+    List<String> roots = so.getRootParents(featureType);
+    if (roots == null || roots.size() > 1)
     {
       /*
-       * feature type is not an SO term
+       * feature type is not an SO term, or has ambiguous root
        */
       return peers;
     }
+    rootSOTerm = roots.get(0);
 
     List<String> types = fr.getRenderOrder();
     for (String type : types)
index 670d887..d4c2278 100644 (file)
@@ -253,4 +253,57 @@ public class SequenceOntologyLite extends OntologyBase
       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;
+  }
 }
index c7776a3..489e462 100644 (file)
@@ -22,6 +22,7 @@ package jalview.ext.so;
 
 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;
@@ -177,4 +178,25 @@ public class SequenceOntologyTest
     assertTrue(parents.contains("sequence_variant"));
     assertTrue(parents.contains("chain"));
   }
+
+  @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");
+  }
 }
index 3076f96..abc9fef 100644 (file)
@@ -2,6 +2,7 @@ package jalview.io.gff;
 
 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;
@@ -88,4 +89,17 @@ public class SequenceOntologyLiteTest
     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");
+  }
 }