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