Merge branch 'develop' into bug/JAL-2323disulfideOverview
[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    */
62   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
63   {
64
65     @Override
66     public int compare(DBRefEntry ref1, DBRefEntry ref2)
67     {
68       String s1 = ref1.getSource();
69       String s2 = ref2.getSource();
70       boolean s1Primary = isPrimarySource(s1);
71       boolean s2Primary = isPrimarySource(s2);
72       if (s1Primary && !s2Primary)
73       {
74         return -1;
75       }
76       if (!s1Primary && s2Primary)
77       {
78         return 1;
79       }
80       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
81               .compareToIgnoreCase(s2));
82       if (comp == 0)
83       {
84         String a1 = ref1.getAccessionId();
85         String a2 = ref2.getAccessionId();
86         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
87                 .compareToIgnoreCase(a2));
88       }
89       return comp;
90     }
91
92     private boolean isPrimarySource(String source)
93     {
94       for (String[] primary : PRIMARY_SOURCES)
95       {
96         for (String s : primary)
97         {
98           if (source.equals(s))
99           {
100             return true;
101           }
102         }
103       }
104       return false;
105     }
106   };
107
108   public SequenceAnnotationReport(String linkImageURL)
109   {
110     this.linkImageURL = linkImageURL;
111   }
112
113   /**
114    * Append text for the list of features to the tooltip
115    * 
116    * @param sb
117    * @param rpos
118    * @param features
119    * @param minmax
120    */
121   public void appendFeatures(final StringBuilder sb, int rpos,
122           List<SequenceFeature> features, Map<String, float[][]> minmax)
123   {
124     if (features != null)
125     {
126       for (SequenceFeature feature : features)
127       {
128         appendFeature(sb, rpos, minmax, feature);
129       }
130     }
131   }
132
133   /**
134    * Appends the feature at rpos to the given buffer
135    * 
136    * @param sb
137    * @param rpos
138    * @param minmax
139    * @param feature
140    */
141   void appendFeature(final StringBuilder sb, int rpos,
142           Map<String, float[][]> minmax, SequenceFeature feature)
143   {
144     if (feature.isContactFeature())
145     {
146       if (feature.getBegin() == rpos || feature.getEnd() == rpos)
147       {
148         if (sb.length() > 6)
149         {
150           sb.append("<br>");
151         }
152         sb.append(feature.getType()).append(" ").append(feature.getBegin())
153                 .append(":")
154                 .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 : minmax.get(feature
227                   .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=\""
272                       + urllink.get(3)
273                       + "\" target=\""
274                       + urllink.get(0)
275                       + "\">"
276                       + (urllink.get(0).toLowerCase()
277                               .equals(urllink.get(1).toLowerCase()) ? urllink
278                               .get(0) : (urllink.get(0) + ":" + urllink
279                               .get(1)))
280                       + "</a></br>");
281             }
282           } catch (Exception x)
283           {
284             System.err.println("problem when creating links from "
285                     + urlstring);
286             x.printStackTrace();
287           }
288         }
289       }
290
291     }
292   }
293
294   /**
295    * 
296    * @param seq
297    * @param link
298    * @return Collection< List<String> > { List<String> { link target, link
299    *         label, dynamic component inserted (if any), url }}
300    */
301   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
302   {
303     Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
304     UrlLink urlLink = new UrlLink(link);
305     if (!urlLink.isValid())
306     {
307       System.err.println(urlLink.getInvalidMessage());
308       return null;
309     }
310
311     urlLink.createLinksFromSeq(seq, urlSets);
312
313     return urlSets.values();
314   }
315
316   public void createSequenceAnnotationReport(final StringBuilder tip,
317           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
318           Map<String, float[][]> minmax)
319   {
320     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
321             minmax, false);
322   }
323
324   /**
325    * Builds an html formatted report of sequence details and appends it to the
326    * provided buffer.
327    * 
328    * @param sb
329    *          buffer to append report to
330    * @param sequence
331    *          the sequence the report is for
332    * @param showDbRefs
333    *          whether to include database references for the sequence
334    * @param showNpFeats
335    *          whether to include non-positional sequence features
336    * @param minmax
337    * @param summary
338    * @return
339    */
340   int createSequenceAnnotationReport(final StringBuilder sb,
341           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
342           Map<String, float[][]> minmax, boolean summary)
343   {
344     String tmp;
345     sb.append("<i>");
346
347     int maxWidth = 0;
348     if (sequence.getDescription() != null)
349     {
350       tmp = sequence.getDescription();
351       sb.append("<br>").append(tmp);
352       maxWidth = Math.max(maxWidth, tmp.length());
353     }
354     SequenceI ds = sequence;
355     while (ds.getDatasetSequence() != null)
356     {
357       ds = ds.getDatasetSequence();
358     }
359     DBRefEntry[] dbrefs = ds.getDBRefs();
360     if (showDbRefs && dbrefs != null)
361     {
362       // note this sorts the refs held on the sequence!
363       Arrays.sort(dbrefs, comparator);
364       boolean ellipsis = false;
365       String source = null;
366       String lastSource = null;
367       int countForSource = 0;
368       int sourceCount = 0;
369       boolean moreSources = false;
370       int lineLength = 0;
371
372       for (DBRefEntry ref : dbrefs)
373       {
374         source = ref.getSource();
375         if (source == null)
376         {
377           // shouldn't happen
378           continue;
379         }
380         boolean sourceChanged = !source.equals(lastSource);
381         if (sourceChanged)
382         {
383           lineLength = 0;
384           countForSource = 0;
385           sourceCount++;
386         }
387         if (sourceCount > MAX_SOURCES && summary)
388         {
389           ellipsis = true;
390           moreSources = true;
391           break;
392         }
393         lastSource = source;
394         countForSource++;
395         if (countForSource == 1 || !summary)
396         {
397           sb.append("<br>");
398         }
399         if (countForSource <= MAX_REFS_PER_SOURCE || !summary)
400         {
401           String accessionId = ref.getAccessionId();
402           lineLength += accessionId.length() + 1;
403           if (countForSource > 1 && summary)
404           {
405             sb.append(", ").append(accessionId);
406             lineLength++;
407           }
408           else
409           {
410             sb.append(source).append(" ").append(accessionId);
411             lineLength += source.length();
412           }
413           maxWidth = Math.max(maxWidth, lineLength);
414         }
415         if (countForSource == MAX_REFS_PER_SOURCE && summary)
416         {
417           sb.append(COMMA).append(ELLIPSIS);
418           ellipsis = true;
419         }
420       }
421       if (moreSources)
422       {
423         sb.append("<br>").append(ELLIPSIS).append(COMMA).append(source)
424                 .append(COMMA).append(ELLIPSIS);
425       }
426       if (ellipsis)
427       {
428         sb.append("<br>(");
429         sb.append(MessageManager.getString("label.output_seq_details"));
430         sb.append(")");
431       }
432     }
433
434     /*
435      * add non-positional features if wanted
436      */
437     SequenceFeature[] features = sequence.getSequenceFeatures();
438     if (showNpFeats && features != null)
439     {
440       for (int i = 0; i < features.length; i++)
441       {
442         if (features[i].begin == 0 && features[i].end == 0)
443         {
444           int sz = -sb.length();
445           appendFeature(sb, 0, minmax, features[i]);
446           sz += sb.length();
447           maxWidth = Math.max(maxWidth, sz);
448         }
449       }
450     }
451     sb.append("</i>");
452     return maxWidth;
453   }
454
455   public void createTooltipAnnotationReport(final StringBuilder tip,
456           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
457           Map<String, float[][]> minmax)
458   {
459     int maxWidth = createSequenceAnnotationReport(tip, sequence,
460             showDbRefs, showNpFeats, minmax, true);
461
462     if (maxWidth > 60)
463     {
464       // ? not sure this serves any useful purpose
465       // tip.insert(0, "<table width=350 border=0><tr><td>");
466       // tip.append("</td></tr></table>");
467     }
468   }
469 }