package jalview.datamodel.features; import jalview.datamodel.ContiguousI; import jalview.datamodel.SequenceFeature; import jalview.io.gff.SequenceOntologyFactory; import jalview.io.gff.SequenceOntologyI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; /** * A class that stores sequence features in a way that supports efficient * querying by type and location (overlap). Intended for (but not limited to) * storage of features for one sequence. * * @author gmcarstairs * */ public class SequenceFeatures implements SequenceFeaturesI { /** * a comparator for sorting features by start position ascending */ private static Comparator FORWARD_STRAND = new Comparator() { @Override public int compare(ContiguousI o1, ContiguousI o2) { return Integer.compare(o1.getBegin(), o2.getBegin()); } }; /** * a comparator for sorting features by end position descending */ private static Comparator REVERSE_STRAND = new Comparator() { @Override public int compare(ContiguousI o1, ContiguousI o2) { return Integer.compare(o2.getEnd(), o1.getEnd()); } }; /* * map from feature type to structured store of features for that type * null types are permitted (but not a good idea!) */ private Map featureStore; /** * Constructor */ public SequenceFeatures() { /* * use a TreeMap so that features are returned in alphabetical order of type * ? wrap as a synchronized map for add and delete operations */ // featureStore = Collections // .synchronizedSortedMap(new TreeMap()); featureStore = new TreeMap(); } /** * Constructor given a list of features */ public SequenceFeatures(List features) { this(); if (features != null) { for (SequenceFeature feature : features) { add(feature); } } } /** * {@inheritDoc} */ @Override public boolean add(SequenceFeature sf) { String type = sf.getType(); if (type == null) { System.err.println("Feature type may not be null: " + sf.toString()); return false; } if (featureStore.get(type) == null) { featureStore.put(type, new FeatureStore()); } return featureStore.get(type).addFeature(sf); } /** * {@inheritDoc} */ @Override public List findFeatures(int from, int to, String... type) { List result = new ArrayList(); for (String featureType : varargToTypes(type)) { FeatureStore features = featureStore.get(featureType); if (features != null) { result.addAll(features.findOverlappingFeatures(from, to)); } } return result; } /** * {@inheritDoc} */ @Override public List getAllFeatures(String... type) { List result = new ArrayList(); result.addAll(getPositionalFeatures(type)); result.addAll(getNonPositionalFeatures()); return result; } /** * {@inheritDoc} */ @Override public List getFeaturesByOntology(String... ontologyTerm) { if (ontologyTerm == null || ontologyTerm.length == 0) { return new ArrayList(); } Set featureTypes = getFeatureTypes(ontologyTerm); return getAllFeatures(featureTypes.toArray(new String[featureTypes .size()])); } /** * {@inheritDoc} */ @Override public int getFeatureCount(boolean positional, String... type) { int result = 0; for (String featureType : varargToTypes(type)) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { result += featureSet.getFeatureCount(positional); } } return result; } /** * {@inheritDoc} */ @Override public int getTotalFeatureLength(String... type) { int result = 0; for (String featureType : varargToTypes(type)) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { result += featureSet.getTotalFeatureLength(); } } return result; } /** * {@inheritDoc} */ @Override public List getPositionalFeatures(String... type) { List result = new ArrayList(); for (String featureType : varargToTypes(type)) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { result.addAll(featureSet.getPositionalFeatures()); } } return result; } /** * A convenience method that converts a vararg for feature types to an * Iterable, replacing the value with the stored feature types if it is null * or empty * * @param type * @return */ protected Iterable varargToTypes(String... type) { if (type == null || type.length == 0) { /* * no vararg parameter supplied */ return featureStore.keySet(); } /* * else make a copy of the list, and remove any null value just in case, * as it would cause errors looking up the features Map * sort in alphabetical order for consistent output behaviour */ List types = new ArrayList(Arrays.asList(type)); types.remove(null); Collections.sort(types); return types; } /** * {@inheritDoc} */ @Override public List getContactFeatures(String... type) { List result = new ArrayList(); for (String featureType : varargToTypes(type)) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { result.addAll(featureSet.getContactFeatures()); } } return result; } /** * {@inheritDoc} */ @Override public List getNonPositionalFeatures(String... type) { List result = new ArrayList(); for (String featureType : varargToTypes(type)) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { result.addAll(featureSet.getNonPositionalFeatures()); } } return result; } /** * {@inheritDoc} */ @Override public boolean delete(SequenceFeature sf) { for (FeatureStore featureSet : featureStore.values()) { if (featureSet.delete(sf)) { return true; } } return false; } /** * {@inheritDoc} */ @Override public boolean hasFeatures() { for (FeatureStore featureSet : featureStore.values()) { if (!featureSet.isEmpty()) { return true; } } return false; } /** * {@inheritDoc} */ @Override public Set getFeatureGroups(boolean positionalFeatures, String... type) { Set groups = new HashSet(); Iterable types = varargToTypes(type); for (String featureType : types) { FeatureStore featureSet = featureStore.get(featureType); if (featureSet != null) { groups.addAll(featureSet.getFeatureGroups(positionalFeatures)); } } return groups; } /** * {@inheritDoc} */ @Override public Set getFeatureTypesForGroups(boolean positionalFeatures, String... groups) { Set result = new HashSet(); for (Entry featureType : featureStore.entrySet()) { Set featureGroups = featureType.getValue().getFeatureGroups( positionalFeatures); for (String group : groups) { if (featureGroups.contains(group)) { /* * yes this feature type includes one of the query groups */ result.add(featureType.getKey()); break; } } } return result; } /** * {@inheritDoc} */ @Override public Set getFeatureTypes(String... soTerm) { Set types = new HashSet(); for (Entry entry : featureStore.entrySet()) { String type = entry.getKey(); if (!entry.getValue().isEmpty() && isOntologyTerm(type, soTerm)) { types.add(type); } } return types; } /** * Answers true if the given type is one of the specified sequence ontology * terms (or a sub-type of one), or if no terms are supplied. Answers false if * filter terms are specified and the given term does not match any of them. * * @param type * @param soTerm * @return */ protected boolean isOntologyTerm(String type, String... soTerm) { if (soTerm == null || soTerm.length == 0) { return true; } SequenceOntologyI so = SequenceOntologyFactory.getInstance(); for (String term : soTerm) { if (so.isA(type, term)) { return true; } } return false; } /** * {@inheritDoc} */ @Override public float getMinimumScore(String type, boolean positional) { return featureStore.containsKey(type) ? featureStore.get(type) .getMinimumScore(positional) : Float.NaN; } /** * {@inheritDoc} */ @Override public float getMaximumScore(String type, boolean positional) { return featureStore.containsKey(type) ? featureStore.get(type) .getMaximumScore(positional) : Float.NaN; } /** * A convenience method to sort features by start position ascending (if on * forward strand), or end position descending (if on reverse strand) * * @param features * @param forwardStrand */ public static void sortFeatures(List features, final boolean forwardStrand) { Collections.sort(features, forwardStrand ? FORWARD_STRAND : REVERSE_STRAND); } /** * {@inheritDoc} This method is 'semi-optimised': it only inspects features * for types that include the specified group, but has to inspect every * feature of those types for matching feature group. This is efficient unless * a sequence has features that share the same type but are in different * groups - an unlikely case. *

* For example, if RESNUM feature is created with group = PDBID, then features * would only be retrieved for those sequences associated with the target * PDBID (group). */ @Override public List getFeaturesForGroup(boolean positional, String group, String... type) { List result = new ArrayList(); Iterable types = varargToTypes(type); for (String featureType : types) { /* * check whether the feature type is present, and also * whether it has features for the specified group */ FeatureStore features = featureStore.get(featureType); if (features != null && features.getFeatureGroups(positional).contains(group)) { result.addAll(features.getFeaturesForGroup(positional, group)); } } return result; } /** * {@inheritDoc} */ @Override public boolean shiftFeatures(int shift) { boolean modified = false; for (FeatureStore fs : featureStore.values()) { modified |= fs.shiftFeatures(shift); } return modified; } }