JAL-1683 replace year/version strings with tokens in source
[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 java.io.*;
24 import java.util.*;
25
26 import jalview.analysis.SequenceIdMatcher;
27 import jalview.datamodel.*;
28 import jalview.schemes.*;
29 import jalview.util.Format;
30
31 /**
32  * Parse and create Jalview Features files Detects GFF format features files and
33  * parses. Does not implement standard print() - call specific printFeatures or
34  * printGFF. Uses AlignmentI.findSequence(String id) to find the sequence object
35  * for the features annotation - this normally works on an exact match.
36  * 
37  * @author AMW
38  * @version $Revision$
39  */
40 public class FeaturesFile extends AlignFile
41 {
42   /**
43    * work around for GFF interpretation bug where source string becomes
44    * description rather than a group
45    */
46   private boolean doGffSource = true;
47
48   /**
49    * Creates a new FeaturesFile object.
50    */
51   public FeaturesFile()
52   {
53   }
54
55   /**
56    * Creates a new FeaturesFile object.
57    * 
58    * @param inFile
59    *          DOCUMENT ME!
60    * @param type
61    *          DOCUMENT ME!
62    * 
63    * @throws IOException
64    *           DOCUMENT ME!
65    */
66   public FeaturesFile(String inFile, String type) throws IOException
67   {
68     super(inFile, type);
69   }
70
71   public FeaturesFile(FileParse source) throws IOException
72   {
73     super(source);
74   }
75
76   /**
77    * Parse GFF or sequence features file using case-independent matching,
78    * discarding URLs
79    * 
80    * @param align
81    *          - alignment/dataset containing sequences that are to be annotated
82    * @param colours
83    *          - hashtable to store feature colour definitions
84    * @param removeHTML
85    *          - process html strings into plain text
86    * @return true if features were added
87    */
88   public boolean parse(AlignmentI align, Hashtable colours,
89           boolean removeHTML)
90   {
91     return parse(align, colours, null, removeHTML, false);
92   }
93
94   /**
95    * Parse GFF or sequence features file optionally using case-independent
96    * matching, discarding URLs
97    * 
98    * @param align
99    *          - alignment/dataset containing sequences that are to be annotated
100    * @param colours
101    *          - hashtable to store feature colour definitions
102    * @param removeHTML
103    *          - process html strings into plain text
104    * @param relaxedIdmatching
105    *          - when true, ID matches to compound sequence IDs are allowed
106    * @return true if features were added
107    */
108   public boolean parse(AlignmentI align, Map colours, boolean removeHTML,
109           boolean relaxedIdMatching)
110   {
111     return parse(align, colours, null, removeHTML, relaxedIdMatching);
112   }
113
114   /**
115    * Parse GFF or sequence features file optionally using case-independent
116    * matching
117    * 
118    * @param align
119    *          - alignment/dataset containing sequences that are to be annotated
120    * @param colours
121    *          - hashtable to store feature colour definitions
122    * @param featureLink
123    *          - hashtable to store associated URLs
124    * @param removeHTML
125    *          - process html strings into plain text
126    * @return true if features were added
127    */
128   public boolean parse(AlignmentI align, Map colours, Map featureLink,
129           boolean removeHTML)
130   {
131     return parse(align, colours, featureLink, removeHTML, false);
132   }
133
134   /**
135    * Parse GFF or sequence features file
136    * 
137    * @param align
138    *          - alignment/dataset containing sequences that are to be annotated
139    * @param colours
140    *          - hashtable to store feature colour definitions
141    * @param featureLink
142    *          - hashtable to store associated URLs
143    * @param removeHTML
144    *          - process html strings into plain text
145    * @param relaxedIdmatching
146    *          - when true, ID matches to compound sequence IDs are allowed
147    * @return true if features were added
148    */
149   public boolean parse(AlignmentI align, Map colours, Map featureLink,
150           boolean removeHTML, boolean relaxedIdmatching)
151   {
152
153     String line = null;
154     try
155     {
156       SequenceI seq = null;
157       String type, desc, token = null;
158
159       int index, start, end;
160       float score;
161       StringTokenizer st;
162       SequenceFeature sf;
163       String featureGroup = null, groupLink = null;
164       Map typeLink = new Hashtable();
165       /**
166        * when true, assume GFF style features rather than Jalview style.
167        */
168       boolean GFFFile = true;
169       while ((line = nextLine()) != null)
170       {
171         if (line.startsWith("#"))
172         {
173           continue;
174         }
175
176         st = new StringTokenizer(line, "\t");
177         if (st.countTokens() == 1)
178         {
179           if (line.trim().equalsIgnoreCase("GFF"))
180           {
181             // Start parsing file as if it might be GFF again.
182             GFFFile = true;
183             continue;
184           }
185         }
186         if (st.countTokens() > 1 && st.countTokens() < 4)
187         {
188           GFFFile = false;
189           type = st.nextToken();
190           if (type.equalsIgnoreCase("startgroup"))
191           {
192             featureGroup = st.nextToken();
193             if (st.hasMoreElements())
194             {
195               groupLink = st.nextToken();
196               featureLink.put(featureGroup, groupLink);
197             }
198           }
199           else if (type.equalsIgnoreCase("endgroup"))
200           {
201             // We should check whether this is the current group,
202             // but at present theres no way of showing more than 1 group
203             st.nextToken();
204             featureGroup = null;
205             groupLink = null;
206           }
207           else
208           {
209             Object colour = null;
210             String colscheme = st.nextToken();
211             if (colscheme.indexOf("|") > -1
212                     || colscheme.trim().equalsIgnoreCase("label"))
213             {
214               // Parse '|' separated graduated colourscheme fields:
215               // [label|][mincolour|maxcolour|[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue]
216               // can either provide 'label' only, first is optional, next two
217               // colors are required (but may be
218               // left blank), next is optional, nxt two min/max are required.
219               // first is either 'label'
220               // first/second and third are both hexadecimal or word equivalent
221               // colour.
222               // next two are values parsed as floats.
223               // fifth is either 'above','below', or 'none'.
224               // sixth is a float value and only required when fifth is either
225               // 'above' or 'below'.
226               StringTokenizer gcol = new StringTokenizer(colscheme, "|",
227                       true);
228               // set defaults
229               int threshtype = AnnotationColourGradient.NO_THRESHOLD;
230               float min = Float.MIN_VALUE, max = Float.MAX_VALUE, threshval = Float.NaN;
231               boolean labelCol = false;
232               // Parse spec line
233               String mincol = gcol.nextToken();
234               if (mincol == "|")
235               {
236                 System.err
237                         .println("Expected either 'label' or a colour specification in the line: "
238                                 + line);
239                 continue;
240               }
241               String maxcol = null;
242               if (mincol.toLowerCase().indexOf("label") == 0)
243               {
244                 labelCol = true;
245                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null); // skip
246                                                                            // '|'
247                 mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
248               }
249               String abso = null, minval, maxval;
250               if (mincol != null)
251               {
252                 // at least four more tokens
253                 if (mincol.equals("|"))
254                 {
255                   mincol = "";
256                 }
257                 else
258                 {
259                   gcol.nextToken(); // skip next '|'
260                 }
261                 // continue parsing rest of line
262                 maxcol = gcol.nextToken();
263                 if (maxcol.equals("|"))
264                 {
265                   maxcol = "";
266                 }
267                 else
268                 {
269                   gcol.nextToken(); // skip next '|'
270                 }
271                 abso = gcol.nextToken();
272                 gcol.nextToken(); // skip next '|'
273                 if (abso.toLowerCase().indexOf("abso") != 0)
274                 {
275                   minval = abso;
276                   abso = null;
277                 }
278                 else
279                 {
280                   minval = gcol.nextToken();
281                   gcol.nextToken(); // skip next '|'
282                 }
283                 maxval = gcol.nextToken();
284                 if (gcol.hasMoreTokens())
285                 {
286                   gcol.nextToken(); // skip next '|'
287                 }
288                 try
289                 {
290                   if (minval.length() > 0)
291                   {
292                     min = new Float(minval).floatValue();
293                   }
294                 } catch (Exception e)
295                 {
296                   System.err
297                           .println("Couldn't parse the minimum value for graduated colour for type ("
298                                   + colscheme
299                                   + ") - did you misspell 'auto' for the optional automatic colour switch ?");
300                   e.printStackTrace();
301                 }
302                 try
303                 {
304                   if (maxval.length() > 0)
305                   {
306                     max = new Float(maxval).floatValue();
307                   }
308                 } catch (Exception e)
309                 {
310                   System.err
311                           .println("Couldn't parse the maximum value for graduated colour for type ("
312                                   + colscheme + ")");
313                   e.printStackTrace();
314                 }
315               }
316               else
317               {
318                 // add in some dummy min/max colours for the label-only
319                 // colourscheme.
320                 mincol = "FFFFFF";
321                 maxcol = "000000";
322               }
323               try
324               {
325                 colour = new jalview.schemes.GraduatedColor(
326                         new UserColourScheme(mincol).findColour('A'),
327                         new UserColourScheme(maxcol).findColour('A'), min,
328                         max);
329               } catch (Exception e)
330               {
331                 System.err
332                         .println("Couldn't parse the graduated colour scheme ("
333                                 + colscheme + ")");
334                 e.printStackTrace();
335               }
336               if (colour != null)
337               {
338                 ((jalview.schemes.GraduatedColor) colour)
339                         .setColourByLabel(labelCol);
340                 ((jalview.schemes.GraduatedColor) colour)
341                         .setAutoScaled(abso == null);
342                 // add in any additional parameters
343                 String ttype = null, tval = null;
344                 if (gcol.hasMoreTokens())
345                 {
346                   // threshold type and possibly a threshold value
347                   ttype = gcol.nextToken();
348                   if (ttype.toLowerCase().startsWith("below"))
349                   {
350                     ((jalview.schemes.GraduatedColor) colour)
351                             .setThreshType(AnnotationColourGradient.BELOW_THRESHOLD);
352                   }
353                   else if (ttype.toLowerCase().startsWith("above"))
354                   {
355                     ((jalview.schemes.GraduatedColor) colour)
356                             .setThreshType(AnnotationColourGradient.ABOVE_THRESHOLD);
357                   }
358                   else
359                   {
360                     ((jalview.schemes.GraduatedColor) colour)
361                             .setThreshType(AnnotationColourGradient.NO_THRESHOLD);
362                     if (!ttype.toLowerCase().startsWith("no"))
363                     {
364                       System.err
365                               .println("Ignoring unrecognised threshold type : "
366                                       + ttype);
367                     }
368                   }
369                 }
370                 if (((GraduatedColor) colour).getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
371                 {
372                   try
373                   {
374                     gcol.nextToken();
375                     tval = gcol.nextToken();
376                     ((jalview.schemes.GraduatedColor) colour)
377                             .setThresh(new Float(tval).floatValue());
378                   } catch (Exception e)
379                   {
380                     System.err
381                             .println("Couldn't parse threshold value as a float: ("
382                                     + tval + ")");
383                     e.printStackTrace();
384                   }
385                 }
386                 // parse the thresh-is-min token ?
387                 if (gcol.hasMoreTokens())
388                 {
389                   System.err
390                           .println("Ignoring additional tokens in parameters in graduated colour specification\n");
391                   while (gcol.hasMoreTokens())
392                   {
393                     System.err.println("|" + gcol.nextToken());
394                   }
395                   System.err.println("\n");
396                 }
397               }
398             }
399             else
400             {
401               UserColourScheme ucs = new UserColourScheme(colscheme);
402               colour = ucs.findColour('A');
403             }
404             if (colour != null)
405             {
406               colours.put(type, colour);
407             }
408             if (st.hasMoreElements())
409             {
410               String link = st.nextToken();
411               typeLink.put(type, link);
412               if (featureLink == null)
413               {
414                 featureLink = new Hashtable();
415               }
416               featureLink.put(type, link);
417             }
418           }
419           continue;
420         }
421         String seqId = "";
422         while (st.hasMoreElements())
423         {
424
425           if (GFFFile)
426           {
427             // Still possible this is an old Jalview file,
428             // which does not have type colours at the beginning
429             seqId = token = st.nextToken();
430             seq = findName(align, seqId, relaxedIdmatching);
431             if (seq != null)
432             {
433               desc = st.nextToken();
434               String group = null;
435               if (doGffSource && desc.indexOf(' ') == -1)
436               {
437                 // could also be a source term rather than description line
438                 group = new String(desc);
439               }
440               type = st.nextToken();
441               try
442               {
443                 String stt = st.nextToken();
444                 if (stt.length() == 0 || stt.equals("-"))
445                 {
446                   start = 0;
447                 }
448                 else
449                 {
450                   start = Integer.parseInt(stt);
451                 }
452               } catch (NumberFormatException ex)
453               {
454                 start = 0;
455               }
456               try
457               {
458                 String stt = st.nextToken();
459                 if (stt.length() == 0 || stt.equals("-"))
460                 {
461                   end = 0;
462                 }
463                 else
464                 {
465                   end = Integer.parseInt(stt);
466                 }
467               } catch (NumberFormatException ex)
468               {
469                 end = 0;
470               }
471               // TODO: decide if non positional feature assertion for input data
472               // where end==0 is generally valid
473               if (end == 0)
474               {
475                 // treat as non-positional feature, regardless.
476                 start = 0;
477               }
478               try
479               {
480                 score = new Float(st.nextToken()).floatValue();
481               } catch (NumberFormatException ex)
482               {
483                 score = 0;
484               }
485
486               sf = new SequenceFeature(type, desc, start, end, score, group);
487
488               try
489               {
490                 sf.setValue("STRAND", st.nextToken());
491                 sf.setValue("FRAME", st.nextToken());
492               } catch (Exception ex)
493               {
494               }
495
496               if (st.hasMoreTokens())
497               {
498                 StringBuffer attributes = new StringBuffer();
499                 while (st.hasMoreTokens())
500                 {
501                   attributes.append("\t" + st.nextElement());
502                 }
503                 // TODO validate and split GFF2 attributes field ? parse out
504                 // ([A-Za-z][A-Za-z0-9_]*) <value> ; and add as
505                 // sf.setValue(attrib, val);
506                 sf.setValue("ATTRIBUTES", attributes.toString());
507               }
508
509               seq.addSequenceFeature(sf);
510               while ((seq = align.findName(seq, seqId, true)) != null)
511               {
512                 seq.addSequenceFeature(new SequenceFeature(sf));
513               }
514               break;
515             }
516           }
517
518           if (GFFFile && seq == null)
519           {
520             desc = token;
521           }
522           else
523           {
524             desc = st.nextToken();
525           }
526           if (!st.hasMoreTokens())
527           {
528             System.err
529                     .println("DEBUG: Run out of tokens when trying to identify the destination for the feature.. giving up.");
530             // in all probability, this isn't a file we understand, so bail
531             // quietly.
532             return false;
533           }
534
535           token = st.nextToken();
536
537           if (!token.equals("ID_NOT_SPECIFIED"))
538           {
539             seq = findName(align, seqId = token, relaxedIdmatching);
540             st.nextToken();
541           }
542           else
543           {
544             seqId = null;
545             try
546             {
547               index = Integer.parseInt(st.nextToken());
548               seq = align.getSequenceAt(index);
549             } catch (NumberFormatException ex)
550             {
551               seq = null;
552             }
553           }
554
555           if (seq == null)
556           {
557             System.out.println("Sequence not found: " + line);
558             break;
559           }
560
561           start = Integer.parseInt(st.nextToken());
562           end = Integer.parseInt(st.nextToken());
563
564           type = st.nextToken();
565
566           if (!colours.containsKey(type))
567           {
568             // Probably the old style groups file
569             UserColourScheme ucs = new UserColourScheme(type);
570             colours.put(type, ucs.findColour('A'));
571           }
572           sf = new SequenceFeature(type, desc, "", start, end, featureGroup);
573           if (st.hasMoreTokens())
574           {
575             try
576             {
577               score = new Float(st.nextToken()).floatValue();
578               // update colourgradient bounds if allowed to
579             } catch (NumberFormatException ex)
580             {
581               score = 0;
582             }
583             sf.setScore(score);
584           }
585           if (groupLink != null && removeHTML)
586           {
587             sf.addLink(groupLink);
588             sf.description += "%LINK%";
589           }
590           if (typeLink.containsKey(type) && removeHTML)
591           {
592             sf.addLink(typeLink.get(type).toString());
593             sf.description += "%LINK%";
594           }
595
596           parseDescriptionHTML(sf, removeHTML);
597
598           seq.addSequenceFeature(sf);
599
600           while (seqId != null
601                   && (seq = align.findName(seq, seqId, false)) != null)
602           {
603             seq.addSequenceFeature(new SequenceFeature(sf));
604           }
605           // If we got here, its not a GFFFile
606           GFFFile = false;
607         }
608       }
609       resetMatcher();
610     } catch (Exception ex)
611     {
612       System.out.println("Error parsing feature file: " + ex + "\n" + line);
613       ex.printStackTrace(System.err);
614       resetMatcher();
615       return false;
616     }
617
618     return true;
619   }
620
621   private AlignmentI lastmatchedAl = null;
622
623   private SequenceIdMatcher matcher = null;
624
625   /**
626    * clear any temporary handles used to speed up ID matching
627    */
628   private void resetMatcher()
629   {
630     lastmatchedAl = null;
631     matcher = null;
632   }
633
634   private SequenceI findName(AlignmentI align, String seqId,
635           boolean relaxedIdMatching)
636   {
637     SequenceI match = null;
638     if (relaxedIdMatching)
639     {
640       if (lastmatchedAl != align)
641       {
642         matcher = new SequenceIdMatcher(
643                 (lastmatchedAl = align).getSequencesArray());
644       }
645       match = matcher.findIdMatch(seqId);
646     }
647     else
648     {
649       match = align.findName(seqId, true);
650     }
651     return match;
652   }
653
654   public void parseDescriptionHTML(SequenceFeature sf, boolean removeHTML)
655   {
656     if (sf.getDescription() == null)
657     {
658       return;
659     }
660     jalview.util.ParseHtmlBodyAndLinks parsed = new jalview.util.ParseHtmlBodyAndLinks(
661             sf.getDescription(), removeHTML, newline);
662
663     sf.description = (removeHTML) ? parsed.getNonHtmlContent()
664             : sf.description;
665     for (String link : parsed.getLinks())
666     {
667       sf.addLink(link);
668     }
669
670   }
671
672   /**
673    * generate a features file for seqs includes non-pos features by default.
674    * 
675    * @param seqs
676    *          source of sequence features
677    * @param visible
678    *          hash of feature types and colours
679    * @return features file contents
680    */
681   public String printJalviewFormat(SequenceI[] seqs, Map<String,Object> visible)
682   {
683     return printJalviewFormat(seqs, visible, true, true);
684   }
685
686   /**
687    * generate a features file for seqs with colours from visible (if any)
688    * 
689    * @param seqs
690    *          source of features
691    * @param visible
692    *          hash of Colours for each feature type
693    * @param visOnly
694    *          when true only feature types in 'visible' will be output
695    * @param nonpos
696    *          indicates if non-positional features should be output (regardless
697    *          of group or type)
698    * @return features file contents
699    */
700   public String printJalviewFormat(SequenceI[] seqs, Map visible,
701           boolean visOnly, boolean nonpos)
702   {
703     StringBuffer out = new StringBuffer();
704     SequenceFeature[] next;
705     boolean featuresGen = false;
706     if (visOnly && !nonpos && (visible == null || visible.size() < 1))
707     {
708       // no point continuing.
709       return "No Features Visible";
710     }
711
712     if (visible != null && visOnly)
713     {
714       // write feature colours only if we're given them and we are generating
715       // viewed features
716       // TODO: decide if feature links should also be written here ?
717       Iterator en = visible.keySet().iterator();
718       String type, color;
719       while (en.hasNext())
720       {
721         type = en.next().toString();
722
723         if (visible.get(type) instanceof GraduatedColor)
724         {
725           GraduatedColor gc = (GraduatedColor) visible.get(type);
726           color = (gc.isColourByLabel() ? "label|" : "")
727                   + Format.getHexString(gc.getMinColor()) + "|"
728                   + Format.getHexString(gc.getMaxColor())
729                   + (gc.isAutoScale() ? "|" : "|abso|") + gc.getMin() + "|"
730                   + gc.getMax() + "|";
731           if (gc.getThreshType() != AnnotationColourGradient.NO_THRESHOLD)
732           {
733             if (gc.getThreshType() == AnnotationColourGradient.BELOW_THRESHOLD)
734             {
735               color += "below";
736             }
737             else
738             {
739               if (gc.getThreshType() != AnnotationColourGradient.ABOVE_THRESHOLD)
740               {
741                 System.err.println("WARNING: Unsupported threshold type ("
742                         + gc.getThreshType() + ") : Assuming 'above'");
743               }
744               color += "above";
745             }
746             // add the value
747             color += "|" + gc.getThresh();
748           }
749           else
750           {
751             color += "none";
752           }
753         }
754         else if (visible.get(type) instanceof java.awt.Color)
755         {
756           color = Format.getHexString((java.awt.Color) visible.get(type));
757         }
758         else
759         {
760           // legacy support for integer objects containing colour triplet values
761           color = Format.getHexString(new java.awt.Color(Integer
762                   .parseInt(visible.get(type).toString())));
763         }
764         out.append(type);
765         out.append("\t");
766         out.append(color);
767         out.append(newline);
768       }
769     }
770     // Work out which groups are both present and visible
771     Vector groups = new Vector();
772     int groupIndex = 0;
773     boolean isnonpos = false;
774
775     for (int i = 0; i < seqs.length; i++)
776     {
777       next = seqs[i].getSequenceFeatures();
778       if (next != null)
779       {
780         for (int j = 0; j < next.length; j++)
781         {
782           isnonpos = next[j].begin == 0 && next[j].end == 0;
783           if ((!nonpos && isnonpos)
784                   || (!isnonpos && visOnly && !visible
785                           .containsKey(next[j].type)))
786           {
787             continue;
788           }
789
790           if (next[j].featureGroup != null
791                   && !groups.contains(next[j].featureGroup))
792           {
793             groups.addElement(next[j].featureGroup);
794           }
795         }
796       }
797     }
798
799     String group = null;
800     do
801     {
802
803       if (groups.size() > 0 && groupIndex < groups.size())
804       {
805         group = groups.elementAt(groupIndex).toString();
806         out.append(newline);
807         out.append("STARTGROUP\t");
808         out.append(group);
809         out.append(newline);
810       }
811       else
812       {
813         group = null;
814       }
815
816       for (int i = 0; i < seqs.length; i++)
817       {
818         next = seqs[i].getSequenceFeatures();
819         if (next != null)
820         {
821           for (int j = 0; j < next.length; j++)
822           {
823             isnonpos = next[j].begin == 0 && next[j].end == 0;
824             if ((!nonpos && isnonpos)
825                     || (!isnonpos && visOnly && !visible
826                             .containsKey(next[j].type)))
827             {
828               // skip if feature is nonpos and we ignore them or if we only
829               // output visible and it isn't non-pos and it's not visible
830               continue;
831             }
832
833             if (group != null
834                     && (next[j].featureGroup == null || !next[j].featureGroup
835                             .equals(group)))
836             {
837               continue;
838             }
839
840             if (group == null && next[j].featureGroup != null)
841             {
842               continue;
843             }
844             // we have features to output
845             featuresGen = true;
846             if (next[j].description == null
847                     || next[j].description.equals(""))
848             {
849               out.append(next[j].type + "\t");
850             }
851             else
852             {
853               if (next[j].links != null
854                       && next[j].getDescription().indexOf("<html>") == -1)
855               {
856                 out.append("<html>");
857               }
858
859               out.append(next[j].description + " ");
860               if (next[j].links != null)
861               {
862                 for (int l = 0; l < next[j].links.size(); l++)
863                 {
864                   String label = next[j].links.elementAt(l).toString();
865                   String href = label.substring(label.indexOf("|") + 1);
866                   label = label.substring(0, label.indexOf("|"));
867
868                   if (next[j].description.indexOf(href) == -1)
869                   {
870                     out.append("<a href=\"" + href + "\">" + label + "</a>");
871                   }
872                 }
873
874                 if (next[j].getDescription().indexOf("</html>") == -1)
875                 {
876                   out.append("</html>");
877                 }
878               }
879
880               out.append("\t");
881             }
882             out.append(seqs[i].getName());
883             out.append("\t-1\t");
884             out.append(next[j].begin);
885             out.append("\t");
886             out.append(next[j].end);
887             out.append("\t");
888             out.append(next[j].type);
889             if (next[j].score != Float.NaN)
890             {
891               out.append("\t");
892               out.append(next[j].score);
893             }
894             out.append(newline);
895           }
896         }
897       }
898
899       if (group != null)
900       {
901         out.append("ENDGROUP\t");
902         out.append(group);
903         out.append(newline);
904         groupIndex++;
905       }
906       else
907       {
908         break;
909       }
910
911     } while (groupIndex < groups.size() + 1);
912
913     if (!featuresGen)
914     {
915       return "No Features Visible";
916     }
917
918     return out.toString();
919   }
920
921   /**
922    * generate a gff file for sequence features includes non-pos features by
923    * default.
924    * 
925    * @param seqs
926    * @param visible
927    * @return
928    */
929   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible)
930   {
931     return printGFFFormat(seqs, visible, true, true);
932   }
933
934   public String printGFFFormat(SequenceI[] seqs, Map<String,Object> visible,
935           boolean visOnly, boolean nonpos)
936   {
937     StringBuffer out = new StringBuffer();
938     SequenceFeature[] next;
939     String source;
940     boolean isnonpos;
941     for (int i = 0; i < seqs.length; i++)
942     {
943       if (seqs[i].getSequenceFeatures() != null)
944       {
945         next = seqs[i].getSequenceFeatures();
946         for (int j = 0; j < next.length; j++)
947         {
948           isnonpos = next[j].begin == 0 && next[j].end == 0;
949           if ((!nonpos && isnonpos)
950                   || (!isnonpos && visOnly && !visible
951                           .containsKey(next[j].type)))
952           {
953             continue;
954           }
955
956           source = next[j].featureGroup;
957           if (source == null)
958           {
959             source = next[j].getDescription();
960           }
961
962           out.append(seqs[i].getName());
963           out.append("\t");
964           out.append(source);
965           out.append("\t");
966           out.append(next[j].type);
967           out.append("\t");
968           out.append(next[j].begin);
969           out.append("\t");
970           out.append(next[j].end);
971           out.append("\t");
972           out.append(next[j].score);
973           out.append("\t");
974
975           if (next[j].getValue("STRAND") != null)
976           {
977             out.append(next[j].getValue("STRAND"));
978             out.append("\t");
979           }
980           else
981           {
982             out.append(".\t");
983           }
984
985           if (next[j].getValue("FRAME") != null)
986           {
987             out.append(next[j].getValue("FRAME"));
988           }
989           else
990           {
991             out.append(".");
992           }
993           // TODO: verify/check GFF - should there be a /t here before attribute
994           // output ?
995
996           if (next[j].getValue("ATTRIBUTES") != null)
997           {
998             out.append(next[j].getValue("ATTRIBUTES"));
999           }
1000
1001           out.append(newline);
1002
1003         }
1004       }
1005     }
1006
1007     return out.toString();
1008   }
1009
1010   /**
1011    * this is only for the benefit of object polymorphism - method does nothing.
1012    */
1013   public void parse()
1014   {
1015     // IGNORED
1016   }
1017
1018   /**
1019    * this is only for the benefit of object polymorphism - method does nothing.
1020    * 
1021    * @return error message
1022    */
1023   public String print()
1024   {
1025     return "USE printGFFFormat() or printJalviewFormat()";
1026   }
1027
1028 }