JAL-653 gff version number field
[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.SequenceIdMatcher;
24 import jalview.datamodel.AlignedCodonFrame;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.SequenceDummy;
27 import jalview.datamodel.SequenceFeature;
28 import jalview.datamodel.SequenceI;
29 import jalview.schemes.AnnotationColourGradient;
30 import jalview.schemes.GraduatedColor;
31 import jalview.schemes.UserColourScheme;
32 import jalview.util.Format;
33 import jalview.util.MapList;
34
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.Hashtable;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.StringTokenizer;
44 import java.util.Vector;
45
46 /**
47  * Parse and create Jalview Features files Detects GFF format features files and
48  * parses. Does not implement standard print() - call specific printFeatures or
49  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object
50  * for the features annotation - this normally works on an exact match.
51  * 
52  * @author AMW
53  * @version $Revision$
54  */
55 public class FeaturesFile extends AlignFile
56 {
57   /**
58    * work around for GFF interpretation bug where source string becomes
59    * description rather than a group
60    */
61   private boolean doGffSource = true;
62
63   private int gffversion;
64
65   /**
66    * Creates a new FeaturesFile object.
67    */
68   public FeaturesFile()
69   {
70   }
71
72   /**
73    * Creates a new FeaturesFile object.
74    * 
75    * @param inFile
76    *          DOCUMENT ME!
77    * @param type
78    *          DOCUMENT ME!
79    * 
80    * @throws IOException
81    *           DOCUMENT ME!
82    */
83   public FeaturesFile(String inFile, String type) throws IOException
84   {
85     super(inFile, type);
86   }
87
88   public FeaturesFile(FileParse source) throws IOException
89   {
90     super(source);
91   }
92
93   /**
94    * Parse GFF or sequence features file using case-independent matching,
95    * discarding URLs
96    * 
97    * @param align
98    *          - alignment/dataset containing sequences that are to be annotated
99    * @param colours
100    *          - hashtable to store feature colour definitions
101    * @param removeHTML
102    *          - process html strings into plain text
103    * @return true if features were added
104    */
105   public boolean parse(AlignmentI align, Hashtable colours,
106           boolean removeHTML)
107   {
108     return parse(align, colours, null, removeHTML, false);
109   }
110
111   /**
112    * Parse GFF or sequence features file optionally using case-independent
113    * matching, discarding URLs
114    * 
115    * @param align
116    *          - alignment/dataset containing sequences that are to be annotated
117    * @param colours
118    *          - hashtable to store feature colour definitions
119    * @param removeHTML
120    *          - process html strings into plain text
121    * @param relaxedIdmatching
122    *          - when true, ID matches to compound sequence IDs are allowed
123    * @return true if features were added
124    */
125   public boolean parse(AlignmentI align, Map colours, boolean removeHTML,
126           boolean relaxedIdMatching)
127   {
128     return parse(align, colours, null, removeHTML, relaxedIdMatching);
129   }
130
131   /**
132    * Parse GFF or sequence features file optionally using case-independent
133    * matching
134    * 
135    * @param align
136    *          - alignment/dataset containing sequences that are to be annotated
137    * @param colours
138    *          - hashtable to store feature colour definitions
139    * @param featureLink
140    *          - hashtable to store associated URLs
141    * @param removeHTML
142    *          - process html strings into plain text
143    * @return true if features were added
144    */
145   public boolean parse(AlignmentI align, Map colours, Map featureLink,
146           boolean removeHTML)
147   {
148     return parse(align, colours, featureLink, removeHTML, false);
149   }
150
151   /**
152    * Parse GFF or sequence features file
153    * 
154    * @param align
155    *          - alignment/dataset containing sequences that are to be annotated
156    * @param colours
157    *          - hashtable to store feature colour definitions
158    * @param featureLink
159    *          - hashtable to store associated URLs
160    * @param removeHTML
161    *          - process html strings into plain text
162    * @param relaxedIdmatching
163    *          - when true, ID matches to compound sequence IDs are allowed
164    * @return true if features were added
165    */
166   public boolean parse(AlignmentI align, Map colours, Map featureLink,
167           boolean removeHTML, boolean relaxedIdmatching)
168   {
169
170     String line = null;
171     try
172     {
173       SequenceI seq = null;
174       /**
175        * keep track of any sequences we try to create from the data if it is a GFF3 file
176        */
177       ArrayList<SequenceI> newseqs = new ArrayList<SequenceI>();
178       String type, desc, token = null;
179
180       int index, start, end;
181       float score;
182       StringTokenizer st;
183       SequenceFeature sf;
184       String featureGroup = null, groupLink = null;
185       Map typeLink = new Hashtable();
186       /**
187        * when true, assume GFF style features rather than Jalview style.
188        */
189       boolean GFFFile = true;
190       while ((line = nextLine()) != null)
191       {
192         if (line.startsWith("#"))
193         {
194           continue;
195         }
196
197         st = new StringTokenizer(line, "\t");
198         if (st.countTokens() == 1)
199         {
200           if (line.trim().equalsIgnoreCase("GFF"))
201           {
202             // Start parsing file as if it might be GFF again.
203             GFFFile = true;
204             continue;
205           }
206         }
207         if (st.countTokens() > 1 && st.countTokens() < 4)
208         {
209           GFFFile = false;
210           type = st.nextToken();
211           if (type.equalsIgnoreCase("startgroup"))
212           {
213             featureGroup = st.nextToken();
214             if (st.hasMoreElements())
215             {
216               groupLink = st.nextToken();
217               featureLink.put(featureGroup, groupLink);
218             }
219           }
220           else if (type.equalsIgnoreCase("endgroup"))
221           {
222             // We should check whether this is the current group,
223             // but at present theres no way of showing more than 1 group
224             st.nextToken();
225             featureGroup = null;
226             groupLink = null;
227           }
228           else
229           {
230             Object colour = null;
231             String colscheme = st.nextToken();
232             if (colscheme.indexOf("|") > -1
233                     || colscheme.trim().equalsIgnoreCase("label"))
234             {
235               // Parse '|' separated graduated colourscheme fields:
236               // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
237               // can either provide 'label' only, first is optional, next two
238               // colors are required (but may be
239               // left blank), next is optional, nxt two min/max are required.
240               // first is either 'label'
241               // first/second and third are both hexadecimal or word equivalent
242               // colour.
243               // next two are values parsed as floats.
244               // fifth is either 'above','below', or 'none'.
245               // sixth is a float value and only required when fifth is either
246               // 'above' or 'below'.
247               StringTokenizer gcol = new StringTokenizer(colscheme, "|",
248                       true);
249               // set defaults
250               int threshtype = AnnotationColourGradient.NO_THRESHOLD;
251               float min = Float.MIN_VALUE, max = Float.MAX_VALUE, threshval = Float.NaN;
252               boolean labelCol = false;
253               // Parse spec line
254               String mincol = gcol.nextToken();
255               if (mincol == "|")
256               {
257                 System.err
258                         .println("Expected either 'label' or a colour specification in the line: "
259                                 + line);
260                 continue;
261               }
262               String maxcol = null;
263               if (mincol.toLowerCase().indexOf("label") == 0)
264               {
265                 labelCol = true;
266                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip
267                                                                            // '|'
268                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
269               }
270               String abso = null, minval, maxval;
271               if (mincol != null)
272               {
273                 // at least four more tokens
274                 if (mincol.equals("|"))
275                 {
276                   mincol = "";
277                 }
278                 else
279                 {
280                   gcol.nextToken(); // skip next '|'
281                 }
282                 // continue parsing rest of line
283                 maxcol = gcol.nextToken();
284                 if (maxcol.equals("|"))
285                 {
286                   maxcol = "";
287                 }
288                 else
289                 {
290                   gcol.nextToken(); // skip next '|'
291                 }
292                 abso = gcol.nextToken();
293                 gcol.nextToken(); // skip next '|'
294                 if (abso.toLowerCase().indexOf("abso") != 0)
295                 {
296                   minval = abso;
297                   abso = null;
298                 }
299                 else
300                 {
301                   minval = gcol.nextToken();
302                   gcol.nextToken(); // skip next '|'
303                 }
304                 maxval = gcol.nextToken();
305                 if (gcol.hasMoreTokens())
306                 {
307                   gcol.nextToken(); // skip next '|'
308                 }
309                 try
310                 {
311                   if (minval.length() > 0)
312                   {
313                     min = new Float(minval).floatValue();
314                   }
315                 } catch (Exception e)
316                 {
317                   System.err
318                           .println("Couldn't parse the minimum value for graduated colour for type ("
319                                   + colscheme
320                                   + ") - did you misspell 'auto' for the optional automatic colour switch ?");
321                   e.printStackTrace();
322                 }
323                 try
324                 {
325                   if (maxval.length() > 0)
326                   {
327                     max = new Float(maxval).floatValue();
328                   }
329                 } catch (Exception e)
330                 {
331                   System.err
332                           .println("Couldn't parse the maximum value for graduated colour for type ("
333                                   + colscheme + ")");
334                   e.printStackTrace();
335                 }
336               }
337               else
338               {
339                 // add in some dummy min/max colours for the label-only
340                 // colourscheme.
341                 mincol = "FFFFFF";
342                 maxcol = "000000";
343               }
344               try
345               {
346                 colour = new jalview.schemes.GraduatedColor(
347                         new UserColourScheme(mincol).findColour('A'),
348                         new UserColourScheme(maxcol).findColour('A'), min,
349                         max);
350               } catch (Exception e)
351               {
352                 System.err
353                         .println("Couldn't parse the graduated colour scheme ("
354                                 + colscheme + ")");
355                 e.printStackTrace();
356               }
357               if (colour != null)
358               {
359                 ((jalview.schemes.GraduatedColor) colour)
360                         .setColourByLabel(labelCol);
361                 ((jalview.schemes.GraduatedColor) colour)
362                         .setAutoScaled(abso == null);
363                 // add in any additional parameters
364                 String ttype = null, tval = null;
365                 if (gcol.hasMoreTokens())
366                 {
367                   // threshold type and possibly a threshold value
368                   ttype = gcol.nextToken();
369                   if (ttype.toLowerCase().startsWith("below"))
370                   {
371                     ((jalview.schemes.GraduatedColor) colour)
372                             .setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
373                   }
374                   else if (ttype.toLowerCase().startsWith("above"))
375                   {
376                     ((jalview.schemes.GraduatedColor) colour)
377                             .setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
378                   }
379                   else
380                   {
381                     ((jalview.schemes.GraduatedColor) colour)
382                             .setThreshType(AnnotationColourGradient.NO_THRESHOLD);
383                     if (!ttype.toLowerCase().startsWith("no"))
384                     {
385                       System.err
386                               .println("Ignoring unrecognised threshold type : "
387                                       + ttype);
388                     }
389                   }
390                 }
391                 if (((GraduatedColor) colour).getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
392                 {
393                   try
394                   {
395                     gcol.nextToken();
396                     tval = gcol.nextToken();
397                     ((jalview.schemes.GraduatedColor) colour)
398                             .setThresh(new Float(tval).floatValue());
399                   } catch (Exception e)
400                   {
401                     System.err
402                             .println("Couldn't parse threshold value as a float: ("
403                                     + tval + ")");
404                     e.printStackTrace();
405                   }
406                 }
407                 // parse the thresh-is-min token ?
408                 if (gcol.hasMoreTokens())
409                 {
410                   System.err
411                           .println("Ignoring additional tokens in parameters in graduated colour specification\n");
412                   while (gcol.hasMoreTokens())
413                   {
414                     System.err.println("|" + gcol.nextToken());
415                   }
416                   System.err.println("\n");
417                 }
418               }
419             }
420             else
421             {
422               UserColourScheme ucs = new UserColourScheme(colscheme);
423               colour = ucs.findColour('A');
424             }
425             if (colour != null)
426             {
427               colours.put(type, colour);
428             }
429             if (st.hasMoreElements())
430             {
431               String link = st.nextToken();
432               typeLink.put(type, link);
433               if (featureLink == null)
434               {
435                 featureLink = new Hashtable();
436               }
437               featureLink.put(type, link);
438             }
439           }
440           continue;
441         }
442         String seqId = "";
443         while (st.hasMoreElements())
444         {
445
446           if (GFFFile)
447           {
448             // Still possible this is an old Jalview file,
449             // which does not have type colours at the beginning
450             seqId = token = st.nextToken();
451             seq = findName(align, seqId, relaxedIdmatching, newseqs);
452             if (seq != null)
453             {
454               desc = st.nextToken();
455               String group = null;
456               if (doGffSource && desc.indexOf(' ') == -1)
457               {
458                 // could also be a source term rather than description line
459                 group = new String(desc);
460               }
461               type = st.nextToken();
462               try
463               {
464                 String stt = st.nextToken();
465                 if (stt.length() == 0 || stt.equals("-"))
466                 {
467                   start = 0;
468                 }
469                 else
470                 {
471                   start = Integer.parseInt(stt);
472                 }
473               } catch (NumberFormatException ex)
474               {
475                 start = 0;
476               }
477               try
478               {
479                 String stt = st.nextToken();
480                 if (stt.length() == 0 || stt.equals("-"))
481                 {
482                   end = 0;
483                 }
484                 else
485                 {
486                   end = Integer.parseInt(stt);
487                 }
488               } catch (NumberFormatException ex)
489               {
490                 end = 0;
491               }
492               // TODO: decide if non positional feature assertion for input data
493               // where end==0 is generally valid
494               if (end == 0)
495               {
496                 // treat as non-positional feature, regardless.
497                 start = 0;
498               }
499               try
500               {
501                 score = new Float(st.nextToken()).floatValue();
502               } catch (NumberFormatException ex)
503               {
504                 score = 0;
505               }
506
507               sf = new SequenceFeature(type, desc, start, end, score, group);
508
509               try
510               {
511                 sf.setValue("STRAND", st.nextToken());
512                 sf.setValue("FRAME", st.nextToken());
513               } catch (Exception ex)
514               {
515               }
516
517               if (st.hasMoreTokens())
518               {
519                 StringBuffer attributes = new StringBuffer();
520                 boolean sep = false;
521                 while (st.hasMoreTokens())
522                 {
523                   attributes.append((sep ? "\t" : "") + st.nextElement());
524                   sep = true;
525                 }
526                 // TODO validate and split GFF2 attributes field ? parse out
527                 // ([A-Za-z][A-Za-z0-9_]*) <value> ; and add as
528                 // sf.setValue(attrib, val);
529                 sf.setValue("ATTRIBUTES", attributes.toString());
530               }
531
532               if (processOrAddSeqFeature(align, newseqs, seq, sf, GFFFile,
533                       relaxedIdmatching))
534               {
535                 // check whether we should add the sequence feature to any other
536                 // sequences in the alignment with the same or similar
537                 while ((seq = align.findName(seq, seqId, true)) != null)
538                 {
539                   seq.addSequenceFeature(new SequenceFeature(sf));
540                 }
541               }
542               break;
543             }
544           }
545
546           if (GFFFile && seq == null)
547           {
548             desc = token;
549           }
550           else
551           {
552             desc = st.nextToken();
553           }
554           if (!st.hasMoreTokens())
555           {
556             System.err
557                     .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");
558             // in all probability, this isn't a file we understand, so bail
559             // quietly.
560             return false;
561           }
562
563           token = st.nextToken();
564
565           if (!token.equals("ID_NOT_SPECIFIED"))
566           {
567             seq = findName(align, seqId = token, relaxedIdmatching, null);
568             st.nextToken();
569           }
570           else
571           {
572             seqId = null;
573             try
574             {
575               index = Integer.parseInt(st.nextToken());
576               seq = align.getSequenceAt(index);
577             } catch (NumberFormatException ex)
578             {
579               seq = null;
580             }
581           }
582
583           if (seq == null)
584           {
585             System.out.println("Sequence not found: " + line);
586             break;
587           }
588
589           start = Integer.parseInt(st.nextToken());
590           end = Integer.parseInt(st.nextToken());
591
592           type = st.nextToken();
593
594           if (!colours.containsKey(type))
595           {
596             // Probably the old style groups file
597             UserColourScheme ucs = new UserColourScheme(type);
598             colours.put(type, ucs.findColour('A'));
599           }
600           sf = new SequenceFeature(type, desc, "", start, end, featureGroup);
601           if (st.hasMoreTokens())
602           {
603             try
604             {
605               score = new Float(st.nextToken()).floatValue();
606               // update colourgradient bounds if allowed to
607             } catch (NumberFormatException ex)
608             {
609               score = 0;
610             }
611             sf.setScore(score);
612           }
613           if (groupLink != null && removeHTML)
614           {
615             sf.addLink(groupLink);
616             sf.description += "%LINK%";
617           }
618           if (typeLink.containsKey(type) && removeHTML)
619           {
620             sf.addLink(typeLink.get(type).toString());
621             sf.description += "%LINK%";
622           }
623
624           parseDescriptionHTML(sf, removeHTML);
625
626           seq.addSequenceFeature(sf);
627
628           while (seqId != null
629                   && (seq = align.findName(seq, seqId, false)) != null)
630           {
631             seq.addSequenceFeature(new SequenceFeature(sf));
632           }
633           // If we got here, its not a GFFFile
634           GFFFile = false;
635         }
636       }
637       resetMatcher();
638     } catch (Exception ex)
639     {
640       // should report somewhere useful for UI if necessary
641       warningMessage = ((warningMessage == null) ? "" : warningMessage)
642               + "Parsing error at\n" + line;
643       System.out.println("Error parsing feature file: " + ex + "\n" + line);
644       ex.printStackTrace(System.err);
645       resetMatcher();
646       return false;
647     }
648
649     return true;
650   }
651
652
653   /**
654    * take a sequence feature and examine its attributes to decide how it should
655    * be added to a sequence
656    * 
657    * @param seq
658    *          - the destination sequence constructed or discovered in the
659    *          current context
660    * @param sf
661    *          - the base feature with ATTRIBUTES property containing any
662    *          additional attributes
663    * @param gFFFile
664    *          - true if we are processing a GFF annotation file
665    * @return true if sf was actually added to the sequence, false if it was
666    *         processed in another way
667    */
668   public boolean processOrAddSeqFeature(AlignmentI align, List<SequenceI> newseqs, SequenceI seq, SequenceFeature sf,
669           boolean gFFFile, boolean relaxedIdMatching)
670   {
671     String attr = (String) sf.getValue("ATTRIBUTES");
672     boolean add = true;
673     if (gFFFile && attr != null)
674     {
675       int nattr=8;
676
677       for (String attset : attr.split("\t"))
678       {
679         if (attset==null || attset.trim().length()==0)
680         {
681           continue;
682         }
683         nattr++;
684         Map<String, List<String>> set = new HashMap<String, List<String>>();
685         // normally, only expect one column - 9 - in this field
686         // the attributes (Gff3) or groups (gff2) field
687         for (String pair : attset.trim().split(";"))
688         {
689           pair = pair.trim();
690           if (pair.length() == 0)
691           {
692             continue;
693           }
694
695           // expect either space seperated (gff2) or '=' separated (gff3) 
696           // key/value pairs here
697
698           int eqpos = pair.indexOf('='),sppos = pair.indexOf(' ');
699           String key = null, value = null;
700
701           if (sppos > -1 && (eqpos == -1 || sppos < eqpos))
702           {
703             key = pair.substring(0, sppos);
704             value = pair.substring(sppos + 1);
705           } else {
706             if (eqpos > -1 && (sppos == -1 || eqpos < sppos))
707             {
708               key = pair.substring(0, eqpos);
709               value = pair.substring(eqpos + 1);
710             } else
711             {
712               key = pair;
713             }
714           }
715           if (key != null)
716           {
717             List<String> vals = set.get(key);
718             if (vals == null)
719             {
720               vals = new ArrayList<String>();
721               set.put(key, vals);
722             }
723             if (value != null)
724             {
725               vals.add(value.trim());
726             }
727           }
728         }
729         try
730         {
731           add &= processGffKey(set, nattr, seq, sf, align, newseqs,
732                   relaxedIdMatching); // process decides if
733                                                      // feature is actually
734                                                      // added
735         } catch (InvalidGFF3FieldException ivfe)
736         {
737           System.err.println(ivfe);
738         }
739       }
740     }
741     if (add)
742     {
743       seq.addSequenceFeature(sf);
744     }
745     return add;
746   }
747
748   public class InvalidGFF3FieldException extends Exception
749   {
750     String field, value;
751
752     public InvalidGFF3FieldException(String field,
753             Map<String, List<String>> set, String message)
754     {
755       super(message + " (Field was " + field + " and value was "
756               + set.get(field).toString());
757       this.field = field;
758       this.value = set.get(field).toString();
759     }
760
761   }
762
763   /**
764    * take a set of keys for a feature and interpret them
765    * 
766    * @param set
767    * @param nattr
768    * @param seq
769    * @param sf
770    * @return
771    */
772   public boolean processGffKey(Map<String, List<String>> set, int nattr,
773           SequenceI seq, SequenceFeature sf, AlignmentI align,
774           List<SequenceI> newseqs, boolean relaxedIdMatching)
775           throws InvalidGFF3FieldException
776   {
777     String attr;
778     // decide how to interpret according to type
779     if (sf.getType().equals("similarity"))
780     {
781       int strand = sf.getStrand();
782       // exonerate cdna/protein map
783       // look for fields 
784       List<SequenceI> querySeq = findNames(align, newseqs,
785               relaxedIdMatching, set.get(attr="Query"));
786       if (querySeq==null || querySeq.size()!=1)
787       {
788         throw new InvalidGFF3FieldException( attr, set,
789                 "Expecting exactly one sequence in Query field (got "
790                         + set.get(attr) + ")");
791       }
792       if (set.containsKey(attr="Align"))
793       {
794         // process the align maps and create cdna/protein maps
795         // ideally, the query sequences are in the alignment, but maybe not...
796         
797         AlignedCodonFrame alco = new AlignedCodonFrame();
798         MapList codonmapping = constructCodonMappingFromAlign(set, attr,
799                 strand);
800
801         // add codon mapping, and hope!
802         alco.addMap(seq, querySeq.get(0), codonmapping);
803         align.addCodonFrame(alco);
804         // everything that's needed to be done is done
805         // no features to create here !
806         return false;
807       }
808
809     }
810     return true;
811   }
812
813   private MapList constructCodonMappingFromAlign(
814           Map<String, List<String>> set,
815           String attr, int strand) throws InvalidGFF3FieldException
816   {
817     if (strand == 0)
818     {
819       throw new InvalidGFF3FieldException(attr, set,
820               "Invalid strand for a codon mapping (cannot be 0)");
821     }
822     List<Integer> fromrange = new ArrayList<Integer>(), torange = new ArrayList<Integer>();
823     int lastppos = 0, lastpframe = 0;
824     for (String range : set.get(attr))
825     {
826       List<Integer> ints = new ArrayList<Integer>();
827       StringTokenizer st = new StringTokenizer(range, " ");
828       while (st.hasMoreTokens())
829       {
830         String num = st.nextToken();
831         try
832         {
833           ints.add(new Integer(num));
834         } catch (NumberFormatException nfe)
835         {
836           throw new InvalidGFF3FieldException(attr, set,
837                   "Invalid number in field " + num);
838         }
839       }
840       // Align positionInRef positionInQuery LengthInRef
841       // contig_1146 exonerate:protein2genome:local similarity 8534 11269
842       // 3652 - . alignment_id 0 ;
843       // Query DDB_G0269124
844       // Align 11270 143 120
845       // corresponds to : 120 bases align at pos 143 in protein to 11270 on
846       // dna in strand direction
847       // Align 11150 187 282
848       // corresponds to : 282 bases align at pos 187 in protein to 11150 on
849       // dna in strand direction
850       //
851       // Align 10865 281 888
852       // Align 9977 578 1068
853       // Align 8909 935 375
854       //
855       if (ints.size() != 3)
856       {
857         throw new InvalidGFF3FieldException(attr, set,
858                 "Invalid number of fields for this attribute ("
859                         + ints.size() + ")");
860       }
861       fromrange.add(new Integer(ints.get(0).intValue()));
862       fromrange.add(new Integer(ints.get(0).intValue() + strand
863               * ints.get(2).intValue()));
864       // how are intron/exon boundaries that do not align in codons
865       // represented
866       if (ints.get(1).equals(lastppos) && lastpframe > 0)
867       {
868         // extend existing to map
869         lastppos += ints.get(2) / 3;
870         lastpframe = ints.get(2) % 3;
871         torange.set(torange.size() - 1, new Integer(lastppos));
872       }
873       else
874       {
875         // new to map range
876         torange.add(ints.get(1));
877         lastppos = ints.get(1) + ints.get(2) / 3;
878         lastpframe = ints.get(2) % 3;
879         torange.add(new Integer(lastppos));
880       }
881     }
882     // from and to ranges must end up being a series of start/end intervals
883     if (fromrange.size() % 2 == 1)
884     {
885       throw new InvalidGFF3FieldException(attr, set,
886               "Couldn't parse the DNA alignment range correctly");
887     }
888     if (torange.size() % 2 == 1)
889     {
890       throw new InvalidGFF3FieldException(attr, set,
891               "Couldn't parse the protein alignment range correctly");
892     }
893     // finally, build the map
894     int[] frommap = new int[fromrange.size()], tomap = new int[torange
895             .size()];
896     int p = 0;
897     for (Integer ip : fromrange)
898     {
899       frommap[p++] = ip.intValue();
900     }
901     p = 0;
902     for (Integer ip : torange)
903     {
904       tomap[p++] = ip.intValue();
905     }
906
907     return new MapList(frommap, tomap, 3, 1);
908   }
909
910   private List<SequenceI> findNames(AlignmentI align,
911           List<SequenceI> newseqs, boolean relaxedIdMatching,
912           List<String> list)
913   {
914     List<SequenceI> found = new ArrayList<SequenceI>();
915     for (String seqId : list)
916     {
917       SequenceI seq = findName(align, seqId, relaxedIdMatching, newseqs);
918       if (seq != null)
919       {
920         found.add(seq);
921       }
922     }
923     return found;
924   }
925
926   private AlignmentI lastmatchedAl = null;
927
928   private SequenceIdMatcher matcher = null;
929
930   /**
931    * clear any temporary handles used to speed up ID matching
932    */
933   private void resetMatcher()
934   {
935     lastmatchedAl = null;
936     matcher = null;
937   }
938
939   private SequenceI findName(AlignmentI align, String seqId,
940           boolean relaxedIdMatching, List<SequenceI> newseqs)
941   {
942     SequenceI match = null;
943     if (relaxedIdMatching)
944     {
945       if (lastmatchedAl != align)
946       {
947         matcher = new SequenceIdMatcher(
948                 (lastmatchedAl = align).getSequencesArray());
949         if (newseqs != null)
950         {
951           matcher.addAll(newseqs);
952         }
953       }
954       match = matcher.findIdMatch(seqId);
955     }
956     else
957     {
958       match = align.findName(seqId, true);
959       
960     }
961     if (match==null && newseqs!=null)
962     {
963       match = new SequenceDummy(seqId);
964       if (relaxedIdMatching)
965       {
966         matcher.addAll(Arrays.asList(new SequenceI[]
967         { match }));
968       }
969     }
970     return match;
971   }
972   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
973   {
974     if (sf.getDescription() == null)
975     {
976       return;
977     }
978     jalview.util.ParseHtmlBodyAndLinks parsed = new jalview.util.ParseHtmlBodyAndLinks(
979             sf.getDescription(), removeHTML, newline);
980
981     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
982             : sf.description;
983     for (String link : parsed.getLinks())
984     {
985       sf.addLink(link);
986     }
987
988   }
989
990   /**
991    * generate a features file for seqs includes non-pos features by default.
992    * 
993    * @param seqs
994    *          source of sequence features
995    * @param visible
996    *          hash of feature types and colours
997    * @return features file contents
998    */
999   public String printJalviewFormat(SequenceI[] seqs, Map<String,Object> visible)
1000   {
1001     return printJalviewFormat(seqs, visible, true, true);
1002   }
1003
1004   /**
1005    * generate a features file for seqs with colours from visible (if any)
1006    * 
1007    * @param seqs
1008    *          source of features
1009    * @param visible
1010    *          hash of Colours for each feature type
1011    * @param visOnly
1012    *          when true only feature types in 'visible' will be output
1013    * @param nonpos
1014    *          indicates if non-positional features should be output (regardless
1015    *          of group or type)
1016    * @return features file contents
1017    */
1018   public String printJalviewFormat(SequenceI[] seqs, Map visible,
1019           boolean visOnly, boolean nonpos)
1020   {
1021     StringBuffer out = new StringBuffer();
1022     SequenceFeature[] next;
1023     boolean featuresGen = false;
1024     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
1025     {
1026       // no point continuing.
1027       return "No Features Visible";
1028     }
1029
1030     if (visible != null && visOnly)
1031     {
1032       // write feature colours only if we're given them and we are generating
1033       // viewed features
1034       // TODO: decide if feature links should also be written here ?
1035       Iterator en = visible.keySet().iterator();
1036       String type, color;
1037       while (en.hasNext())
1038       {
1039         type = en.next().toString();
1040
1041         if (visible.get(type) instanceof GraduatedColor)
1042         {
1043           GraduatedColor gc = (GraduatedColor) visible.get(type);
1044           color = (gc.isColourByLabel() ? "label|" : "")
1045                   + Format.getHexString(gc.getMinColor()) + "|"
1046                   + Format.getHexString(gc.getMaxColor())
1047                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
1048                   + gc.getMax() + "|";
1049           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
1050           {
1051             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
1052             {
1053               color += "below";
1054             }
1055             else
1056             {
1057               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
1058               {
1059                 System.err.println("WARNING: Unsupported threshold type ("
1060                         + gc.getThreshType() + ") : Assuming 'above'");
1061               }
1062               color += "above";
1063             }
1064             // add the value
1065             color += "|" + gc.getThresh();
1066           }
1067           else
1068           {
1069             color += "none";
1070           }
1071         }
1072         else if (visible.get(type) instanceof java.awt.Color)
1073         {
1074           color = Format.getHexString((java.awt.Color) visible.get(type));
1075         }
1076         else
1077         {
1078           // legacy support for integer objects containing colour triplet values
1079           color = Format.getHexString(new java.awt.Color(Integer
1080                   .parseInt(visible.get(type).toString())));
1081         }
1082         out.append(type);
1083         out.append("\t");
1084         out.append(color);
1085         out.append(newline);
1086       }
1087     }
1088     // Work out which groups are both present and visible
1089     Vector groups = new Vector();
1090     int groupIndex = 0;
1091     boolean isnonpos = false;
1092
1093     for (int i = 0; i < seqs.length; i++)
1094     {
1095       next = seqs[i].getSequenceFeatures();
1096       if (next != null)
1097       {
1098         for (int j = 0; j < next.length; j++)
1099         {
1100           isnonpos = next[j].begin == 0 && next[j].end == 0;
1101           if ((!nonpos && isnonpos)
1102                   || (!isnonpos && visOnly && !visible
1103                           .containsKey(next[j].type)))
1104           {
1105             continue;
1106           }
1107
1108           if (next[j].featureGroup != null
1109                   && !groups.contains(next[j].featureGroup))
1110           {
1111             groups.addElement(next[j].featureGroup);
1112           }
1113         }
1114       }
1115     }
1116
1117     String group = null;
1118     do
1119     {
1120
1121       if (groups.size() > 0 && groupIndex < groups.size())
1122       {
1123         group = groups.elementAt(groupIndex).toString();
1124         out.append(newline);
1125         out.append("STARTGROUP\t");
1126         out.append(group);
1127         out.append(newline);
1128       }
1129       else
1130       {
1131         group = null;
1132       }
1133
1134       for (int i = 0; i < seqs.length; i++)
1135       {
1136         next = seqs[i].getSequenceFeatures();
1137         if (next != null)
1138         {
1139           for (int j = 0; j < next.length; j++)
1140           {
1141             isnonpos = next[j].begin == 0 && next[j].end == 0;
1142             if ((!nonpos && isnonpos)
1143                     || (!isnonpos && visOnly && !visible
1144                             .containsKey(next[j].type)))
1145             {
1146               // skip if feature is nonpos and we ignore them or if we only
1147               // output visible and it isn't non-pos and it's not visible
1148               continue;
1149             }
1150
1151             if (group != null
1152                     && (next[j].featureGroup == null || !next[j].featureGroup
1153                             .equals(group)))
1154             {
1155               continue;
1156             }
1157
1158             if (group == null && next[j].featureGroup != null)
1159             {
1160               continue;
1161             }
1162             // we have features to output
1163             featuresGen = true;
1164             if (next[j].description == null
1165                     || next[j].description.equals(""))
1166             {
1167               out.append(next[j].type + "\t");
1168             }
1169             else
1170             {
1171               if (next[j].links != null
1172                       && next[j].getDescription().indexOf("<html>") == -1)
1173               {
1174                 out.append("<html>");
1175               }
1176
1177               out.append(next[j].description + " ");
1178               if (next[j].links != null)
1179               {
1180                 for (int l = 0; l < next[j].links.size(); l++)
1181                 {
1182                   String label = next[j].links.elementAt(l).toString();
1183                   String href = label.substring(label.indexOf("|") + 1);
1184                   label = label.substring(0, label.indexOf("|"));
1185
1186                   if (next[j].description.indexOf(href) == -1)
1187                   {
1188                     out.append("<a href=\"" + href + "\">" + label + "</a>");
1189                   }
1190                 }
1191
1192                 if (next[j].getDescription().indexOf("</html>") == -1)
1193                 {
1194                   out.append("</html>");
1195                 }
1196               }
1197
1198               out.append("\t");
1199             }
1200             out.append(seqs[i].getName());
1201             out.append("\t-1\t");
1202             out.append(next[j].begin);
1203             out.append("\t");
1204             out.append(next[j].end);
1205             out.append("\t");
1206             out.append(next[j].type);
1207             if (next[j].score != Float.NaN)
1208             {
1209               out.append("\t");
1210               out.append(next[j].score);
1211             }
1212             out.append(newline);
1213           }
1214         }
1215       }
1216
1217       if (group != null)
1218       {
1219         out.append("ENDGROUP\t");
1220         out.append(group);
1221         out.append(newline);
1222         groupIndex++;
1223       }
1224       else
1225       {
1226         break;
1227       }
1228
1229     } while (groupIndex < groups.size() + 1);
1230
1231     if (!featuresGen)
1232     {
1233       return "No Features Visible";
1234     }
1235
1236     return out.toString();
1237   }
1238
1239   /**
1240    * generate a gff file for sequence features includes non-pos features by
1241    * default.
1242    * 
1243    * @param seqs
1244    * @param visible
1245    * @return
1246    */
1247   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible)
1248   {
1249     return printGFFFormat(seqs, visible, true, true);
1250   }
1251
1252   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible,
1253           boolean visOnly, boolean nonpos)
1254   {
1255     StringBuffer out = new StringBuffer();
1256     SequenceFeature[] next;
1257     String source;
1258     boolean isnonpos;
1259     for (int i = 0; i < seqs.length; i++)
1260     {
1261       if (seqs[i].getSequenceFeatures() != null)
1262       {
1263         next = seqs[i].getSequenceFeatures();
1264         for (int j = 0; j < next.length; j++)
1265         {
1266           isnonpos = next[j].begin == 0 && next[j].end == 0;
1267           if ((!nonpos && isnonpos)
1268                   || (!isnonpos && visOnly && !visible
1269                           .containsKey(next[j].type)))
1270           {
1271             continue;
1272           }
1273
1274           source = next[j].featureGroup;
1275           if (source == null)
1276           {
1277             source = next[j].getDescription();
1278           }
1279
1280           out.append(seqs[i].getName());
1281           out.append("\t");
1282           out.append(source);
1283           out.append("\t");
1284           out.append(next[j].type);
1285           out.append("\t");
1286           out.append(next[j].begin);
1287           out.append("\t");
1288           out.append(next[j].end);
1289           out.append("\t");
1290           out.append(next[j].score);
1291           out.append("\t");
1292
1293           if (next[j].getValue("STRAND") != null)
1294           {
1295             out.append(next[j].getValue("STRAND"));
1296             out.append("\t");
1297           }
1298           else
1299           {
1300             out.append(".\t");
1301           }
1302
1303           if (next[j].getValue("FRAME") != null)
1304           {
1305             out.append(next[j].getValue("FRAME"));
1306           }
1307           else
1308           {
1309             out.append(".");
1310           }
1311           // TODO: verify/check GFF - should there be a /t here before attribute
1312           // output ?
1313
1314           if (next[j].getValue("ATTRIBUTES") != null)
1315           {
1316             out.append(next[j].getValue("ATTRIBUTES"));
1317           }
1318
1319           out.append(newline);
1320
1321         }
1322       }
1323     }
1324
1325     return out.toString();
1326   }
1327
1328   /**
1329    * this is only for the benefit of object polymorphism - method does nothing.
1330    */
1331   public void parse()
1332   {
1333     // IGNORED
1334   }
1335
1336   /**
1337    * this is only for the benefit of object polymorphism - method does nothing.
1338    * 
1339    * @return error message
1340    */
1341   public String print()
1342   {
1343     return "USE printGFFFormat() or printJalviewFormat()";
1344   }
1345
1346 }