X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fio%2FFeaturesFile.java;h=aa21b0f653f870544c337266c37e8fe8f1fe5fe1;hb=72fb6df14f1ff426bbafa18dcc9a90450da93018;hp=d2282b1f05c37f77dc3cc42af7a34fd26f483030;hpb=be762d8d9c71a7aa3121e845c45911c7192b7827;p=jalview.git diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index d2282b1..aa21b0f 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -24,6 +24,7 @@ import jalview.analysis.AlignmentUtils; import jalview.analysis.SequenceIdMatcher; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; +import jalview.api.FeatureRenderer; import jalview.api.FeaturesSourceI; import jalview.datamodel.AlignedCodonFrame; import jalview.datamodel.Alignment; @@ -31,6 +32,8 @@ import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceDummy; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; +import jalview.datamodel.features.FeatureMatcherSet; +import jalview.datamodel.features.FeatureMatcherSetI; import jalview.io.gff.GffHelperBase; import jalview.io.gff.GffHelperFactory; import jalview.io.gff.GffHelperI; @@ -68,12 +71,20 @@ import java.util.Map.Entry; */ public class FeaturesFile extends AlignFile implements FeaturesSourceI { + private static final String TAB_REGEX = "\\t"; + + private static final String STARTGROUP = "STARTGROUP"; + + private static final String ENDGROUP = "ENDGROUP"; + + private static final String STARTFILTERS = "STARTFILTERS"; + + private static final String ENDFILTERS = "ENDFILTERS"; + private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED"; private static final String NOTE = "Note"; - protected static final String TAB = "\t"; - protected static final String GFF_VERSION = "##gff-version"; private AlignmentI lastmatchedAl = null; @@ -94,11 +105,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI /** * Constructor which does not parse the file immediately * - * @param file + * @param file File or String filename * @param paste * @throws IOException */ - public FeaturesFile(String file, DataSourceType paste) + public FeaturesFile(Object file, DataSourceType paste) throws IOException { super(false, file, paste); @@ -121,7 +132,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param type * @throws IOException */ - public FeaturesFile(boolean parseImmediately, String file, + public FeaturesFile(boolean parseImmediately, Object file, DataSourceType type) throws IOException { super(parseImmediately, file, type); @@ -169,7 +180,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param align * - alignment/dataset containing sequences that are to be annotated * @param colours - * - hashtable to store feature colour definitions + * - map to store feature colour definitions * @param removeHTML * - process html strings into plain text * @param relaxedIdmatching @@ -180,11 +191,34 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI Map colours, boolean removeHTML, boolean relaxedIdmatching) { - Map gffProps = new HashMap(); + return parse(align, colours, null, removeHTML, relaxedIdmatching); + } + + /** + * Parse GFF or Jalview format sequence features file + * + * @param align + * - alignment/dataset containing sequences that are to be annotated + * @param colours + * - map to store feature colour definitions + * @param filters + * - map to store feature filter definitions + * @param removeHTML + * - process html strings into plain text + * @param relaxedIdmatching + * - when true, ID matches to compound sequence IDs are allowed + * @return true if features were added + */ + public boolean parse(AlignmentI align, + Map colours, + Map filters, boolean removeHTML, + boolean relaxedIdmatching) + { + Map gffProps = new HashMap<>(); /* * keep track of any sequences we try to create from the data */ - List newseqs = new ArrayList(); + List newseqs = new ArrayList<>(); String line = null; try @@ -204,7 +238,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI continue; } - gffColumns = line.split("\\t"); // tab as regex + gffColumns = line.split(TAB_REGEX); if (gffColumns.length == 1) { if (line.trim().equalsIgnoreCase("GFF")) @@ -218,18 +252,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - if (gffColumns.length > 1 && gffColumns.length < 4) + if (gffColumns.length > 0 && gffColumns.length < 4) { /* * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or * a feature type colour specification */ String ft = gffColumns[0]; - if (ft.equalsIgnoreCase("startgroup")) + if (ft.equalsIgnoreCase(STARTFILTERS)) + { + parseFilters(filters); + continue; + } + if (ft.equalsIgnoreCase(STARTGROUP)) { featureGroup = gffColumns[1]; } - else if (ft.equalsIgnoreCase("endgroup")) + else if (ft.equalsIgnoreCase(ENDGROUP)) { // We should check whether this is the current group, // but at present there's no way of showing more than 1 group @@ -290,6 +329,43 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** + * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type + * filter to the map for each line parsed. After exit from this method, + * nextLine() should return the line after ENDFILTERS (or we are already at + * end of file if ENDFILTERS was missing). + * + * @param filters + * @throws IOException + */ + protected void parseFilters(Map filters) + throws IOException + { + String line; + while ((line = nextLine()) != null) + { + if (line.toUpperCase().startsWith(ENDFILTERS)) + { + return; + } + String[] tokens = line.split(TAB_REGEX); + if (tokens.length != 2) + { + System.err.println(String.format("Invalid token count %d for %d", + tokens.length, line)); + } + else + { + String featureType = tokens[0]; + FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]); + if (fm != null && filters != null) + { + filters.put(featureType, fm); + } + } + } + } + + /** * Try to parse a Jalview format feature specification and add it as a * sequence feature to any matching sequences in the alignment. Returns true * if successful (a feature was added), or false if not. @@ -493,20 +569,21 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * included (with no check on type or group). * * @param sequences - * source of features - * @param visible - * map of colour for each visible feature type - * @param visibleFeatureGroups + * @param fr * @param includeNonPositional * if true, include non-positional features (regardless of group or * type) * @return */ public String printJalviewFormat(SequenceI[] sequences, - Map visible, - List visibleFeatureGroups, boolean includeNonPositional) + FeatureRenderer fr, boolean includeNonPositional) { - if (!includeNonPositional && (visible == null || visible.isEmpty())) + Map visibleColours = fr + .getDisplayedFeatureCols(); + Map featureFilters = fr.getFeatureFilters(); + + if (!includeNonPositional + && (visibleColours == null || visibleColours.isEmpty())) { // no point continuing. return "No Features Visible"; @@ -517,9 +594,10 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI */ // TODO: decide if feature links should also be written here ? StringBuilder out = new StringBuilder(256); - if (visible != null) + if (visibleColours != null) { - for (Entry featureColour : visible.entrySet()) + for (Entry featureColour : visibleColours + .entrySet()) { FeatureColourI colour = featureColour.getValue(); out.append(colour.toJalviewFormat(featureColour.getKey())).append( @@ -527,79 +605,157 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } } - String[] types = visible == null ? new String[0] : visible.keySet() - .toArray(new String[visible.keySet().size()]); + String[] types = visibleColours == null ? new String[0] + : visibleColours.keySet() + .toArray(new String[visibleColours.keySet().size()]); + + /* + * feature filters if any + */ + outputFeatureFilters(out, visibleColours, featureFilters); + + /* + * output features within groups + */ + int count = outputFeaturesByGroup(out, fr, types, sequences, + includeNonPositional); + + return count > 0 ? out.toString() : "No Features Visible"; + } + + /** + * Outputs any feature filters defined for visible feature types, sandwiched by + * STARTFILTERS and ENDFILTERS lines + * + * @param out + * @param visible + * @param featureFilters + */ + void outputFeatureFilters(StringBuilder out, + Map visible, + Map featureFilters) + { + if (visible == null || featureFilters == null + || featureFilters.isEmpty()) + { + return; + } + + boolean first = true; + for (String featureType : visible.keySet()) + { + FeatureMatcherSetI filter = featureFilters.get(featureType); + if (filter != null) + { + if (first) + { + first = false; + out.append(newline).append(STARTFILTERS).append(newline); + } + out.append(featureType).append(TAB).append(filter.toStableString()) + .append(newline); + } + } + if (!first) + { + out.append(ENDFILTERS).append(newline); + } + + } + + /** + * Appends output of visible sequence features within feature groups to the + * output buffer. Groups other than the null or empty group are sandwiched by + * STARTGROUP and ENDGROUP lines. Answers the number of features written. + * + * @param out + * @param fr + * @param featureTypes + * @param sequences + * @param includeNonPositional + * @return + */ + private int outputFeaturesByGroup(StringBuilder out, + FeatureRenderer fr, String[] featureTypes, + SequenceI[] sequences, boolean includeNonPositional) + { + List featureGroups = fr.getFeatureGroups(); /* * sort groups alphabetically, and ensure that features with a * null or empty group are output after those in named groups */ - List sortedGroups = new ArrayList(visibleFeatureGroups); + List sortedGroups = new ArrayList<>(featureGroups); sortedGroups.remove(null); sortedGroups.remove(""); Collections.sort(sortedGroups); sortedGroups.add(null); sortedGroups.add(""); - boolean foundSome = false; + int count = 0; + List visibleGroups = fr.getDisplayedFeatureGroups(); /* - * first output any non-positional features + * loop over all groups (may be visible or not); + * non-positional features are output even if group is not visible */ - if (includeNonPositional) + for (String group : sortedGroups) { + boolean firstInGroup = true; + boolean isNullGroup = group == null || "".equals(group); + for (int i = 0; i < sequences.length; i++) { String sequenceName = sequences[i].getName(); - for (SequenceFeature feature : sequences[i].getFeatures() - .getNonPositionalFeatures()) + List features = new ArrayList<>(); + + /* + * get any non-positional features in this group, if wanted + * (for any feature type, whether visible or not) + */ + if (includeNonPositional) { - foundSome = true; - out.append(formatJalviewFeature(sequenceName, feature)); + features.addAll(sequences[i].getFeatures() + .getFeaturesForGroup(false, group)); } - } - } - for (String group : sortedGroups) - { - boolean isNamedGroup = (group != null && !"".equals(group)); - if (isNamedGroup) - { - out.append(newline); - out.append("STARTGROUP").append(TAB); - out.append(group); - out.append(newline); - } - - /* - * output positional features within groups - */ - for (int i = 0; i < sequences.length; i++) - { - String sequenceName = sequences[i].getName(); - List features = new ArrayList(); - if (types.length > 0) + /* + * add positional features for visible feature types, but + * (for named groups) only if feature group is visible + */ + if (featureTypes.length > 0 + && (isNullGroup || visibleGroups.contains(group))) { features.addAll(sequences[i].getFeatures().getFeaturesForGroup( - true, group, types)); + true, group, featureTypes)); } - for (SequenceFeature sequenceFeature : features) + for (SequenceFeature sf : features) { - foundSome = true; - out.append(formatJalviewFeature(sequenceName, sequenceFeature)); + if (sf.isNonPositional() || fr.isVisible(sf)) + { + count++; + if (firstInGroup) + { + out.append(newline); + if (!isNullGroup) + { + out.append(STARTGROUP).append(TAB).append(group) + .append(newline); + } + } + firstInGroup = false; + out.append(formatJalviewFeature(sequenceName, sf)); + } } } - if (isNamedGroup) + if (!isNullGroup && !firstInGroup) { - out.append("ENDGROUP").append(TAB); - out.append(group); - out.append(newline); + out.append(ENDGROUP).append(TAB).append(group).append(newline); } } - - return foundSome ? out.toString() : "No Features Visible"; + return count; } /** @@ -673,14 +829,15 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI AlignViewportI av = getViewport(); if (av != null) { - if (av.getAlignment() != null) + AlignmentI a = av.getAlignment(); + if (a != null) { - dataset = av.getAlignment().getDataset(); + dataset = a.getDataset(); } if (dataset == null) { // working in the applet context ? - dataset = av.getAlignment(); + dataset = a; } } else @@ -688,7 +845,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI dataset = new Alignment(new SequenceI[] {}); } - Map featureColours = new HashMap(); + Map featureColours = new HashMap<>(); boolean parseResult = parse(dataset, featureColours, false, true); if (!parseResult) { @@ -728,46 +885,49 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @return */ public String printGffFormat(SequenceI[] sequences, - Map visible, - List visibleFeatureGroups, - boolean includeNonPositionalFeatures) + FeatureRenderer fr, boolean includeNonPositionalFeatures) { + Map visibleColours = fr.getDisplayedFeatureCols(); + StringBuilder out = new StringBuilder(256); - out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); + out.append(String.format("%s %d" + newline, GFF_VERSION, + gffVersion == 0 ? 2 : gffVersion)); if (!includeNonPositionalFeatures - && (visible == null || visible.isEmpty())) + && (visibleColours == null || visibleColours.isEmpty())) { return out.toString(); } - String[] types = visible == null ? new String[0] : visible.keySet() - .toArray( - new String[visible.keySet().size()]); + String[] types = visibleColours == null ? new String[0] + : visibleColours.keySet() + .toArray(new String[visibleColours.keySet().size()]); for (SequenceI seq : sequences) { - List features = new ArrayList(); + List features = new ArrayList<>(); if (includeNonPositionalFeatures) { features.addAll(seq.getFeatures().getNonPositionalFeatures()); } - if (visible != null && !visible.isEmpty()) + if (visibleColours != null && !visibleColours.isEmpty()) { features.addAll(seq.getFeatures().getPositionalFeatures(types)); } for (SequenceFeature sf : features) { - String source = sf.featureGroup; - if (!sf.isNonPositional() && source != null - && !visibleFeatureGroups.contains(source)) + if (!sf.isNonPositional() && !fr.isVisible(sf)) { - // group is not visible + /* + * feature hidden by group visibility, colour threshold, + * or feature filter condition + */ continue; } + String source = sf.featureGroup; if (source == null) { source = sf.getDescription();