From: gmungoc Date: Mon, 22 Apr 2019 14:13:52 +0000 (+0100) Subject: JAL-3010 choose feature types to apply changes to by parent SO term X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=47279fad178f912157a0d53560bd89b21f745b28;p=jalview.git JAL-3010 choose feature types to apply changes to by parent SO term --- diff --git a/resources/lang/Messages.properties b/resources/lang/Messages.properties index d091426..1492fcf 100644 --- a/resources/lang/Messages.properties +++ b/resources/lang/Messages.properties @@ -1339,8 +1339,8 @@ label.most_polymer_residues = Most Polymer Residues label.cached_structures = Cached Structures label.free_text_search = Free Text Search label.summary_view = Summary View -label.summary_view_tip = Show only top level ontology terms -label.apply_to_subtypes = Apply changes to all ''{0}'' features +label.summary_view_tip = Group Sequence Ontology terms +label.apply_to_subtypes = Apply to features and sub-types of label.apply_also_to = Apply also to: label.backupfiles_confirm_delete = Confirm delete label.backupfiles_confirm_delete_old_files = Delete the following older backup files? (see the Backups tab in Preferences for more options) diff --git a/resources/lang/Messages_es.properties b/resources/lang/Messages_es.properties index 4935a3d..6e465a6 100644 --- a/resources/lang/Messages_es.properties +++ b/resources/lang/Messages_es.properties @@ -1340,8 +1340,8 @@ label.most_polymer_residues = M label.cached_structures = Estructuras en Caché label.free_text_search = Búsqueda de texto libre label.summary_view = Vista Resumida -label.summary_view_tip = Mostrar solo términos de ontología de nivel mayor -label.apply_to_subtypes = Aplicar cambios también a todas características de tipo ''{0}'' +label.summary_view_tip = Agrupar términos de la Sequence Ontology +label.apply_to_subtypes = Aplicar cambios también a características y subtipos de label.apply_also_to = Aplicar también a: label.backupfiles_confirm_delete = Confirmar borrar label.backupfiles_confirm_delete_old_files = ¿Borrar los siguientes archivos? (ver la pestaña 'Copias' de la ventana de Preferencias para más opciones) diff --git a/src/jalview/datamodel/ontology/OntologyI.java b/src/jalview/datamodel/ontology/OntologyI.java index 5ac5a97..9ae2ad4 100644 --- a/src/jalview/datamodel/ontology/OntologyI.java +++ b/src/jalview/datamodel/ontology/OntologyI.java @@ -38,6 +38,15 @@ public interface OntologyI List getChildTerms(String parent, List terms); /** + * Answers a (possibly empty) list of the immediate parent terms of the given + * term + * + * @param term + * @return + */ + List 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. * diff --git a/src/jalview/ext/so/SequenceOntology.java b/src/jalview/ext/so/SequenceOntology.java index 8a3805d..3b8bad4 100644 --- a/src/jalview/ext/so/SequenceOntology.java +++ b/src/jalview/ext/so/SequenceOntology.java @@ -552,4 +552,20 @@ public class SequenceOntology extends OntologyBase return result; } + + @Override + public List getParents(String term) + { + List 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; + } } diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java index 0dd0f1f..e7efea9 100644 --- a/src/jalview/gui/FeatureTypeSettings.java +++ b/src/jalview/gui/FeatureTypeSettings.java @@ -52,6 +52,7 @@ import java.awt.event.MouseEvent; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -220,19 +221,24 @@ public class FeatureTypeSettings extends JalviewDialog private String rootSOTerm; /* - * feature types present in Feature Renderer which have the same Sequence - * Ontology root parent as the one this editor is acting on + * a map whose keys are Sequence Ontology terms - selected from the + * current term and its parents in the SO - whose subterms include + * additional feature types; the map entry is the list of additional + * feature types that match the key or have it as a parent term; in + * other words, distinct 'aggregations' that include the current feature type */ - private final List peerSoTerms; + private final Map> relatedSoTerms; /* * if true, filter or colour settings are also applied to - * any sub-types of rootSOTerm in the Sequence Ontology + * any sub-types of parentTerm in the Sequence Ontology */ private boolean applyFiltersToSubtypes; private boolean applyColourToSubtypes; + private String parentSOTerm; + /** * Constructor * @@ -245,20 +251,24 @@ public class FeatureTypeSettings extends JalviewDialog this.featureType = theType; ap = fr.ap; - peerSoTerms = findSequenceOntologyPeers(this.featureType); + relatedSoTerms = findSequenceOntologyGroupings(this.featureType, + fr.getRenderOrder()); /* - * save original colours and filters for this feature type - * and any sub-types, to restore on Cancel + * save original colours and filters for this feature type, + * and any related types, to restore on Cancel */ originalFilters = new HashMap<>(); originalFilters.put(theType, fr.getFeatureFilter(theType)); originalColours = new HashMap<>(); originalColours.put(theType, fr.getFeatureColours().get(theType)); - for (String child : peerSoTerms) + for (List related : relatedSoTerms.values()) { - originalFilters.put(child, fr.getFeatureFilter(child)); - originalColours.put(child, fr.getFeatureColours().get(child)); + for (String type : related) + { + originalFilters.put(type, fr.getFeatureFilter(type)); + originalColours.put(type, fr.getFeatureColours().get(type)); + } } adjusting = true; @@ -288,42 +298,57 @@ public class FeatureTypeSettings extends JalviewDialog } /** - * Answers a (possibly empty) list of feature types known to the Feature - * Renderer which share a top level Sequence Ontology parent with the current - * feature type. The current type is not included. + * Answers a (possibly empty) map of any Sequence Ontology terms (the current + * feature type and its parents) which incorporate additional known feature + * types (the map entry). + *

+ * For example if {@code stop_gained} and {@code stop_lost} are known feature + * types, then SO term {@ nonsynonymous_variant} is the first common parent of + * both terms * + * @param featureType + * the current feature type being configured + * @param featureTypes + * all known feature types on the alignment * @return */ - protected List findSequenceOntologyPeers(String featureType) + protected static Map> findSequenceOntologyGroupings( + String featureType, List featureTypes) { - List peers = new ArrayList<>(); + List sortedTypes = new ArrayList<>(featureTypes); + Collections.sort(sortedTypes); + + Map> parents = new HashMap<>(); /* - * first find the SO term (if any) that is the root - * parent of the current type + * method: + * walk up featureType and all of its parents + * find other feature types which are subsumed by each term + * add each distinct aggregation of included feature types to the map */ + List candidates = new ArrayList<>(); SequenceOntologyI so = SequenceOntologyFactory.getInstance(); - List roots = so.getRootParents(featureType); - if (roots == null || roots.size() > 1) + candidates.add(featureType); + while (!candidates.isEmpty()) { - /* - * feature type is not an SO term, or has ambiguous root - */ - return peers; - } - rootSOTerm = roots.get(0); - - List types = fr.getRenderOrder(); - for (String type : types) - { - if (!type.equals(featureType) && so.isA(type, rootSOTerm)) + String term = candidates.remove(0); + List includedFeatures = new ArrayList<>(); + for (String type : sortedTypes) { - peers.add(type); + if (!type.equals(featureType) && so.isA(type, term)) + { + includedFeatures.add(type); + } } - + if (!includedFeatures.isEmpty() + && !parents.containsValue(includedFeatures)) + { + parents.put(term, includedFeatures); + } + candidates.addAll(so.getParents(term)); } - Collections.sort(peers); // sort for ease of reading in tooltip - return peers; + + return parents; } /** @@ -811,9 +836,9 @@ public class FeatureTypeSettings extends JalviewDialog MessageManager.getString("action.colour"), true); /* - * option to apply colour to peer types as well (if there are any) + * option to apply colour to other selected types as well */ - if (!peerSoTerms.isEmpty()) + if (!relatedSoTerms.isEmpty()) { applyColourToSubtypes = false; colourByPanel.add(initSubtypesPanel(false)); @@ -918,8 +943,8 @@ public class FeatureTypeSettings extends JalviewDialog } /** - * Constructs and returns a panel with a checkbox for the option to apply any - * changes also to sub-types of the feature type + * Constructs and returns a panel with the option to apply any changes also to + * sub-types of SO terms at or above the feature type * * @return */ @@ -927,10 +952,46 @@ public class FeatureTypeSettings extends JalviewDialog { JPanel toSubtypes = new JPanel(new FlowLayout(FlowLayout.LEFT)); toSubtypes.setBackground(Color.WHITE); + + /* + * checkbox 'apply to sub-types of...' + */ JCheckBox applyToSubtypesCB = new JCheckBox(MessageManager .formatMessage("label.apply_to_subtypes", rootSOTerm)); - applyToSubtypesCB.setToolTipText(getSubtypesTooltip()); - applyToSubtypesCB.addActionListener(new ActionListener() + toSubtypes.add(applyToSubtypesCB); + + /* + * combobox to choose 'parent' of sub-types + */ + List 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(String o1, String o2) + { + return Integer.compare(relatedSoTerms.get(o1).size(), + relatedSoTerms.get(o2).size()); + } + }); + List tooltips = new ArrayList<>(); + for (String term : soTerms) + { + tooltips.add(getSOTermsTooltip(relatedSoTerms.get(term))); + } + JComboBox parentType = JvSwingUtils + .buildComboWithTooltips(soTerms, tooltips); + toSubtypes.add(parentType); + + /* + * on toggle of checkbox, or change of parent SO term, + * reset and then reapply filters to the selected scope + */ + final ActionListener action = new ActionListener() { /* * reset and reapply settings on toggle of checkbox @@ -938,6 +999,7 @@ public class FeatureTypeSettings extends JalviewDialog @Override public void actionPerformed(ActionEvent e) { + parentSOTerm = (String) parentType.getSelectedItem(); if (forFilters) { applyFiltersToSubtypes = applyToSubtypesCB.isSelected(); @@ -951,8 +1013,9 @@ public class FeatureTypeSettings extends JalviewDialog colourChanged(true); } } - }); - toSubtypes.add(applyToSubtypesCB); + }; + applyToSubtypesCB.addActionListener(action); + parentType.addActionListener(action); return toSubtypes; } @@ -1001,7 +1064,7 @@ public class FeatureTypeSettings extends JalviewDialog fr.setColour(featureType, acg); if (applyColourToSubtypes) { - for (String child : peerSoTerms) + for (String child : relatedSoTerms.get(parentSOTerm)) { fr.setColour(child, acg); } @@ -1155,6 +1218,10 @@ public class FeatureTypeSettings extends JalviewDialog ap.paintAlignment(true, true); } + /** + * Restores filters for all feature types to their values when the dialog was + * opened + */ protected void restoreOriginalFilters() { for (Entry entry : originalFilters @@ -1164,6 +1231,10 @@ public class FeatureTypeSettings extends JalviewDialog } } + /** + * Restores colours for all feature types to their values when the dialog was + * opened + */ protected void restoreOriginalColours() { for (Entry entry : originalColours.entrySet()) @@ -1317,9 +1388,9 @@ public class FeatureTypeSettings extends JalviewDialog outerPanel.setBackground(Color.white); /* - * option to apply colour to peer types as well (if there are any) + * option to apply colour to other selected types as well */ - if (!peerSoTerms.isEmpty()) + if (!relatedSoTerms.isEmpty()) { applyFiltersToSubtypes = false; outerPanel.add(initSubtypesPanel(true)); @@ -1381,16 +1452,18 @@ public class FeatureTypeSettings extends JalviewDialog } /** - * Builds a tooltip for the 'Apply to subtypes' checkbox with a list of - * subtypes of this feature type + * Builds a tooltip for the 'Apply also to...' combobox with a list of known + * feature types (excluding the current type) which are sub-types of the + * selected Sequence Ontology term * + * @param * @return */ - protected String getSubtypesTooltip() + protected String getSOTermsTooltip(List list) { - StringBuilder sb = new StringBuilder(20 * peerSoTerms.size()); + StringBuilder sb = new StringBuilder(20 * relatedSoTerms.size()); sb.append(MessageManager.getString("label.apply_also_to")); - for (String child : peerSoTerms) + for (String child : list) { sb.append("
").append(child); } @@ -1934,7 +2007,7 @@ public class FeatureTypeSettings extends JalviewDialog fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined); if (applyFiltersToSubtypes) { - for (String child : peerSoTerms) + for (String child : relatedSoTerms.get(parentSOTerm)) { fr.setFeatureFilter(child, combined.isEmpty() ? null : combined); } diff --git a/src/jalview/io/gff/SequenceOntologyLite.java b/src/jalview/io/gff/SequenceOntologyLite.java index d4c2278..6abb5d6 100644 --- a/src/jalview/io/gff/SequenceOntologyLite.java +++ b/src/jalview/io/gff/SequenceOntologyLite.java @@ -306,4 +306,11 @@ public class SequenceOntologyLite extends OntologyBase return top.isEmpty() ? null : top; } + + @Override + public List getParents(String term) + { + List result = parents.get(term); + return result == null ? new ArrayList<>() : result; + } } diff --git a/test/jalview/ext/so/SequenceOntologyTest.java b/test/jalview/ext/so/SequenceOntologyTest.java index 9cfbe88..7eb01c9 100644 --- a/test/jalview/ext/so/SequenceOntologyTest.java +++ b/test/jalview/ext/so/SequenceOntologyTest.java @@ -183,6 +183,26 @@ public class SequenceOntologyTest } @Test(groups = "Functional") + public void testGetParents() + { + // invalid term + List 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 roots = so.getRootParents("xyz"); diff --git a/test/jalview/gui/FeatureTypeSettingsTest.java b/test/jalview/gui/FeatureTypeSettingsTest.java new file mode 100644 index 0000000..6a32b83 --- /dev/null +++ b/test/jalview/gui/FeatureTypeSettingsTest.java @@ -0,0 +1,147 @@ +package jalview.gui; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import jalview.ext.so.SequenceOntology; +import jalview.io.gff.SequenceOntologyFactory; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class FeatureTypeSettingsTest +{ + @BeforeClass(alwaysRun = true) + public void setUp() + { + SequenceOntologyFactory.setInstance(new SequenceOntology()); + } + + @AfterClass(alwaysRun = true) + public void tearDown() + { + SequenceOntologyFactory.setInstance(null); + } + + @Test(groups="Functional") + public void testfindSequenceOntologyGroupings() + { + /* + * typical gnomAD feature types, plus the top level 'sequence_variant' as in dbSNP + */ + List featureTypes = Arrays.asList("sequence_variant", + "inframe_insertion", "stop_lost", "stop_gained", + "5_prime_UTR_variant", "non_coding_transcript_exon_variant", + "synonymous_variant", "inframe_deletion", "frameshift_variant", + "upstream_gene_variant", "splice_region_variant", + "missense_variant"); + + /* + * for stop_gained: + * transcript_variant further adds 5_prime_UTR_variant, + * non_coding_transcript_exon_variant, synonymous_variant, splice_region_variant + * feature_variant further adds upstream_gene_variant + * sequence_variant further adds sequence_variant + */ + Map> map = FeatureTypeSettings + .findSequenceOntologyGroupings("stop_gained", featureTypes); + assertEquals(map.size(), 10); + + /* + * feature_truncation adds inframe_deletion + */ + List 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]"); + } +}