JAL-2791 refactored Export Features to apply all visibility tests
[jalview.git] / src / jalview / io / FeaturesFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.analysis.AlignmentUtils;
24 import jalview.analysis.SequenceIdMatcher;
25 import jalview.api.AlignViewportI;
26 import jalview.api.FeatureColourI;
27 import jalview.api.FeatureRenderer;
28 import jalview.api.FeaturesSourceI;
29 import jalview.datamodel.AlignedCodonFrame;
30 import jalview.datamodel.Alignment;
31 import jalview.datamodel.AlignmentI;
32 import jalview.datamodel.SequenceDummy;
33 import jalview.datamodel.SequenceFeature;
34 import jalview.datamodel.SequenceI;
35 import jalview.datamodel.features.FeatureMatcherSet;
36 import jalview.datamodel.features.FeatureMatcherSetI;
37 import jalview.io.gff.GffHelperBase;
38 import jalview.io.gff.GffHelperFactory;
39 import jalview.io.gff.GffHelperI;
40 import jalview.schemes.FeatureColour;
41 import jalview.util.ColorUtils;
42 import jalview.util.MapList;
43 import jalview.util.ParseHtmlBodyAndLinks;
44 import jalview.util.StringUtils;
45
46 import java.awt.Color;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Map.Entry;
55
56 /**
57  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
58  * format. These are tab-delimited formats but with differences in the use of
59  * columns.
60  * 
61  * A Jalview feature file may define feature colours and then declare that the
62  * remainder of the file is in GFF format with the line 'GFF'.
63  * 
64  * GFF3 files may include alignment mappings for features, which Jalview will
65  * attempt to model, and may include sequence data following a ##FASTA line.
66  * 
67  * 
68  * @author AMW
69  * @author jbprocter
70  * @author gmcarstairs
71  */
72 public class FeaturesFile extends AlignFile implements FeaturesSourceI
73 {
74   private static final String TAB_REGEX = "\\t";
75
76   private static final String STARTGROUP = "STARTGROUP";
77
78   private static final String ENDGROUP = "ENDGROUP";
79
80   private static final String STARTFILTERS = "STARTFILTERS";
81
82   private static final String ENDFILTERS = "ENDFILTERS";
83
84   private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
85
86   private static final String NOTE = "Note";
87
88   protected static final String GFF_VERSION = "##gff-version";
89
90   private AlignmentI lastmatchedAl = null;
91
92   private SequenceIdMatcher matcher = null;
93
94   protected AlignmentI dataset;
95
96   protected int gffVersion;
97
98   /**
99    * Creates a new FeaturesFile object.
100    */
101   public FeaturesFile()
102   {
103   }
104
105   /**
106    * Constructor which does not parse the file immediately
107    * 
108    * @param file
109    * @param paste
110    * @throws IOException
111    */
112   public FeaturesFile(String file, DataSourceType paste)
113           throws IOException
114   {
115     super(false, file, paste);
116   }
117
118   /**
119    * @param source
120    * @throws IOException
121    */
122   public FeaturesFile(FileParse source) throws IOException
123   {
124     super(source);
125   }
126
127   /**
128    * Constructor that optionally parses the file immediately
129    * 
130    * @param parseImmediately
131    * @param file
132    * @param type
133    * @throws IOException
134    */
135   public FeaturesFile(boolean parseImmediately, String file,
136           DataSourceType type) throws IOException
137   {
138     super(parseImmediately, file, type);
139   }
140
141   /**
142    * Parse GFF or sequence features file using case-independent matching,
143    * discarding URLs
144    * 
145    * @param align
146    *          - alignment/dataset containing sequences that are to be annotated
147    * @param colours
148    *          - hashtable to store feature colour definitions
149    * @param removeHTML
150    *          - process html strings into plain text
151    * @return true if features were added
152    */
153   public boolean parse(AlignmentI align,
154           Map<String, FeatureColourI> colours, boolean removeHTML)
155   {
156     return parse(align, colours, removeHTML, false);
157   }
158
159   /**
160    * Extends the default addProperties by also adding peptide-to-cDNA mappings
161    * (if any) derived while parsing a GFF file
162    */
163   @Override
164   public void addProperties(AlignmentI al)
165   {
166     super.addProperties(al);
167     if (dataset != null && dataset.getCodonFrames() != null)
168     {
169       AlignmentI ds = (al.getDataset() == null) ? al : al.getDataset();
170       for (AlignedCodonFrame codons : dataset.getCodonFrames())
171       {
172         ds.addCodonFrame(codons);
173       }
174     }
175   }
176
177   /**
178    * Parse GFF or Jalview format sequence features file
179    * 
180    * @param align
181    *          - alignment/dataset containing sequences that are to be annotated
182    * @param colours
183    *          - map to store feature colour definitions
184    * @param removeHTML
185    *          - process html strings into plain text
186    * @param relaxedIdmatching
187    *          - when true, ID matches to compound sequence IDs are allowed
188    * @return true if features were added
189    */
190   public boolean parse(AlignmentI align,
191           Map<String, FeatureColourI> colours, boolean removeHTML,
192           boolean relaxedIdmatching)
193   {
194     return parse(align, colours, null, removeHTML, relaxedIdmatching);
195   }
196
197   /**
198    * Parse GFF or Jalview format sequence features file
199    * 
200    * @param align
201    *          - alignment/dataset containing sequences that are to be annotated
202    * @param colours
203    *          - map to store feature colour definitions
204    * @param filters
205    *          - map to store feature filter definitions
206    * @param removeHTML
207    *          - process html strings into plain text
208    * @param relaxedIdmatching
209    *          - when true, ID matches to compound sequence IDs are allowed
210    * @return true if features were added
211    */
212   public boolean parse(AlignmentI align,
213           Map<String, FeatureColourI> colours,
214           Map<String, FeatureMatcherSetI> filters, boolean removeHTML,
215           boolean relaxedIdmatching)
216   {
217     Map<String, String> gffProps = new HashMap<>();
218     /*
219      * keep track of any sequences we try to create from the data
220      */
221     List<SequenceI> newseqs = new ArrayList<>();
222
223     String line = null;
224     try
225     {
226       String[] gffColumns;
227       String featureGroup = null;
228
229       while ((line = nextLine()) != null)
230       {
231         // skip comments/process pragmas
232         if (line.length() == 0 || line.startsWith("#"))
233         {
234           if (line.toLowerCase().startsWith("##"))
235           {
236             processGffPragma(line, gffProps, align, newseqs);
237           }
238           continue;
239         }
240
241         gffColumns = line.split(TAB_REGEX);
242         if (gffColumns.length == 1)
243         {
244           if (line.trim().equalsIgnoreCase("GFF"))
245           {
246             /*
247              * Jalview features file with appended GFF
248              * assume GFF2 (though it may declare ##gff-version 3)
249              */
250             gffVersion = 2;
251             continue;
252           }
253         }
254
255         if (gffColumns.length > 0 && gffColumns.length < 4)
256         {
257           /*
258            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
259            * a feature type colour specification
260            */
261           String ft = gffColumns[0];
262           if (ft.equalsIgnoreCase(STARTFILTERS))
263           {
264             parseFilters(filters);
265             continue;
266           }
267           if (ft.equalsIgnoreCase(STARTGROUP))
268           {
269             featureGroup = gffColumns[1];
270           }
271           else if (ft.equalsIgnoreCase(ENDGROUP))
272           {
273             // We should check whether this is the current group,
274             // but at present there's no way of showing more than 1 group
275             featureGroup = null;
276           }
277           else
278           {
279             String colscheme = gffColumns[1];
280             FeatureColourI colour = FeatureColour
281                     .parseJalviewFeatureColour(colscheme);
282             if (colour != null)
283             {
284               colours.put(ft, colour);
285             }
286           }
287           continue;
288         }
289
290         /*
291          * if not a comment, GFF pragma, startgroup, endgroup or feature
292          * colour specification, that just leaves a feature details line
293          * in either Jalview or GFF format
294          */
295         if (gffVersion == 0)
296         {
297           parseJalviewFeature(line, gffColumns, align, colours, removeHTML,
298                   relaxedIdmatching, featureGroup);
299         }
300         else
301         {
302           parseGff(gffColumns, align, relaxedIdmatching, newseqs);
303         }
304       }
305       resetMatcher();
306     } catch (Exception ex)
307     {
308       // should report somewhere useful for UI if necessary
309       warningMessage = ((warningMessage == null) ? "" : warningMessage)
310               + "Parsing error at\n" + line;
311       System.out.println("Error parsing feature file: " + ex + "\n" + line);
312       ex.printStackTrace(System.err);
313       resetMatcher();
314       return false;
315     }
316
317     /*
318      * experimental - add any dummy sequences with features to the alignment
319      * - we need them for Ensembl feature extraction - though maybe not otherwise
320      */
321     for (SequenceI newseq : newseqs)
322     {
323       if (newseq.getFeatures().hasFeatures())
324       {
325         align.addSequence(newseq);
326       }
327     }
328     return true;
329   }
330
331   /**
332    * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type
333    * filter to the map for each line parsed. After exit from this method,
334    * nextLine() should return the line after ENDFILTERS (or we are already at
335    * end of file if ENDFILTERS was missing).
336    * 
337    * @param filters
338    * @throws IOException
339    */
340   protected void parseFilters(Map<String, FeatureMatcherSetI> filters)
341           throws IOException
342   {
343     String line;
344     while ((line = nextLine()) != null)
345     {
346       if (line.toUpperCase().startsWith(ENDFILTERS))
347       {
348         return;
349       }
350       String[] tokens = line.split(TAB_REGEX);
351       if (tokens.length != 2)
352       {
353         System.err.println(String.format("Invalid token count %d for %d",
354                 tokens.length, line));
355       }
356       else
357       {
358         String featureType = tokens[0];
359         FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]);
360         if (fm != null && filters != null)
361         {
362           filters.put(featureType, fm);
363         }
364       }
365     }
366   }
367
368   /**
369    * Try to parse a Jalview format feature specification and add it as a
370    * sequence feature to any matching sequences in the alignment. Returns true
371    * if successful (a feature was added), or false if not.
372    * 
373    * @param line
374    * @param gffColumns
375    * @param alignment
376    * @param featureColours
377    * @param removeHTML
378    * @param relaxedIdmatching
379    * @param featureGroup
380    */
381   protected boolean parseJalviewFeature(String line, String[] gffColumns,
382           AlignmentI alignment, Map<String, FeatureColourI> featureColours,
383           boolean removeHTML, boolean relaxedIdMatching,
384           String featureGroup)
385   {
386     /*
387      * tokens: description seqid seqIndex start end type [score]
388      */
389     if (gffColumns.length < 6)
390     {
391       System.err.println("Ignoring feature line '" + line
392               + "' with too few columns (" + gffColumns.length + ")");
393       return false;
394     }
395     String desc = gffColumns[0];
396     String seqId = gffColumns[1];
397     SequenceI seq = findSequence(seqId, alignment, null, relaxedIdMatching);
398
399     if (!ID_NOT_SPECIFIED.equals(seqId))
400     {
401       seq = findSequence(seqId, alignment, null, relaxedIdMatching);
402     }
403     else
404     {
405       seqId = null;
406       seq = null;
407       String seqIndex = gffColumns[2];
408       try
409       {
410         int idx = Integer.parseInt(seqIndex);
411         seq = alignment.getSequenceAt(idx);
412       } catch (NumberFormatException ex)
413       {
414         System.err.println("Invalid sequence index: " + seqIndex);
415       }
416     }
417
418     if (seq == null)
419     {
420       System.out.println("Sequence not found: " + line);
421       return false;
422     }
423
424     int startPos = Integer.parseInt(gffColumns[3]);
425     int endPos = Integer.parseInt(gffColumns[4]);
426
427     String ft = gffColumns[5];
428
429     if (!featureColours.containsKey(ft))
430     {
431       /* 
432        * Perhaps an old style groups file with no colours -
433        * synthesize a colour from the feature type
434        */
435       Color colour = ColorUtils.createColourFromName(ft);
436       featureColours.put(ft, new FeatureColour(colour));
437     }
438     SequenceFeature sf = null;
439     if (gffColumns.length > 6)
440     {
441       float score = Float.NaN;
442       try
443       {
444         score = new Float(gffColumns[6]).floatValue();
445       } catch (NumberFormatException ex)
446       {
447         sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
448       }
449       sf = new SequenceFeature(ft, desc, startPos, endPos, score,
450               featureGroup);
451     }
452     else
453     {
454       sf = new SequenceFeature(ft, desc, startPos, endPos, featureGroup);
455     }
456
457     parseDescriptionHTML(sf, removeHTML);
458
459     seq.addSequenceFeature(sf);
460
461     while (seqId != null
462             && (seq = alignment.findName(seq, seqId, false)) != null)
463     {
464       seq.addSequenceFeature(new SequenceFeature(sf));
465     }
466     return true;
467   }
468
469   /**
470    * clear any temporary handles used to speed up ID matching
471    */
472   protected void resetMatcher()
473   {
474     lastmatchedAl = null;
475     matcher = null;
476   }
477
478   /**
479    * Returns a sequence matching the given id, as follows
480    * <ul>
481    * <li>strict matching is on exact sequence name</li>
482    * <li>relaxed matching allows matching on a token within the sequence name,
483    * or a dbxref</li>
484    * <li>first tries to find a match in the alignment sequences</li>
485    * <li>else tries to find a match in the new sequences already generated while
486    * parsing the features file</li>
487    * <li>else creates a new placeholder sequence, adds it to the new sequences
488    * list, and returns it</li>
489    * </ul>
490    * 
491    * @param seqId
492    * @param align
493    * @param newseqs
494    * @param relaxedIdMatching
495    * 
496    * @return
497    */
498   protected SequenceI findSequence(String seqId, AlignmentI align,
499           List<SequenceI> newseqs, boolean relaxedIdMatching)
500   {
501     // TODO encapsulate in SequenceIdMatcher, share the matcher
502     // with the GffHelper (removing code duplication)
503     SequenceI match = null;
504     if (relaxedIdMatching)
505     {
506       if (lastmatchedAl != align)
507       {
508         lastmatchedAl = align;
509         matcher = new SequenceIdMatcher(align.getSequencesArray());
510         if (newseqs != null)
511         {
512           matcher.addAll(newseqs);
513         }
514       }
515       match = matcher.findIdMatch(seqId);
516     }
517     else
518     {
519       match = align.findName(seqId, true);
520       if (match == null && newseqs != null)
521       {
522         for (SequenceI m : newseqs)
523         {
524           if (seqId.equals(m.getName()))
525           {
526             return m;
527           }
528         }
529       }
530
531     }
532     if (match == null && newseqs != null)
533     {
534       match = new SequenceDummy(seqId);
535       if (relaxedIdMatching)
536       {
537         matcher.addAll(Arrays.asList(new SequenceI[] { match }));
538       }
539       // add dummy sequence to the newseqs list
540       newseqs.add(match);
541     }
542     return match;
543   }
544
545   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
546   {
547     if (sf.getDescription() == null)
548     {
549       return;
550     }
551     ParseHtmlBodyAndLinks parsed = new ParseHtmlBodyAndLinks(
552             sf.getDescription(), removeHTML, newline);
553
554     if (removeHTML)
555     {
556       sf.setDescription(parsed.getNonHtmlContent());
557     }
558
559     for (String link : parsed.getLinks())
560     {
561       sf.addLink(link);
562     }
563   }
564
565   /**
566    * Returns contents of a Jalview format features file, for visible features,
567    * as filtered by type and group. Features with a null group are displayed if
568    * their feature type is visible. Non-positional features may optionally be
569    * included (with no check on type or group).
570    * 
571    * @param sequences
572    * @param fr
573    * @param includeNonPositional
574    *          if true, include non-positional features (regardless of group or
575    *          type)
576    * @return
577    */
578   public String printJalviewFormat(SequenceI[] sequences,
579           FeatureRenderer fr, boolean includeNonPositional)
580   {
581     Map<String, FeatureColourI> visibleColours = fr
582             .getDisplayedFeatureCols();
583     Map<String, FeatureMatcherSetI> featureFilters = fr.getFeatureFilters();
584     List<String> visibleFeatureGroups = fr.getDisplayedFeatureGroups();
585
586     if (!includeNonPositional
587             && (visibleColours == null || visibleColours.isEmpty()))
588     {
589       // no point continuing.
590       return "No Features Visible";
591     }
592
593     /*
594      * write out feature colours (if we know them)
595      */
596     // TODO: decide if feature links should also be written here ?
597     StringBuilder out = new StringBuilder(256);
598     if (visibleColours != null)
599     {
600       for (Entry<String, FeatureColourI> featureColour : visibleColours
601               .entrySet())
602       {
603         FeatureColourI colour = featureColour.getValue();
604         out.append(colour.toJalviewFormat(featureColour.getKey())).append(
605                 newline);
606       }
607     }
608
609     String[] types = visibleColours == null ? new String[0]
610             : visibleColours.keySet()
611                     .toArray(new String[visibleColours.keySet().size()]);
612
613     /*
614      * feature filters if any
615      */
616     outputFeatureFilters(out, visibleColours, featureFilters);
617
618     /*
619      * sort groups alphabetically, and ensure that features with a
620      * null or empty group are output after those in named groups
621      */
622     List<String> sortedGroups = new ArrayList<>(visibleFeatureGroups);
623     sortedGroups.remove(null);
624     sortedGroups.remove("");
625     Collections.sort(sortedGroups);
626     sortedGroups.add(null);
627     sortedGroups.add("");
628
629     boolean foundSome = false;
630
631     /*
632      * first output any non-positional features
633      */
634     if (includeNonPositional)
635     {
636       for (int i = 0; i < sequences.length; i++)
637       {
638         String sequenceName = sequences[i].getName();
639         for (SequenceFeature feature : sequences[i].getFeatures()
640                 .getNonPositionalFeatures())
641         {
642           foundSome = true;
643           out.append(formatJalviewFeature(sequenceName, feature));
644         }
645       }
646     }
647
648     /*
649      * positional features within groups
650      */
651     foundSome |= outputFeaturesByGroup(out, fr, sortedGroups, types,
652             sequences);
653
654     return foundSome ? out.toString() : "No Features Visible";
655   }
656
657   /**
658    * Outputs any feature filters defined for visible feature types, sandwiched by
659    * STARTFILTERS and ENDFILTERS lines
660    * 
661    * @param out
662    * @param visible
663    * @param featureFilters
664    */
665   void outputFeatureFilters(StringBuilder out,
666           Map<String, FeatureColourI> visible,
667           Map<String, FeatureMatcherSetI> featureFilters)
668   {
669     if (visible == null || featureFilters == null
670             || featureFilters.isEmpty())
671     {
672       return;
673     }
674
675     boolean first = true;
676     for (String featureType : visible.keySet())
677     {
678       FeatureMatcherSetI filter = featureFilters.get(featureType);
679       if (filter != null)
680       {
681         if (first)
682         {
683           first = false;
684           out.append(newline).append(STARTFILTERS).append(newline);
685         }
686         out.append(featureType).append(TAB).append(filter.toStableString())
687                 .append(newline);
688       }
689     }
690     if (!first)
691     {
692       out.append(ENDFILTERS).append(newline);
693     }
694
695   }
696
697   /**
698    * Appends output of visible sequence features within feature groups to the
699    * output buffer. Groups other than the null or empty group are sandwiched by
700    * STARTGROUP and ENDGROUP lines. Answers true if at least one feature was
701    * written, else false.
702    * 
703    * @param out
704    * @param fr
705    * @param groups
706    * @param featureTypes
707    * @param sequences
708    * @return
709    */
710   private boolean outputFeaturesByGroup(StringBuilder out,
711           FeatureRenderer fr, List<String> groups, String[] featureTypes,
712           SequenceI[] sequences)
713   {
714     boolean foundSome = false;
715     for (String group : groups)
716     {
717       boolean firstInGroup = true;
718       boolean isNamedGroup = (group != null && !"".equals(group));
719
720       /*
721        * output positional features within groups
722        */
723       for (int i = 0; i < sequences.length; i++)
724       {
725         String sequenceName = sequences[i].getName();
726         List<SequenceFeature> features = new ArrayList<>();
727         if (featureTypes.length > 0)
728         {
729           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
730                   true, group, featureTypes));
731         }
732
733         for (SequenceFeature sequenceFeature : features)
734         {
735           if (fr.isVisible(sequenceFeature))
736           {
737             foundSome = true;
738             if (firstInGroup && isNamedGroup)
739             {
740               out.append(newline).append(STARTGROUP).append(TAB)
741                       .append(group).append(newline);
742             }
743             firstInGroup = false;
744             out.append(formatJalviewFeature(sequenceName, sequenceFeature));
745           }
746         }
747       }
748
749       if (isNamedGroup && !firstInGroup)
750       {
751         out.append(ENDGROUP).append(TAB).append(group).append(newline);
752       }
753     }
754     return foundSome;
755   }
756
757   /**
758    * @param out
759    * @param sequenceName
760    * @param sequenceFeature
761    */
762   protected String formatJalviewFeature(
763           String sequenceName, SequenceFeature sequenceFeature)
764   {
765     StringBuilder out = new StringBuilder(64);
766     if (sequenceFeature.description == null
767             || sequenceFeature.description.equals(""))
768     {
769       out.append(sequenceFeature.type).append(TAB);
770     }
771     else
772     {
773       if (sequenceFeature.links != null
774               && sequenceFeature.getDescription().indexOf("<html>") == -1)
775       {
776         out.append("<html>");
777       }
778
779       out.append(sequenceFeature.description);
780       if (sequenceFeature.links != null)
781       {
782         for (int l = 0; l < sequenceFeature.links.size(); l++)
783         {
784           String label = sequenceFeature.links.elementAt(l);
785           String href = label.substring(label.indexOf("|") + 1);
786           label = label.substring(0, label.indexOf("|"));
787
788           if (sequenceFeature.description.indexOf(href) == -1)
789           {
790             out.append(" <a href=\"" + href + "\">" + label + "</a>");
791           }
792         }
793
794         if (sequenceFeature.getDescription().indexOf("</html>") == -1)
795         {
796           out.append("</html>");
797         }
798       }
799
800       out.append(TAB);
801     }
802     out.append(sequenceName);
803     out.append("\t-1\t");
804     out.append(sequenceFeature.begin);
805     out.append(TAB);
806     out.append(sequenceFeature.end);
807     out.append(TAB);
808     out.append(sequenceFeature.type);
809     if (!Float.isNaN(sequenceFeature.score))
810     {
811       out.append(TAB);
812       out.append(sequenceFeature.score);
813     }
814     out.append(newline);
815
816     return out.toString();
817   }
818
819   /**
820    * Parse method that is called when a GFF file is dragged to the desktop
821    */
822   @Override
823   public void parse()
824   {
825     AlignViewportI av = getViewport();
826     if (av != null)
827     {
828       if (av.getAlignment() != null)
829       {
830         dataset = av.getAlignment().getDataset();
831       }
832       if (dataset == null)
833       {
834         // working in the applet context ?
835         dataset = av.getAlignment();
836       }
837     }
838     else
839     {
840       dataset = new Alignment(new SequenceI[] {});
841     }
842
843     Map<String, FeatureColourI> featureColours = new HashMap<>();
844     boolean parseResult = parse(dataset, featureColours, false, true);
845     if (!parseResult)
846     {
847       // pass error up somehow
848     }
849     if (av != null)
850     {
851       // update viewport with the dataset data ?
852     }
853     else
854     {
855       setSeqs(dataset.getSequencesArray());
856     }
857   }
858
859   /**
860    * Implementation of unused abstract method
861    * 
862    * @return error message
863    */
864   @Override
865   public String print(SequenceI[] sqs, boolean jvsuffix)
866   {
867     System.out.println("Use printGffFormat() or printJalviewFormat()");
868     return null;
869   }
870
871   /**
872    * Returns features output in GFF2 format
873    * 
874    * @param sequences
875    *          the sequences whose features are to be output
876    * @param visible
877    *          a map whose keys are the type names of visible features
878    * @param visibleFeatureGroups
879    * @param includeNonPositionalFeatures
880    * @return
881    */
882   public String printGffFormat(SequenceI[] sequences,
883           FeatureRenderer fr, boolean includeNonPositionalFeatures)
884   {
885     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
886
887     StringBuilder out = new StringBuilder(256);
888
889     out.append(String.format("%s %d\n", GFF_VERSION, gffVersion == 0 ? 2 : gffVersion));
890
891     if (!includeNonPositionalFeatures
892             && (visibleColours == null || visibleColours.isEmpty()))
893     {
894       return out.toString();
895     }
896
897     String[] types = visibleColours == null ? new String[0]
898             : visibleColours.keySet()
899                     .toArray(new String[visibleColours.keySet().size()]);
900
901     for (SequenceI seq : sequences)
902     {
903       List<SequenceFeature> features = new ArrayList<>();
904       if (includeNonPositionalFeatures)
905       {
906         features.addAll(seq.getFeatures().getNonPositionalFeatures());
907       }
908       if (visibleColours != null && !visibleColours.isEmpty())
909       {
910         features.addAll(seq.getFeatures().getPositionalFeatures(types));
911       }
912
913       for (SequenceFeature sf : features)
914       {
915         if (!sf.isNonPositional() && !fr.isVisible(sf))
916         {
917           /*
918            * feature hidden by group visibility, colour threshold,
919            * or feature filter condition
920            */
921           continue;
922         }
923
924         String source = sf.featureGroup;
925         if (source == null)
926         {
927           source = sf.getDescription();
928         }
929
930         out.append(seq.getName());
931         out.append(TAB);
932         out.append(source);
933         out.append(TAB);
934         out.append(sf.type);
935         out.append(TAB);
936         out.append(sf.begin);
937         out.append(TAB);
938         out.append(sf.end);
939         out.append(TAB);
940         out.append(sf.score);
941         out.append(TAB);
942
943         int strand = sf.getStrand();
944         out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
945         out.append(TAB);
946
947         String phase = sf.getPhase();
948         out.append(phase == null ? "." : phase);
949
950         // miscellaneous key-values (GFF column 9)
951         String attributes = sf.getAttributes();
952         if (attributes != null)
953         {
954           out.append(TAB).append(attributes);
955         }
956
957         out.append(newline);
958       }
959     }
960
961     return out.toString();
962   }
963
964   /**
965    * Returns a mapping given list of one or more Align descriptors (exonerate
966    * format)
967    * 
968    * @param alignedRegions
969    *          a list of "Align fromStart toStart fromCount"
970    * @param mapIsFromCdna
971    *          if true, 'from' is dna, else 'from' is protein
972    * @param strand
973    *          either 1 (forward) or -1 (reverse)
974    * @return
975    * @throws IOException
976    */
977   protected MapList constructCodonMappingFromAlign(
978           List<String> alignedRegions, boolean mapIsFromCdna, int strand)
979           throws IOException
980   {
981     if (strand == 0)
982     {
983       throw new IOException(
984               "Invalid strand for a codon mapping (cannot be 0)");
985     }
986     int regions = alignedRegions.size();
987     // arrays to hold [start, end] for each aligned region
988     int[] fromRanges = new int[regions * 2]; // from dna
989     int[] toRanges = new int[regions * 2]; // to protein
990     int fromRangesIndex = 0;
991     int toRangesIndex = 0;
992
993     for (String range : alignedRegions)
994     {
995       /* 
996        * Align mapFromStart mapToStart mapFromCount
997        * e.g. if mapIsFromCdna
998        *     Align 11270 143 120
999        * means:
1000        *     120 bases from pos 11270 align to pos 143 in peptide
1001        * if !mapIsFromCdna this would instead be
1002        *     Align 143 11270 40 
1003        */
1004       String[] tokens = range.split(" ");
1005       if (tokens.length != 3)
1006       {
1007         throw new IOException("Wrong number of fields for Align");
1008       }
1009       int fromStart = 0;
1010       int toStart = 0;
1011       int fromCount = 0;
1012       try
1013       {
1014         fromStart = Integer.parseInt(tokens[0]);
1015         toStart = Integer.parseInt(tokens[1]);
1016         fromCount = Integer.parseInt(tokens[2]);
1017       } catch (NumberFormatException nfe)
1018       {
1019         throw new IOException(
1020                 "Invalid number in Align field: " + nfe.getMessage());
1021       }
1022
1023       /*
1024        * Jalview always models from dna to protein, so adjust values if the
1025        * GFF mapping is from protein to dna
1026        */
1027       if (!mapIsFromCdna)
1028       {
1029         fromCount *= 3;
1030         int temp = fromStart;
1031         fromStart = toStart;
1032         toStart = temp;
1033       }
1034       fromRanges[fromRangesIndex++] = fromStart;
1035       fromRanges[fromRangesIndex++] = fromStart + strand * (fromCount - 1);
1036
1037       /*
1038        * If a codon has an intron gap, there will be contiguous 'toRanges';
1039        * this is handled for us by the MapList constructor. 
1040        * (It is not clear that exonerate ever generates this case)  
1041        */
1042       toRanges[toRangesIndex++] = toStart;
1043       toRanges[toRangesIndex++] = toStart + (fromCount - 1) / 3;
1044     }
1045
1046     return new MapList(fromRanges, toRanges, 3, 1);
1047   }
1048
1049   /**
1050    * Parse a GFF format feature. This may include creating a 'dummy' sequence to
1051    * hold the feature, or for its mapped sequence, or both, to be resolved
1052    * either later in the GFF file (##FASTA section), or when the user loads
1053    * additional sequences.
1054    * 
1055    * @param gffColumns
1056    * @param alignment
1057    * @param relaxedIdMatching
1058    * @param newseqs
1059    * @return
1060    */
1061   protected SequenceI parseGff(String[] gffColumns, AlignmentI alignment,
1062           boolean relaxedIdMatching, List<SequenceI> newseqs)
1063   {
1064     /*
1065      * GFF: seqid source type start end score strand phase [attributes]
1066      */
1067     if (gffColumns.length < 5)
1068     {
1069       System.err.println("Ignoring GFF feature line with too few columns ("
1070               + gffColumns.length + ")");
1071       return null;
1072     }
1073
1074     /*
1075      * locate referenced sequence in alignment _or_ 
1076      * as a forward or external reference (SequenceDummy)
1077      */
1078     String seqId = gffColumns[0];
1079     SequenceI seq = findSequence(seqId, alignment, newseqs,
1080             relaxedIdMatching);
1081
1082     SequenceFeature sf = null;
1083     GffHelperI helper = GffHelperFactory.getHelper(gffColumns);
1084     if (helper != null)
1085     {
1086       try
1087       {
1088         sf = helper.processGff(seq, gffColumns, alignment, newseqs,
1089                 relaxedIdMatching);
1090         if (sf != null)
1091         {
1092           seq.addSequenceFeature(sf);
1093           while ((seq = alignment.findName(seq, seqId, true)) != null)
1094           {
1095             seq.addSequenceFeature(new SequenceFeature(sf));
1096           }
1097         }
1098       } catch (IOException e)
1099       {
1100         System.err.println("GFF parsing failed with: " + e.getMessage());
1101         return null;
1102       }
1103     }
1104
1105     return seq;
1106   }
1107
1108   /**
1109    * Process the 'column 9' data of the GFF file. This is less formally defined,
1110    * and its interpretation will vary depending on the tool that has generated
1111    * it.
1112    * 
1113    * @param attributes
1114    * @param sf
1115    */
1116   protected void processGffColumnNine(String attributes, SequenceFeature sf)
1117   {
1118     sf.setAttributes(attributes);
1119
1120     /*
1121      * Parse attributes in column 9 and add them to the sequence feature's 
1122      * 'otherData' table; use Note as a best proxy for description
1123      */
1124     char nameValueSeparator = gffVersion == 3 ? '=' : ' ';
1125     // TODO check we don't break GFF2 values which include commas here
1126     Map<String, List<String>> nameValues = GffHelperBase
1127             .parseNameValuePairs(attributes, ";", nameValueSeparator, ",");
1128     for (Entry<String, List<String>> attr : nameValues.entrySet())
1129     {
1130       String values = StringUtils.listToDelimitedString(attr.getValue(),
1131               "; ");
1132       sf.setValue(attr.getKey(), values);
1133       if (NOTE.equals(attr.getKey()))
1134       {
1135         sf.setDescription(values);
1136       }
1137     }
1138   }
1139
1140   /**
1141    * After encountering ##fasta in a GFF3 file, process the remainder of the
1142    * file as FAST sequence data. Any placeholder sequences created during
1143    * feature parsing are updated with the actual sequences.
1144    * 
1145    * @param align
1146    * @param newseqs
1147    * @throws IOException
1148    */
1149   protected void processAsFasta(AlignmentI align, List<SequenceI> newseqs)
1150           throws IOException
1151   {
1152     try
1153     {
1154       mark();
1155     } catch (IOException q)
1156     {
1157     }
1158     FastaFile parser = new FastaFile(this);
1159     List<SequenceI> includedseqs = parser.getSeqs();
1160
1161     SequenceIdMatcher smatcher = new SequenceIdMatcher(newseqs);
1162
1163     /*
1164      * iterate over includedseqs, and replacing matching ones with newseqs
1165      * sequences. Generic iterator not used here because we modify
1166      * includedseqs as we go
1167      */
1168     for (int p = 0, pSize = includedseqs.size(); p < pSize; p++)
1169     {
1170       // search for any dummy seqs that this sequence can be used to update
1171       SequenceI includedSeq = includedseqs.get(p);
1172       SequenceI dummyseq = smatcher.findIdMatch(includedSeq);
1173       if (dummyseq != null && dummyseq instanceof SequenceDummy)
1174       {
1175         // probably have the pattern wrong
1176         // idea is that a flyweight proxy for a sequence ID can be created for
1177         // 1. stable reference creation
1178         // 2. addition of annotation
1179         // 3. future replacement by a real sequence
1180         // current pattern is to create SequenceDummy objects - a convenience
1181         // constructor for a Sequence.
1182         // problem is that when promoted to a real sequence, all references
1183         // need to be updated somehow. We avoid that by keeping the same object.
1184         ((SequenceDummy) dummyseq).become(includedSeq);
1185         dummyseq.createDatasetSequence();
1186
1187         /*
1188          * Update mappings so they are now to the dataset sequence
1189          */
1190         for (AlignedCodonFrame mapping : align.getCodonFrames())
1191         {
1192           mapping.updateToDataset(dummyseq);
1193         }
1194
1195         /*
1196          * replace parsed sequence with the realised forward reference
1197          */
1198         includedseqs.set(p, dummyseq);
1199
1200         /*
1201          * and remove from the newseqs list
1202          */
1203         newseqs.remove(dummyseq);
1204       }
1205     }
1206
1207     /*
1208      * finally add sequences to the dataset
1209      */
1210     for (SequenceI seq : includedseqs)
1211     {
1212       // experimental: mapping-based 'alignment' to query sequence
1213       AlignmentUtils.alignSequenceAs(seq, align,
1214               String.valueOf(align.getGapCharacter()), false, true);
1215
1216       // rename sequences if GFF handler requested this
1217       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
1218       List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
1219       if (!sfs.isEmpty())
1220       {
1221         String newName = (String) sfs.get(0).getValue(
1222                 GffHelperI.RENAME_TOKEN);
1223         if (newName != null)
1224         {
1225           seq.setName(newName);
1226         }
1227       }
1228       align.addSequence(seq);
1229     }
1230   }
1231
1232   /**
1233    * Process a ## directive
1234    * 
1235    * @param line
1236    * @param gffProps
1237    * @param align
1238    * @param newseqs
1239    * @throws IOException
1240    */
1241   protected void processGffPragma(String line, Map<String, String> gffProps,
1242           AlignmentI align, List<SequenceI> newseqs) throws IOException
1243   {
1244     line = line.trim();
1245     if ("###".equals(line))
1246     {
1247       // close off any open 'forward references'
1248       return;
1249     }
1250
1251     String[] tokens = line.substring(2).split(" ");
1252     String pragma = tokens[0];
1253     String value = tokens.length == 1 ? null : tokens[1];
1254
1255     if ("gff-version".equalsIgnoreCase(pragma))
1256     {
1257       if (value != null)
1258       {
1259         try
1260         {
1261           // value may be e.g. "3.1.2"
1262           gffVersion = Integer.parseInt(value.split("\\.")[0]);
1263         } catch (NumberFormatException e)
1264         {
1265           // ignore
1266         }
1267       }
1268     }
1269     else if ("sequence-region".equalsIgnoreCase(pragma))
1270     {
1271       // could capture <seqid start end> if wanted here
1272     }
1273     else if ("feature-ontology".equalsIgnoreCase(pragma))
1274     {
1275       // should resolve against the specified feature ontology URI
1276     }
1277     else if ("attribute-ontology".equalsIgnoreCase(pragma))
1278     {
1279       // URI of attribute ontology - not currently used in GFF3
1280     }
1281     else if ("source-ontology".equalsIgnoreCase(pragma))
1282     {
1283       // URI of source ontology - not currently used in GFF3
1284     }
1285     else if ("species-build".equalsIgnoreCase(pragma))
1286     {
1287       // save URI of specific NCBI taxon version of annotations
1288       gffProps.put("species-build", value);
1289     }
1290     else if ("fasta".equalsIgnoreCase(pragma))
1291     {
1292       // process the rest of the file as a fasta file and replace any dummy
1293       // sequence IDs
1294       processAsFasta(align, newseqs);
1295     }
1296     else
1297     {
1298       System.err.println("Ignoring unknown pragma: " + line);
1299     }
1300   }
1301 }