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