aa21b0f653f870544c337266c37e8fe8f1fe5fe1
[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 File or String filename
109    * @param paste
110    * @throws IOException
111    */
112   public FeaturesFile(Object 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, Object 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
585     if (!includeNonPositional
586             && (visibleColours == null || visibleColours.isEmpty()))
587     {
588       // no point continuing.
589       return "No Features Visible";
590     }
591
592     /*
593      * write out feature colours (if we know them)
594      */
595     // TODO: decide if feature links should also be written here ?
596     StringBuilder out = new StringBuilder(256);
597     if (visibleColours != null)
598     {
599       for (Entry<String, FeatureColourI> featureColour : visibleColours
600               .entrySet())
601       {
602         FeatureColourI colour = featureColour.getValue();
603         out.append(colour.toJalviewFormat(featureColour.getKey())).append(
604                 newline);
605       }
606     }
607
608     String[] types = visibleColours == null ? new String[0]
609             : visibleColours.keySet()
610                     .toArray(new String[visibleColours.keySet().size()]);
611
612     /*
613      * feature filters if any
614      */
615     outputFeatureFilters(out, visibleColours, featureFilters);
616
617     /*
618      * output features within groups
619      */
620     int count = outputFeaturesByGroup(out, fr, types, sequences,
621             includeNonPositional);
622
623     return count > 0 ? out.toString() : "No Features Visible";
624   }
625
626   /**
627    * Outputs any feature filters defined for visible feature types, sandwiched by
628    * STARTFILTERS and ENDFILTERS lines
629    * 
630    * @param out
631    * @param visible
632    * @param featureFilters
633    */
634   void outputFeatureFilters(StringBuilder out,
635           Map<String, FeatureColourI> visible,
636           Map<String, FeatureMatcherSetI> featureFilters)
637   {
638     if (visible == null || featureFilters == null
639             || featureFilters.isEmpty())
640     {
641       return;
642     }
643
644     boolean first = true;
645     for (String featureType : visible.keySet())
646     {
647       FeatureMatcherSetI filter = featureFilters.get(featureType);
648       if (filter != null)
649       {
650         if (first)
651         {
652           first = false;
653           out.append(newline).append(STARTFILTERS).append(newline);
654         }
655         out.append(featureType).append(TAB).append(filter.toStableString())
656                 .append(newline);
657       }
658     }
659     if (!first)
660     {
661       out.append(ENDFILTERS).append(newline);
662     }
663
664   }
665
666   /**
667    * Appends output of visible sequence features within feature groups to the
668    * output buffer. Groups other than the null or empty group are sandwiched by
669    * STARTGROUP and ENDGROUP lines. Answers the number of features written.
670    * 
671    * @param out
672    * @param fr
673    * @param featureTypes
674    * @param sequences
675    * @param includeNonPositional
676    * @return
677    */
678   private int outputFeaturesByGroup(StringBuilder out,
679           FeatureRenderer fr, String[] featureTypes,
680           SequenceI[] sequences, boolean includeNonPositional)
681   {
682     List<String> featureGroups = fr.getFeatureGroups();
683
684     /*
685      * sort groups alphabetically, and ensure that features with a
686      * null or empty group are output after those in named groups
687      */
688     List<String> sortedGroups = new ArrayList<>(featureGroups);
689     sortedGroups.remove(null);
690     sortedGroups.remove("");
691     Collections.sort(sortedGroups);
692     sortedGroups.add(null);
693     sortedGroups.add("");
694
695     int count = 0;
696     List<String> visibleGroups = fr.getDisplayedFeatureGroups();
697
698     /*
699      * loop over all groups (may be visible or not);
700      * non-positional features are output even if group is not visible
701      */
702     for (String group : sortedGroups)
703     {
704       boolean firstInGroup = true;
705       boolean isNullGroup = group == null || "".equals(group);
706
707       for (int i = 0; i < sequences.length; i++)
708       {
709         String sequenceName = sequences[i].getName();
710         List<SequenceFeature> features = new ArrayList<>();
711
712         /*
713          * get any non-positional features in this group, if wanted
714          * (for any feature type, whether visible or not)
715          */
716         if (includeNonPositional)
717         {
718           features.addAll(sequences[i].getFeatures()
719                   .getFeaturesForGroup(false, group));
720         }
721
722         /*
723          * add positional features for visible feature types, but
724          * (for named groups) only if feature group is visible
725          */
726         if (featureTypes.length > 0
727                 && (isNullGroup || visibleGroups.contains(group)))
728         {
729           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
730                   true, group, featureTypes));
731         }
732
733         for (SequenceFeature sf : features)
734         {
735           if (sf.isNonPositional() || fr.isVisible(sf))
736           {
737             count++;
738             if (firstInGroup)
739             {
740               out.append(newline);
741               if (!isNullGroup)
742               {
743                 out.append(STARTGROUP).append(TAB).append(group)
744                         .append(newline);
745               }
746             }
747             firstInGroup = false;
748             out.append(formatJalviewFeature(sequenceName, sf));
749           }
750         }
751       }
752
753       if (!isNullGroup && !firstInGroup)
754       {
755         out.append(ENDGROUP).append(TAB).append(group).append(newline);
756       }
757     }
758     return count;
759   }
760
761   /**
762    * @param out
763    * @param sequenceName
764    * @param sequenceFeature
765    */
766   protected String formatJalviewFeature(
767           String sequenceName, SequenceFeature sequenceFeature)
768   {
769     StringBuilder out = new StringBuilder(64);
770     if (sequenceFeature.description == null
771             || sequenceFeature.description.equals(""))
772     {
773       out.append(sequenceFeature.type).append(TAB);
774     }
775     else
776     {
777       if (sequenceFeature.links != null
778               && sequenceFeature.getDescription().indexOf("<html>") == -1)
779       {
780         out.append("<html>");
781       }
782
783       out.append(sequenceFeature.description);
784       if (sequenceFeature.links != null)
785       {
786         for (int l = 0; l < sequenceFeature.links.size(); l++)
787         {
788           String label = sequenceFeature.links.elementAt(l);
789           String href = label.substring(label.indexOf("|") + 1);
790           label = label.substring(0, label.indexOf("|"));
791
792           if (sequenceFeature.description.indexOf(href) == -1)
793           {
794             out.append(" <a href=\"" + href + "\">" + label + "</a>");
795           }
796         }
797
798         if (sequenceFeature.getDescription().indexOf("</html>") == -1)
799         {
800           out.append("</html>");
801         }
802       }
803
804       out.append(TAB);
805     }
806     out.append(sequenceName);
807     out.append("\t-1\t");
808     out.append(sequenceFeature.begin);
809     out.append(TAB);
810     out.append(sequenceFeature.end);
811     out.append(TAB);
812     out.append(sequenceFeature.type);
813     if (!Float.isNaN(sequenceFeature.score))
814     {
815       out.append(TAB);
816       out.append(sequenceFeature.score);
817     }
818     out.append(newline);
819
820     return out.toString();
821   }
822
823   /**
824    * Parse method that is called when a GFF file is dragged to the desktop
825    */
826   @Override
827   public void parse()
828   {
829     AlignViewportI av = getViewport();
830     if (av != null)
831     {
832       AlignmentI a = av.getAlignment();
833       if (a != null)
834       {
835         dataset = a.getDataset();
836       }
837       if (dataset == null)
838       {
839         // working in the applet context ?
840         dataset = a;
841       }
842     }
843     else
844     {
845       dataset = new Alignment(new SequenceI[] {});
846     }
847
848     Map<String, FeatureColourI> featureColours = new HashMap<>();
849     boolean parseResult = parse(dataset, featureColours, false, true);
850     if (!parseResult)
851     {
852       // pass error up somehow
853     }
854     if (av != null)
855     {
856       // update viewport with the dataset data ?
857     }
858     else
859     {
860       setSeqs(dataset.getSequencesArray());
861     }
862   }
863
864   /**
865    * Implementation of unused abstract method
866    * 
867    * @return error message
868    */
869   @Override
870   public String print(SequenceI[] sqs, boolean jvsuffix)
871   {
872     System.out.println("Use printGffFormat() or printJalviewFormat()");
873     return null;
874   }
875
876   /**
877    * Returns features output in GFF2 format
878    * 
879    * @param sequences
880    *          the sequences whose features are to be output
881    * @param visible
882    *          a map whose keys are the type names of visible features
883    * @param visibleFeatureGroups
884    * @param includeNonPositionalFeatures
885    * @return
886    */
887   public String printGffFormat(SequenceI[] sequences,
888           FeatureRenderer fr, boolean includeNonPositionalFeatures)
889   {
890     Map<String, FeatureColourI> visibleColours = fr.getDisplayedFeatureCols();
891
892     StringBuilder out = new StringBuilder(256);
893
894     out.append(String.format("%s %d" + newline, GFF_VERSION,
895             gffVersion == 0 ? 2 : gffVersion));
896
897     if (!includeNonPositionalFeatures
898             && (visibleColours == null || visibleColours.isEmpty()))
899     {
900       return out.toString();
901     }
902
903     String[] types = visibleColours == null ? new String[0]
904             : visibleColours.keySet()
905                     .toArray(new String[visibleColours.keySet().size()]);
906
907     for (SequenceI seq : sequences)
908     {
909       List<SequenceFeature> features = new ArrayList<>();
910       if (includeNonPositionalFeatures)
911       {
912         features.addAll(seq.getFeatures().getNonPositionalFeatures());
913       }
914       if (visibleColours != null && !visibleColours.isEmpty())
915       {
916         features.addAll(seq.getFeatures().getPositionalFeatures(types));
917       }
918
919       for (SequenceFeature sf : features)
920       {
921         if (!sf.isNonPositional() && !fr.isVisible(sf))
922         {
923           /*
924            * feature hidden by group visibility, colour threshold,
925            * or feature filter condition
926            */
927           continue;
928         }
929
930         String source = sf.featureGroup;
931         if (source == null)
932         {
933           source = sf.getDescription();
934         }
935
936         out.append(seq.getName());
937         out.append(TAB);
938         out.append(source);
939         out.append(TAB);
940         out.append(sf.type);
941         out.append(TAB);
942         out.append(sf.begin);
943         out.append(TAB);
944         out.append(sf.end);
945         out.append(TAB);
946         out.append(sf.score);
947         out.append(TAB);
948
949         int strand = sf.getStrand();
950         out.append(strand == 1 ? "+" : (strand == -1 ? "-" : "."));
951         out.append(TAB);
952
953         String phase = sf.getPhase();
954         out.append(phase == null ? "." : phase);
955
956         // miscellaneous key-values (GFF column 9)
957         String attributes = sf.getAttributes();
958         if (attributes != null)
959         {
960           out.append(TAB).append(attributes);
961         }
962
963         out.append(newline);
964       }
965     }
966
967     return out.toString();
968   }
969
970   /**
971    * Returns a mapping given list of one or more Align descriptors (exonerate
972    * format)
973    * 
974    * @param alignedRegions
975    *          a list of "Align fromStart toStart fromCount"
976    * @param mapIsFromCdna
977    *          if true, 'from' is dna, else 'from' is protein
978    * @param strand
979    *          either 1 (forward) or -1 (reverse)
980    * @return
981    * @throws IOException
982    */
983   protected MapList constructCodonMappingFromAlign(
984           List<String> alignedRegions, boolean mapIsFromCdna, int strand)
985           throws IOException
986   {
987     if (strand == 0)
988     {
989       throw new IOException(
990               "Invalid strand for a codon mapping (cannot be 0)");
991     }
992     int regions = alignedRegions.size();
993     // arrays to hold [start, end] for each aligned region
994     int[] fromRanges = new int[regions * 2]; // from dna
995     int[] toRanges = new int[regions * 2]; // to protein
996     int fromRangesIndex = 0;
997     int toRangesIndex = 0;
998
999     for (String range : alignedRegions)
1000     {
1001       /* 
1002        * Align mapFromStart mapToStart mapFromCount
1003        * e.g. if mapIsFromCdna
1004        *     Align 11270 143 120
1005        * means:
1006        *     120 bases from pos 11270 align to pos 143 in peptide
1007        * if !mapIsFromCdna this would instead be
1008        *     Align 143 11270 40 
1009        */
1010       String[] tokens = range.split(" ");
1011       if (tokens.length != 3)
1012       {
1013         throw new IOException("Wrong number of fields for Align");
1014       }
1015       int fromStart = 0;
1016       int toStart = 0;
1017       int fromCount = 0;
1018       try
1019       {
1020         fromStart = Integer.parseInt(tokens[0]);
1021         toStart = Integer.parseInt(tokens[1]);
1022         fromCount = Integer.parseInt(tokens[2]);
1023       } catch (NumberFormatException nfe)
1024       {
1025         throw new IOException(
1026                 "Invalid number in Align field: " + nfe.getMessage());
1027       }
1028
1029       /*
1030        * Jalview always models from dna to protein, so adjust values if the
1031        * GFF mapping is from protein to dna
1032        */
1033       if (!mapIsFromCdna)
1034       {
1035         fromCount *= 3;
1036         int temp = fromStart;
1037         fromStart = toStart;
1038         toStart = temp;
1039       }
1040       fromRanges[fromRangesIndex++] = fromStart;
1041       fromRanges[fromRangesIndex++] = fromStart + strand * (fromCount - 1);
1042
1043       /*
1044        * If a codon has an intron gap, there will be contiguous 'toRanges';
1045        * this is handled for us by the MapList constructor. 
1046        * (It is not clear that exonerate ever generates this case)  
1047        */
1048       toRanges[toRangesIndex++] = toStart;
1049       toRanges[toRangesIndex++] = toStart + (fromCount - 1) / 3;
1050     }
1051
1052     return new MapList(fromRanges, toRanges, 3, 1);
1053   }
1054
1055   /**
1056    * Parse a GFF format feature. This may include creating a 'dummy' sequence to
1057    * hold the feature, or for its mapped sequence, or both, to be resolved
1058    * either later in the GFF file (##FASTA section), or when the user loads
1059    * additional sequences.
1060    * 
1061    * @param gffColumns
1062    * @param alignment
1063    * @param relaxedIdMatching
1064    * @param newseqs
1065    * @return
1066    */
1067   protected SequenceI parseGff(String[] gffColumns, AlignmentI alignment,
1068           boolean relaxedIdMatching, List<SequenceI> newseqs)
1069   {
1070     /*
1071      * GFF: seqid source type start end score strand phase [attributes]
1072      */
1073     if (gffColumns.length < 5)
1074     {
1075       System.err.println("Ignoring GFF feature line with too few columns ("
1076               + gffColumns.length + ")");
1077       return null;
1078     }
1079
1080     /*
1081      * locate referenced sequence in alignment _or_ 
1082      * as a forward or external reference (SequenceDummy)
1083      */
1084     String seqId = gffColumns[0];
1085     SequenceI seq = findSequence(seqId, alignment, newseqs,
1086             relaxedIdMatching);
1087
1088     SequenceFeature sf = null;
1089     GffHelperI helper = GffHelperFactory.getHelper(gffColumns);
1090     if (helper != null)
1091     {
1092       try
1093       {
1094         sf = helper.processGff(seq, gffColumns, alignment, newseqs,
1095                 relaxedIdMatching);
1096         if (sf != null)
1097         {
1098           seq.addSequenceFeature(sf);
1099           while ((seq = alignment.findName(seq, seqId, true)) != null)
1100           {
1101             seq.addSequenceFeature(new SequenceFeature(sf));
1102           }
1103         }
1104       } catch (IOException e)
1105       {
1106         System.err.println("GFF parsing failed with: " + e.getMessage());
1107         return null;
1108       }
1109     }
1110
1111     return seq;
1112   }
1113
1114   /**
1115    * Process the 'column 9' data of the GFF file. This is less formally defined,
1116    * and its interpretation will vary depending on the tool that has generated
1117    * it.
1118    * 
1119    * @param attributes
1120    * @param sf
1121    */
1122   protected void processGffColumnNine(String attributes, SequenceFeature sf)
1123   {
1124     sf.setAttributes(attributes);
1125
1126     /*
1127      * Parse attributes in column 9 and add them to the sequence feature's 
1128      * 'otherData' table; use Note as a best proxy for description
1129      */
1130     char nameValueSeparator = gffVersion == 3 ? '=' : ' ';
1131     // TODO check we don't break GFF2 values which include commas here
1132     Map<String, List<String>> nameValues = GffHelperBase
1133             .parseNameValuePairs(attributes, ";", nameValueSeparator, ",");
1134     for (Entry<String, List<String>> attr : nameValues.entrySet())
1135     {
1136       String values = StringUtils.listToDelimitedString(attr.getValue(),
1137               "; ");
1138       sf.setValue(attr.getKey(), values);
1139       if (NOTE.equals(attr.getKey()))
1140       {
1141         sf.setDescription(values);
1142       }
1143     }
1144   }
1145
1146   /**
1147    * After encountering ##fasta in a GFF3 file, process the remainder of the
1148    * file as FAST sequence data. Any placeholder sequences created during
1149    * feature parsing are updated with the actual sequences.
1150    * 
1151    * @param align
1152    * @param newseqs
1153    * @throws IOException
1154    */
1155   protected void processAsFasta(AlignmentI align, List<SequenceI> newseqs)
1156           throws IOException
1157   {
1158     try
1159     {
1160       mark();
1161     } catch (IOException q)
1162     {
1163     }
1164     FastaFile parser = new FastaFile(this);
1165     List<SequenceI> includedseqs = parser.getSeqs();
1166
1167     SequenceIdMatcher smatcher = new SequenceIdMatcher(newseqs);
1168
1169     /*
1170      * iterate over includedseqs, and replacing matching ones with newseqs
1171      * sequences. Generic iterator not used here because we modify
1172      * includedseqs as we go
1173      */
1174     for (int p = 0, pSize = includedseqs.size(); p < pSize; p++)
1175     {
1176       // search for any dummy seqs that this sequence can be used to update
1177       SequenceI includedSeq = includedseqs.get(p);
1178       SequenceI dummyseq = smatcher.findIdMatch(includedSeq);
1179       if (dummyseq != null && dummyseq instanceof SequenceDummy)
1180       {
1181         // probably have the pattern wrong
1182         // idea is that a flyweight proxy for a sequence ID can be created for
1183         // 1. stable reference creation
1184         // 2. addition of annotation
1185         // 3. future replacement by a real sequence
1186         // current pattern is to create SequenceDummy objects - a convenience
1187         // constructor for a Sequence.
1188         // problem is that when promoted to a real sequence, all references
1189         // need to be updated somehow. We avoid that by keeping the same object.
1190         ((SequenceDummy) dummyseq).become(includedSeq);
1191         dummyseq.createDatasetSequence();
1192
1193         /*
1194          * Update mappings so they are now to the dataset sequence
1195          */
1196         for (AlignedCodonFrame mapping : align.getCodonFrames())
1197         {
1198           mapping.updateToDataset(dummyseq);
1199         }
1200
1201         /*
1202          * replace parsed sequence with the realised forward reference
1203          */
1204         includedseqs.set(p, dummyseq);
1205
1206         /*
1207          * and remove from the newseqs list
1208          */
1209         newseqs.remove(dummyseq);
1210       }
1211     }
1212
1213     /*
1214      * finally add sequences to the dataset
1215      */
1216     for (SequenceI seq : includedseqs)
1217     {
1218       // experimental: mapping-based 'alignment' to query sequence
1219       AlignmentUtils.alignSequenceAs(seq, align,
1220               String.valueOf(align.getGapCharacter()), false, true);
1221
1222       // rename sequences if GFF handler requested this
1223       // TODO a more elegant way e.g. gffHelper.postProcess(newseqs) ?
1224       List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures();
1225       if (!sfs.isEmpty())
1226       {
1227         String newName = (String) sfs.get(0).getValue(
1228                 GffHelperI.RENAME_TOKEN);
1229         if (newName != null)
1230         {
1231           seq.setName(newName);
1232         }
1233       }
1234       align.addSequence(seq);
1235     }
1236   }
1237
1238   /**
1239    * Process a ## directive
1240    * 
1241    * @param line
1242    * @param gffProps
1243    * @param align
1244    * @param newseqs
1245    * @throws IOException
1246    */
1247   protected void processGffPragma(String line, Map<String, String> gffProps,
1248           AlignmentI align, List<SequenceI> newseqs) throws IOException
1249   {
1250     line = line.trim();
1251     if ("###".equals(line))
1252     {
1253       // close off any open 'forward references'
1254       return;
1255     }
1256
1257     String[] tokens = line.substring(2).split(" ");
1258     String pragma = tokens[0];
1259     String value = tokens.length == 1 ? null : tokens[1];
1260
1261     if ("gff-version".equalsIgnoreCase(pragma))
1262     {
1263       if (value != null)
1264       {
1265         try
1266         {
1267           // value may be e.g. "3.1.2"
1268           gffVersion = Integer.parseInt(value.split("\\.")[0]);
1269         } catch (NumberFormatException e)
1270         {
1271           // ignore
1272         }
1273       }
1274     }
1275     else if ("sequence-region".equalsIgnoreCase(pragma))
1276     {
1277       // could capture <seqid start end> if wanted here
1278     }
1279     else if ("feature-ontology".equalsIgnoreCase(pragma))
1280     {
1281       // should resolve against the specified feature ontology URI
1282     }
1283     else if ("attribute-ontology".equalsIgnoreCase(pragma))
1284     {
1285       // URI of attribute ontology - not currently used in GFF3
1286     }
1287     else if ("source-ontology".equalsIgnoreCase(pragma))
1288     {
1289       // URI of source ontology - not currently used in GFF3
1290     }
1291     else if ("species-build".equalsIgnoreCase(pragma))
1292     {
1293       // save URI of specific NCBI taxon version of annotations
1294       gffProps.put("species-build", value);
1295     }
1296     else if ("fasta".equalsIgnoreCase(pragma))
1297     {
1298       // process the rest of the file as a fasta file and replace any dummy
1299       // sequence IDs
1300       processAsFasta(align, newseqs);
1301     }
1302     else
1303     {
1304       System.err.println("Ignoring unknown pragma: " + line);
1305     }
1306   }
1307 }