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