X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=src%2Fjalview%2Fio%2FFeaturesFile.java;h=5f680998a366361e2e805406e14182fcd28f3137;hb=8946f41687f4c822ac8d15ee8551f23f156735c4;hp=2dd5f26275e0ed0d12a4c6b706ae9b2281181066;hpb=0b8abe58b934e03c34575b06d532a99f0ba70196;p=jalview.git diff --git a/src/jalview/io/FeaturesFile.java b/src/jalview/io/FeaturesFile.java index 2dd5f26..5f68099 100755 --- a/src/jalview/io/FeaturesFile.java +++ b/src/jalview/io/FeaturesFile.java @@ -23,6 +23,8 @@ package jalview.io; 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; @@ -30,13 +32,13 @@ 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; -import jalview.schemes.AnnotationColourGradient; -import jalview.schemes.GraduatedColor; -import jalview.schemes.UserColourScheme; -import jalview.util.Format; +import jalview.schemes.FeatureColour; +import jalview.util.ColorUtils; import jalview.util.MapList; import jalview.util.ParseHtmlBodyAndLinks; import jalview.util.StringUtils; @@ -45,12 +47,11 @@ import java.awt.Color; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.StringTokenizer; /** * Parses and writes features files, which may be in Jalview, GFF2 or GFF3 @@ -70,12 +71,20 @@ import java.util.StringTokenizer; */ 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; @@ -96,13 +105,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI /** * Constructor which does not parse the file immediately * - * @param inFile - * @param type + * @param file File or String filename + * @param paste * @throws IOException */ - public FeaturesFile(String inFile, String type) throws IOException + public FeaturesFile(Object file, DataSourceType paste) + throws IOException { - super(false, inFile, type); + super(false, file, paste); } /** @@ -118,14 +128,14 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * Constructor that optionally parses the file immediately * * @param parseImmediately - * @param inFile + * @param file * @param type * @throws IOException */ - public FeaturesFile(boolean parseImmediately, String inFile, String type) - throws IOException + public FeaturesFile(boolean parseImmediately, Object file, + DataSourceType type) throws IOException { - super(parseImmediately, inFile, type); + super(parseImmediately, file, type); } /** @@ -140,8 +150,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * - process html strings into plain text * @return true if features were added */ - public boolean parse(AlignmentI align, Map colours, - boolean removeHTML) + public boolean parse(AlignmentI align, + Map colours, boolean removeHTML) { return parse(align, colours, removeHTML, false); } @@ -170,21 +180,45 @@ 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 + * - when true, ID matches to compound sequence IDs are allowed + * @return true if features were added + */ + public boolean parse(AlignmentI align, + Map colours, boolean removeHTML, + boolean relaxedIdmatching) + { + 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, - boolean removeHTML, boolean relaxedIdmatching) + public boolean parse(AlignmentI align, + Map colours, + Map filters, boolean removeHTML, + boolean relaxedIdmatching) { - Map gffProps = new HashMap(); + 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,26 +252,37 @@ 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 theres no way of showing more than 1 group + // but at present there's no way of showing more than 1 group featureGroup = null; } else { - parseFeatureColour(line, ft, gffColumns, colours); + String colscheme = gffColumns[1]; + FeatureColourI colour = FeatureColour + .parseJalviewFeatureColour(colscheme); + if (colour != null) + { + colours.put(ft, colour); + } } continue; } @@ -275,7 +320,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI */ for (SequenceI newseq : newseqs) { - if (newseq.getSequenceFeatures() != null) + if (newseq.getFeatures().hasFeatures()) { align.addSequence(newseq); } @@ -284,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. @@ -297,8 +379,9 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param featureGroup */ protected boolean parseJalviewFeature(String line, String[] gffColumns, - AlignmentI alignment, Map featureColours, - boolean removeHTML, boolean relaxedIdMatching, String featureGroup) + AlignmentI alignment, Map featureColours, + boolean removeHTML, boolean relaxedIdMatching, + String featureGroup) { /* * tokens: description seqid seqIndex start end type [score] @@ -349,23 +432,26 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * Perhaps an old style groups file with no colours - * synthesize a colour from the feature type */ - UserColourScheme ucs = new UserColourScheme(ft); - featureColours.put(ft, ucs.findColour('A')); + Color colour = ColorUtils.createColourFromName(ft); + featureColours.put(ft, new FeatureColour(colour)); } - SequenceFeature sf = new SequenceFeature(ft, desc, "", startPos, - endPos, featureGroup); + SequenceFeature sf = null; if (gffColumns.length > 6) { float score = Float.NaN; try { - score = new Float(gffColumns[6]).floatValue(); - // update colourgradient bounds if allowed to + score = Float.valueOf(gffColumns[6]).floatValue(); } catch (NumberFormatException ex) { - // leave as NaN + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } - sf.setScore(score); + sf = new SequenceFeature(ft, desc, startPos, endPos, score, + featureGroup); + } + else + { + sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup); } parseDescriptionHTML(sf, removeHTML); @@ -381,225 +467,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI } /** - * Process a feature type colour specification - * - * @param line - * the current input line (for error messages only) - * @param featureType - * the first token on the line - * @param gffColumns - * holds tokens on the line - * @param colours - * map to which to add derived colour specification - */ - protected void parseFeatureColour(String line, String featureType, - String[] gffColumns, Map colours) - { - Object colour = null; - String colscheme = gffColumns[1]; - if (colscheme.indexOf("|") > -1 - || colscheme.trim().equalsIgnoreCase("label")) - { - colour = parseGraduatedColourScheme(line, colscheme); - } - else - { - UserColourScheme ucs = new UserColourScheme(colscheme); - colour = ucs.findColour('A'); - } - if (colour != null) - { - colours.put(featureType, colour); - } - } - - /** - * Parse a Jalview graduated colour descriptor - * - * @param line - * @param colourDescriptor - * @return - */ - protected GraduatedColor parseGraduatedColourScheme(String line, - String colourDescriptor) - { - // Parse '|' separated graduated colourscheme fields: - // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] - // can either provide 'label' only, first is optional, next two - // colors are required (but may be - // left blank), next is optional, nxt two min/max are required. - // first is either 'label' - // first/second and third are both hexadecimal or word equivalent - // colour. - // next two are values parsed as floats. - // fifth is either 'above','below', or 'none'. - // sixth is a float value and only required when fifth is either - // 'above' or 'below'. - StringTokenizer gcol = new StringTokenizer(colourDescriptor, "|", true); - // set defaults - float min = Float.MIN_VALUE, max = Float.MAX_VALUE; - boolean labelCol = false; - // Parse spec line - String mincol = gcol.nextToken(); - if (mincol == "|") - { - System.err - .println("Expected either 'label' or a colour specification in the line: " - + line); - return null; - } - String maxcol = null; - if (mincol.toLowerCase().indexOf("label") == 0) - { - labelCol = true; - mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip '|' - mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); - } - String abso = null, minval, maxval; - if (mincol != null) - { - // at least four more tokens - if (mincol.equals("|")) - { - mincol = ""; - } - else - { - gcol.nextToken(); // skip next '|' - } - // continue parsing rest of line - maxcol = gcol.nextToken(); - if (maxcol.equals("|")) - { - maxcol = ""; - } - else - { - gcol.nextToken(); // skip next '|' - } - abso = gcol.nextToken(); - gcol.nextToken(); // skip next '|' - if (abso.toLowerCase().indexOf("abso") != 0) - { - minval = abso; - abso = null; - } - else - { - minval = gcol.nextToken(); - gcol.nextToken(); // skip next '|' - } - maxval = gcol.nextToken(); - if (gcol.hasMoreTokens()) - { - gcol.nextToken(); // skip next '|' - } - try - { - if (minval.length() > 0) - { - min = Float.valueOf(minval); - } - } catch (Exception e) - { - System.err - .println("Couldn't parse the minimum value for graduated colour for type (" - + colourDescriptor - + ") - did you misspell 'auto' for the optional automatic colour switch ?"); - e.printStackTrace(); - } - try - { - if (maxval.length() > 0) - { - max = Float.valueOf(maxval); - } - } catch (Exception e) - { - System.err - .println("Couldn't parse the maximum value for graduated colour for type (" - + colourDescriptor + ")"); - e.printStackTrace(); - } - } - else - { - // add in some dummy min/max colours for the label-only - // colourscheme. - mincol = "FFFFFF"; - maxcol = "000000"; - } - - GraduatedColor colour = null; - try - { - colour = new GraduatedColor( - new UserColourScheme(mincol).findColour('A'), - new UserColourScheme(maxcol).findColour('A'), min, max); - } catch (Exception e) - { - System.err.println("Couldn't parse the graduated colour scheme (" - + colourDescriptor + ")"); - e.printStackTrace(); - } - if (colour != null) - { - colour.setColourByLabel(labelCol); - colour.setAutoScaled(abso == null); - // add in any additional parameters - String ttype = null, tval = null; - if (gcol.hasMoreTokens()) - { - // threshold type and possibly a threshold value - ttype = gcol.nextToken(); - if (ttype.toLowerCase().startsWith("below")) - { - colour.setThreshType(AnnotationColourGradient.BELOW_THRESHOLD); - } - else if (ttype.toLowerCase().startsWith("above")) - { - colour.setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD); - } - else - { - colour.setThreshType(AnnotationColourGradient.NO_THRESHOLD); - if (!ttype.toLowerCase().startsWith("no")) - { - System.err.println("Ignoring unrecognised threshold type : " - + ttype); - } - } - } - if (colour.getThreshType() != AnnotationColourGradient.NO_THRESHOLD) - { - try - { - gcol.nextToken(); - tval = gcol.nextToken(); - colour.setThresh(new Float(tval).floatValue()); - } catch (Exception e) - { - System.err.println("Couldn't parse threshold value as a float: (" - + tval + ")"); - e.printStackTrace(); - } - } - // parse the thresh-is-min token ? - if (gcol.hasMoreTokens()) - { - System.err - .println("Ignoring additional tokens in parameters in graduated colour specification\n"); - while (gcol.hasMoreTokens()) - { - System.err.println("|" + gcol.nextToken()); - } - System.err.println("\n"); - } - } - return colour; - } - - /** * clear any temporary handles used to speed up ID matching */ protected void resetMatcher() @@ -684,260 +551,271 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks( sf.getDescription(), removeHTML, newline); - sf.description = (removeHTML) ? parsed.getNonHtmlContent() - : sf.description; + if (removeHTML) + { + sf.setDescription(parsed.getNonHtmlContent()); + } + for (String link : parsed.getLinks()) { sf.addLink(link); } - } /** - * generate a features file for seqs includes non-pos features by default. + * Returns contents of a Jalview format features file, for visible features, + * as filtered by type and group. Features with a null group are displayed if + * their feature type is visible. Non-positional features may optionally be + * included (with no check on type or group). * * @param sequences - * source of sequence features - * @param visible - * hash of feature types and colours - * @return features file contents + * @param fr + * @param includeNonPositional + * if true, include non-positional features (regardless of group or + * type) + * @return */ public String printJalviewFormat(SequenceI[] sequences, - Map visible) + FeatureRenderer fr, boolean includeNonPositional) { - return printJalviewFormat(sequences, visible, true, true); + Map visibleColours = fr + .getDisplayedFeatureCols(); + Map featureFilters = fr.getFeatureFilters(); + + if (!includeNonPositional + && (visibleColours == null || visibleColours.isEmpty())) + { + // no point continuing. + return "No Features Visible"; + } + + /* + * write out feature colours (if we know them) + */ + // TODO: decide if feature links should also be written here ? + StringBuilder out = new StringBuilder(256); + if (visibleColours != null) + { + for (Entry featureColour : visibleColours + .entrySet()) + { + FeatureColourI colour = featureColour.getValue(); + out.append(colour.toJalviewFormat(featureColour.getKey())).append( + newline); + } + } + + 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"; } /** - * generate a features file for seqs with colours from visible (if any) + * Outputs any feature filters defined for visible feature types, sandwiched by + * STARTFILTERS and ENDFILTERS lines * - * @param sequences - * source of features + * @param out * @param visible - * hash of Colours for each feature type - * @param visOnly - * when true only feature types in 'visible' will be output - * @param nonpos - * indicates if non-positional features should be output (regardless - * of group or type) - * @return features file contents + * @param featureFilters */ - public String printJalviewFormat(SequenceI[] sequences, - Map visible, boolean visOnly, boolean nonpos) + void outputFeatureFilters(StringBuilder out, + Map visible, + Map featureFilters) { - StringBuilder out = new StringBuilder(256); - boolean featuresGen = false; - if (visOnly && !nonpos && (visible == null || visible.size() < 1)) + if (visible == null || featureFilters == null + || featureFilters.isEmpty()) { - // no point continuing. - return "No Features Visible"; + return; } - if (visible != null && visOnly) + boolean first = true; + for (String featureType : visible.keySet()) { - // write feature colours only if we're given them and we are generating - // viewed features - // TODO: decide if feature links should also be written here ? - Iterator en = visible.keySet().iterator(); - String featureType, color; - while (en.hasNext()) + FeatureMatcherSetI filter = featureFilters.get(featureType); + if (filter != null) { - featureType = en.next().toString(); - - if (visible.get(featureType) instanceof GraduatedColor) - { - GraduatedColor gc = (GraduatedColor) visible.get(featureType); - color = (gc.isColourByLabel() ? "label|" : "") - + Format.getHexString(gc.getMinColor()) + "|" - + Format.getHexString(gc.getMaxColor()) - + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|" - + gc.getMax() + "|"; - if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD) - { - if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD) - { - color += "below"; - } - else - { - if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD) - { - System.err.println("WARNING: Unsupported threshold type (" - + gc.getThreshType() + ") : Assuming 'above'"); - } - color += "above"; - } - // add the value - color += "|" + gc.getThresh(); - } - else - { - color += "none"; - } - } - else if (visible.get(featureType) instanceof Color) - { - color = Format.getHexString((Color) visible.get(featureType)); - } - else + if (first) { - // legacy support for integer objects containing colour triplet values - color = Format.getHexString(new Color(Integer.parseInt(visible - .get(featureType).toString()))); + first = false; + out.append(newline).append(STARTFILTERS).append(newline); } - out.append(featureType); - out.append(TAB); - out.append(color); - out.append(newline); + out.append(featureType).append(TAB).append(filter.toStableString()) + .append(newline); } } - // Work out which groups are both present and visible - List groups = new ArrayList(); - int groupIndex = 0; - boolean isnonpos = false; - - SequenceFeature[] features; - for (int i = 0; i < sequences.length; i++) + if (!first) { - features = sequences[i].getSequenceFeatures(); - if (features != null) - { - for (int j = 0; j < features.length; j++) - { - isnonpos = features[j].begin == 0 && features[j].end == 0; - if ((!nonpos && isnonpos) - || (!isnonpos && visOnly && !visible - .containsKey(features[j].type))) - { - continue; - } - - if (features[j].featureGroup != null - && !groups.contains(features[j].featureGroup)) - { - groups.add(features[j].featureGroup); - } - } - } + out.append(ENDFILTERS).append(newline); } - String group = null; - do + } + + /** + * 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<>(featureGroups); + sortedGroups.remove(null); + sortedGroups.remove(""); + Collections.sort(sortedGroups); + sortedGroups.add(null); + sortedGroups.add(""); + + int count = 0; + List visibleGroups = fr.getDisplayedFeatureGroups(); + + /* + * loop over all groups (may be visible or not); + * non-positional features are output even if group is not visible + */ + for (String group : sortedGroups) { - if (groups.size() > 0 && groupIndex < groups.size()) - { - group = groups.get(groupIndex); - out.append(newline); - out.append("STARTGROUP").append(TAB); - out.append(group); - out.append(newline); - } - else - { - group = null; - } + boolean firstInGroup = true; + boolean isNullGroup = group == null || "".equals(group); for (int i = 0; i < sequences.length; i++) { - features = sequences[i].getSequenceFeatures(); - if (features != null) + String sequenceName = sequences[i].getName(); + List features = new ArrayList<>(); + + /* + * get any non-positional features in this group, if wanted + * (for any feature type, whether visible or not) + */ + if (includeNonPositional) { - for (int j = 0; j < features.length; j++) - { - isnonpos = features[j].begin == 0 && features[j].end == 0; - if ((!nonpos && isnonpos) - || (!isnonpos && visOnly && !visible - .containsKey(features[j].type))) - { - // skip if feature is nonpos and we ignore them or if we only - // output visible and it isn't non-pos and it's not visible - continue; - } + features.addAll(sequences[i].getFeatures() + .getFeaturesForGroup(false, group)); + } - if (group != null - && (features[j].featureGroup == null || !features[j].featureGroup - .equals(group))) - { - continue; - } + /* + * 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, featureTypes)); + } - if (group == null && features[j].featureGroup != null) - { - continue; - } - // we have features to output - featuresGen = true; - if (features[j].description == null - || features[j].description.equals("")) - { - out.append(features[j].type).append(TAB); - } - else + for (SequenceFeature sf : features) + { + if (sf.isNonPositional() || fr.isVisible(sf)) + { + count++; + if (firstInGroup) { - if (features[j].links != null - && features[j].getDescription().indexOf("") == -1) - { - out.append(""); - } - - out.append(features[j].description + " "); - if (features[j].links != null) + out.append(newline); + if (!isNullGroup) { - for (int l = 0; l < features[j].links.size(); l++) - { - String label = features[j].links.elementAt(l).toString(); - String href = label.substring(label.indexOf("|") + 1); - label = label.substring(0, label.indexOf("|")); - - if (features[j].description.indexOf(href) == -1) - { - out.append("" + label + ""); - } - } - - if (features[j].getDescription().indexOf("") == -1) - { - out.append(""); - } + out.append(STARTGROUP).append(TAB).append(group) + .append(newline); } - - out.append(TAB); - } - out.append(sequences[i].getName()); - out.append("\t-1\t"); - out.append(features[j].begin); - out.append(TAB); - out.append(features[j].end); - out.append(TAB); - out.append(features[j].type); - if (!Float.isNaN(features[j].score)) - { - out.append(TAB); - out.append(features[j].score); } - out.append(newline); + firstInGroup = false; + out.append(formatJalviewFeature(sequenceName, sf)); } } } - if (group != null) + if (!isNullGroup && !firstInGroup) { - out.append("ENDGROUP").append(TAB); - out.append(group); - out.append(newline); - groupIndex++; + out.append(ENDGROUP).append(TAB).append(group).append(newline); } - else + } + return count; + } + + /** + * @param out + * @param sequenceName + * @param sequenceFeature + */ + protected String formatJalviewFeature( + String sequenceName, SequenceFeature sequenceFeature) + { + StringBuilder out = new StringBuilder(64); + if (sequenceFeature.description == null + || sequenceFeature.description.equals("")) + { + out.append(sequenceFeature.type).append(TAB); + } + else + { + if (sequenceFeature.links != null + && sequenceFeature.getDescription().indexOf("") == -1) { - break; + out.append(""); } - } while (groupIndex < groups.size() + 1); + out.append(sequenceFeature.description); + if (sequenceFeature.links != null) + { + for (int l = 0; l < sequenceFeature.links.size(); l++) + { + String label = sequenceFeature.links.elementAt(l); + String href = label.substring(label.indexOf("|") + 1); + label = label.substring(0, label.indexOf("|")); + + if (sequenceFeature.description.indexOf(href) == -1) + { + out.append(" " + label + ""); + } + } + + if (sequenceFeature.getDescription().indexOf("") == -1) + { + out.append(""); + } + } - if (!featuresGen) + out.append(TAB); + } + out.append(sequenceName); + out.append("\t-1\t"); + out.append(sequenceFeature.begin); + out.append(TAB); + out.append(sequenceFeature.end); + out.append(TAB); + out.append(sequenceFeature.type); + if (!Float.isNaN(sequenceFeature.score)) { - return "No Features Visible"; + out.append(TAB); + out.append(sequenceFeature.score); } + out.append(newline); return out.toString(); } @@ -966,7 +844,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI dataset = new Alignment(new SequenceI[] {}); } - boolean parseResult = parse(dataset, null, false, true); + Map featureColours = new HashMap<>(); + boolean parseResult = parse(dataset, featureColours, false, true); if (!parseResult) { // pass error up somehow @@ -987,25 +866,10 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @return error message */ @Override - public String print() + public String print(SequenceI[] sqs, boolean jvsuffix) { - return "Use printGffFormat() or printJalviewFormat()"; - } - - /** - * Returns features output in GFF2 format, including hidden and non-positional - * features - * - * @param sequences - * the sequences whose features are to be output - * @param visible - * a map whose keys are the type names of visible features - * @return - */ - public String printGffFormat(SequenceI[] sequences, - Map visible) - { - return printGffFormat(sequences, visible, true, true); + System.out.println("Use printGffFormat() or printJalviewFormat()"); + return null; } /** @@ -1015,79 +879,86 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * the sequences whose features are to be output * @param visible * a map whose keys are the type names of visible features - * @param outputVisibleOnly + * @param visibleFeatureGroups * @param includeNonPositionalFeatures * @return */ public String printGffFormat(SequenceI[] sequences, - Map visible, boolean outputVisibleOnly, - 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)); - String source; - boolean isnonpos; + + out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion)); + + if (!includeNonPositionalFeatures + && (visibleColours == null || visibleColours.isEmpty())) + { + return out.toString(); + } + + String[] types = visibleColours == null ? new String[0] + : visibleColours.keySet() + .toArray(new String[visibleColours.keySet().size()]); + for (SequenceI seq : sequences) { - SequenceFeature[] features = seq.getSequenceFeatures(); - if (features != null) + List features = new ArrayList<>(); + if (includeNonPositionalFeatures) { - for (SequenceFeature sf : features) + features.addAll(seq.getFeatures().getNonPositionalFeatures()); + } + if (visibleColours != null && !visibleColours.isEmpty()) + { + features.addAll(seq.getFeatures().getPositionalFeatures(types)); + } + + for (SequenceFeature sf : features) + { + if (!sf.isNonPositional() && !fr.isVisible(sf)) { - isnonpos = sf.begin == 0 && sf.end == 0; - if (!includeNonPositionalFeatures && isnonpos) - { - /* - * ignore non-positional features if not wanted - */ - continue; - } - // TODO why the test !isnonpos here? - // what about not visible non-positional features? - if (!isnonpos && outputVisibleOnly - && !visible.containsKey(sf.type)) - { - /* - * ignore not visible features if not wanted - */ - continue; - } + /* + * feature hidden by group visibility, colour threshold, + * or feature filter condition + */ + continue; + } - source = sf.featureGroup; - if (source == null) - { - source = sf.getDescription(); - } + String source = sf.featureGroup; + if (source == null) + { + source = sf.getDescription(); + } - out.append(seq.getName()); - out.append(TAB); - out.append(source); - out.append(TAB); - out.append(sf.type); - out.append(TAB); - out.append(sf.begin); - out.append(TAB); - out.append(sf.end); - out.append(TAB); - out.append(sf.score); - out.append(TAB); - - int strand = sf.getStrand(); - out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); - out.append(TAB); - - String phase = sf.getPhase(); - out.append(phase == null ? "." : phase); - - // miscellaneous key-values (GFF column 9) - String attributes = sf.getAttributes(); - if (attributes != null) - { - out.append(TAB).append(attributes); - } + out.append(seq.getName()); + out.append(TAB); + out.append(source); + out.append(TAB); + out.append(sf.type); + out.append(TAB); + out.append(sf.begin); + out.append(TAB); + out.append(sf.end); + out.append(TAB); + out.append(sf.score); + out.append(TAB); + + int strand = sf.getStrand(); + out.append(strand == 1 ? "+" : (strand == -1 ? "-" : ".")); + out.append(TAB); + + String phase = sf.getPhase(); + out.append(phase == null ? "." : phase); - out.append(newline); + // miscellaneous key-values (GFF column 9) + String attributes = sf.getAttributes(); + if (attributes != null) + { + out.append(TAB).append(attributes); } + + out.append(newline); } } @@ -1149,8 +1020,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI fromCount = Integer.parseInt(tokens[2]); } catch (NumberFormatException nfe) { - throw new IOException("Invalid number in Align field: " - + nfe.getMessage()); + throw new IOException( + "Invalid number in Align field: " + nfe.getMessage()); } /* @@ -1348,10 +1219,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI // rename sequences if GFF handler requested this // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ? - SequenceFeature[] sfs = seq.getSequenceFeatures(); - if (sfs != null) + List sfs = seq.getFeatures().getPositionalFeatures(); + if (!sfs.isEmpty()) { - String newName = (String) sfs[0].getValue(GffHelperI.RENAME_TOKEN); + String newName = (String) sfs.get(0).getValue( + GffHelperI.RENAME_TOKEN); if (newName != null) { seq.setName(newName); @@ -1370,9 +1242,8 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI * @param newseqs * @throws IOException */ - protected void processGffPragma(String line, - Map gffProps, AlignmentI align, - List newseqs) throws IOException + protected void processGffPragma(String line, Map gffProps, + AlignmentI align, List newseqs) throws IOException { line = line.trim(); if ("###".equals(line))