JAL-2738 copy to spikes/mungo
[jalview.git] / src / jalview / io / SequenceAnnotationReport.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.io;
22
23 import jalview.datamodel.DBRefEntry;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.datamodel.SequenceI;
27 import jalview.io.gff.GffConstants;
28 import jalview.util.MessageManager;
29 import jalview.util.UrlLink;
30
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Comparator;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Map;
37
38 /**
39  * generate HTML reports for a sequence
40  * 
41  * @author jimp
42  */
43 public class SequenceAnnotationReport
44 {
45   private static final String COMMA = ",";
46
47   private static final String ELLIPSIS = "...";
48
49   private static final int MAX_REFS_PER_SOURCE = 4;
50
51   private static final int MAX_SOURCES = 40;
52
53   private static final String[][] PRIMARY_SOURCES = new String[][] {
54       DBRefSource.CODINGDBS, DBRefSource.DNACODINGDBS,
55       DBRefSource.PROTEINDBS };
56
57   final String linkImageURL;
58
59   /*
60    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
61    * with 'Primary' sources placed before others
62    */
63   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
64   {
65
66     @Override
67     public int compare(DBRefEntry ref1, DBRefEntry ref2)
68     {
69       String s1 = ref1.getSource();
70       String s2 = ref2.getSource();
71       boolean s1Primary = isPrimarySource(s1);
72       boolean s2Primary = isPrimarySource(s2);
73       if (s1Primary && !s2Primary)
74       {
75         return -1;
76       }
77       if (!s1Primary && s2Primary)
78       {
79         return 1;
80       }
81       int comp = s1 == null ? -1
82               : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
83       if (comp == 0)
84       {
85         String a1 = ref1.getAccessionId();
86         String a2 = ref2.getAccessionId();
87         comp = a1 == null ? -1
88                 : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
89       }
90       return comp;
91     }
92
93     private boolean isPrimarySource(String source)
94     {
95       for (String[] primary : PRIMARY_SOURCES)
96       {
97         for (String s : primary)
98         {
99           if (source.equals(s))
100           {
101             return true;
102           }
103         }
104       }
105       return false;
106     }
107   };
108
109   public SequenceAnnotationReport(String linkImageURL)
110   {
111     this.linkImageURL = linkImageURL;
112   }
113
114   /**
115    * Append text for the list of features to the tooltip
116    * 
117    * @param sb
118    * @param rpos
119    * @param features
120    * @param minmax
121    */
122   public void appendFeatures(final StringBuilder sb, int rpos,
123           List<SequenceFeature> features, Map<String, float[][]> minmax)
124   {
125     if (features != null)
126     {
127       for (SequenceFeature feature : features)
128       {
129         appendFeature(sb, rpos, minmax, feature);
130       }
131     }
132   }
133
134   /**
135    * Appends the feature at rpos to the given buffer
136    * 
137    * @param sb
138    * @param rpos
139    * @param minmax
140    * @param feature
141    */
142   void appendFeature(final StringBuilder sb, int rpos,
143           Map<String, float[][]> minmax, SequenceFeature feature)
144   {
145     if (feature.isContactFeature())
146     {
147       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
148       {
149         if (sb.length() > 6)
150         {
151           sb.append("<br>");
152         }
153         sb.append(feature.getType()).append(" ").append(feature.getBegin())
154                 .append(":").append(feature.getEnd());
155       }
156     }
157     else
158     {
159       if (sb.length() > 6)
160       {
161         sb.append("<br>");
162       }
163       // TODO: remove this hack to display link only features
164       boolean linkOnly = feature.getValue("linkonly") != null;
165       if (!linkOnly)
166       {
167         sb.append(feature.getType()).append(" ");
168         if (rpos != 0)
169         {
170           // we are marking a positional feature
171           sb.append(feature.begin);
172         }
173         if (feature.begin != feature.end)
174         {
175           sb.append(" ").append(feature.end);
176         }
177
178         if (feature.getDescription() != null
179                 && !feature.description.equals(feature.getType()))
180         {
181           String tmpString = feature.getDescription();
182           String tmp2up = tmpString.toUpperCase();
183           int startTag = tmp2up.indexOf("<HTML>");
184           if (startTag > -1)
185           {
186             tmpString = tmpString.substring(startTag + 6);
187             tmp2up = tmp2up.substring(startTag + 6);
188           }
189           int endTag = tmp2up.indexOf("</BODY>");
190           if (endTag > -1)
191           {
192             tmpString = tmpString.substring(0, endTag);
193             tmp2up = tmp2up.substring(0, endTag);
194           }
195           endTag = tmp2up.indexOf("</HTML>");
196           if (endTag > -1)
197           {
198             tmpString = tmpString.substring(0, endTag);
199           }
200
201           if (startTag > -1)
202           {
203             sb.append("; ").append(tmpString);
204           }
205           else
206           {
207             if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
208             {
209               // The description does not specify html is to
210               // be used, so we must remove < > symbols
211               tmpString = tmpString.replaceAll("<", "&lt;");
212               tmpString = tmpString.replaceAll(">", "&gt;");
213
214               sb.append("; ");
215               sb.append(tmpString);
216             }
217             else
218             {
219               sb.append("; ").append(tmpString);
220             }
221           }
222         }
223         // check score should be shown
224         if (!Float.isNaN(feature.getScore()))
225         {
226           float[][] rng = (minmax == null) ? null
227                   : minmax.get(feature.getType());
228           if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
229           {
230             sb.append(" Score=").append(String.valueOf(feature.getScore()));
231           }
232         }
233         String status = (String) feature.getValue("status");
234         if (status != null && status.length() > 0)
235         {
236           sb.append("; (").append(status).append(")");
237         }
238         String clinSig = (String) feature
239                 .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
240         if (clinSig != null)
241         {
242           sb.append("; ").append(clinSig);
243         }
244       }
245     }
246   }
247
248   /**
249    * Format and appends any hyperlinks for the sequence feature to the string
250    * buffer
251    * 
252    * @param sb
253    * @param feature
254    */
255   void appendLinks(final StringBuffer sb, SequenceFeature feature)
256   {
257     if (feature.links != null)
258     {
259       if (linkImageURL != null)
260       {
261         sb.append(" <img src=\"" + linkImageURL + "\">");
262       }
263       else
264       {
265         for (String urlstring : feature.links)
266         {
267           try
268           {
269             for (List<String> urllink : createLinksFrom(null, urlstring))
270             {
271               sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
272                       + urllink.get(0) + "\">"
273                       + (urllink.get(0).toLowerCase()
274                               .equals(urllink.get(1).toLowerCase())
275                                       ? urllink.get(0)
276                                       : (urllink.get(0) + ":"
277                                               + urllink.get(1)))
278                       + "</a></br>");
279             }
280           } catch (Exception x)
281           {
282             System.err.println(
283                     "problem when creating links from " + urlstring);
284             x.printStackTrace();
285           }
286         }
287       }
288
289     }
290   }
291
292   /**
293    * 
294    * @param seq
295    * @param link
296    * @return Collection< List<String> > { List<String> { link target, link
297    *         label, dynamic component inserted (if any), url }}
298    */
299   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
300   {
301     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
302     UrlLink urlLink = new UrlLink(link);
303     if (!urlLink.isValid())
304     {
305       System.err.println(urlLink.getInvalidMessage());
306       return null;
307     }
308
309     urlLink.createLinksFromSeq(seq, urlSets);
310
311     return urlSets.values();
312   }
313
314   public void createSequenceAnnotationReport(final StringBuilder tip,
315           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
316           Map<String, float[][]> minmax)
317   {
318     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
319             minmax, false);
320   }
321
322   /**
323    * Builds an html formatted report of sequence details and appends it to the
324    * provided buffer.
325    * 
326    * @param sb
327    *          buffer to append report to
328    * @param sequence
329    *          the sequence the report is for
330    * @param showDbRefs
331    *          whether to include database references for the sequence
332    * @param showNpFeats
333    *          whether to include non-positional sequence features
334    * @param minmax
335    * @param summary
336    * @return
337    */
338   int createSequenceAnnotationReport(final StringBuilder sb,
339           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
340           Map<String, float[][]> minmax, boolean summary)
341   {
342     String tmp;
343     sb.append("<i>");
344
345     int maxWidth = 0;
346     if (sequence.getDescription() != null)
347     {
348       tmp = sequence.getDescription();
349       sb.append("<br>").append(tmp);
350       maxWidth = Math.max(maxWidth, tmp.length());
351     }
352     SequenceI ds = sequence;
353     while (ds.getDatasetSequence() != null)
354     {
355       ds = ds.getDatasetSequence();
356     }
357     
358     if (showDbRefs)
359     {
360       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
361     }
362
363     /*
364      * add non-positional features if wanted
365      */
366     if (showNpFeats)
367     {
368       for (SequenceFeature sf : sequence.getFeatures()
369               .getNonPositionalFeatures())
370       {
371         int sz = -sb.length();
372         appendFeature(sb, 0, minmax, sf);
373         sz += sb.length();
374         maxWidth = Math.max(maxWidth, sz);
375       }
376     }
377     sb.append("</i>");
378     return maxWidth;
379   }
380
381   /**
382    * A helper method that appends any DBRefs, returning the maximum line length
383    * added
384    * 
385    * @param sb
386    * @param ds
387    * @param summary
388    * @return
389    */
390   protected int appendDbRefs(final StringBuilder sb, SequenceI ds,
391           boolean summary)
392   {
393     DBRefEntry[] dbrefs = ds.getDBRefs();
394     if (dbrefs == null)
395     {
396       return 0;
397     }
398
399     // note this sorts the refs held on the sequence!
400     Arrays.sort(dbrefs, comparator);
401     boolean ellipsis = false;
402     String source = null;
403     String lastSource = null;
404     int countForSource = 0;
405     int sourceCount = 0;
406     boolean moreSources = false;
407     int maxLineLength = 0;
408     int lineLength = 0;
409
410     for (DBRefEntry ref : dbrefs)
411     {
412       source = ref.getSource();
413       if (source == null)
414       {
415         // shouldn't happen
416         continue;
417       }
418       boolean sourceChanged = !source.equals(lastSource);
419       if (sourceChanged)
420       {
421         lineLength = 0;
422         countForSource = 0;
423         sourceCount++;
424       }
425       if (sourceCount > MAX_SOURCES && summary)
426       {
427         ellipsis = true;
428         moreSources = true;
429         break;
430       }
431       lastSource = source;
432       countForSource++;
433       if (countForSource == 1 || !summary)
434       {
435         sb.append("<br>");
436       }
437       if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
438       {
439         String accessionId = ref.getAccessionId();
440         lineLength += accessionId.length() + 1;
441         if (countForSource > 1 && summary)
442         {
443           sb.append(", ").append(accessionId);
444           lineLength++;
445         }
446         else
447         {
448           sb.append(source).append(" ").append(accessionId);
449           lineLength += source.length();
450         }
451         maxLineLength = Math.max(maxLineLength, lineLength);
452       }
453       if (countForSource == MAX_REFS_PER_SOURCE && summary)
454       {
455         sb.append(COMMA).append(ELLIPSIS);
456         ellipsis = true;
457       }
458     }
459     if (moreSources)
460     {
461       sb.append("<br>").append(source)
462               .append(COMMA).append(ELLIPSIS);
463     }
464     if (ellipsis)
465     {
466       sb.append("<br>(");
467       sb.append(MessageManager.getString("label.output_seq_details"));
468       sb.append(")");
469     }
470
471     return maxLineLength;
472   }
473
474   public void createTooltipAnnotationReport(final StringBuilder tip,
475           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
476           Map<String, float[][]> minmax)
477   {
478     int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
479             showNpFeats, minmax, true);
480
481     if (maxWidth > 60)
482     {
483       // ? not sure this serves any useful purpose
484       // tip.insert(0, "<table width=350 border=0><tr><td>");
485       // tip.append("</td></tr></table>");
486     }
487   }
488 }