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