JAL-1723 tooltip dbrefs sorted and limited to two per source
[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.SequenceFeature;
25 import jalview.datamodel.SequenceI;
26 import jalview.util.UrlLink;
27
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.Comparator;
32 import java.util.Hashtable;
33 import java.util.List;
34
35 /**
36  * generate HTML reports for a sequence
37  * 
38  * @author jimp
39  */
40 public class SequenceAnnotationReport
41 {
42   final String linkImageURL;
43
44   /*
45    * Comparator to order DBRefEntry by Source + accession id (case-insensitive)
46    */
47   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
48   {
49     @Override
50     public int compare(DBRefEntry ref1, DBRefEntry ref2)
51     {
52       String s1 = ref1.getSource();
53       String s2 = ref2.getSource();
54       int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
55               .compareToIgnoreCase(s2));
56       if (comp == 0)
57       {
58         String a1 = ref1.getAccessionId();
59         String a2 = ref2.getAccessionId();
60         comp = a1 == null ? -1 : (a2 == null ? 1 : a1
61                 .compareToIgnoreCase(a2));
62       }
63       return comp;
64     }
65   };
66
67   public SequenceAnnotationReport(String linkImageURL)
68   {
69     this.linkImageURL = linkImageURL;
70   }
71
72   /**
73    * appends the features at rpos to the given stringbuffer ready for display in
74    * a tooltip
75    * 
76    * @param tooltipText
77    * @param linkImageURL
78    * @param rpos
79    * @param features
80    * @param minmax
81    *          TODO refactor to Jalview 'utilities' somehow.
82    */
83   public void appendFeatures(final StringBuilder tooltipText, int rpos,
84           List<SequenceFeature> features, Hashtable minmax)
85   {
86     String tmpString;
87     if (features != null)
88     {
89       for (SequenceFeature feature : features)
90       {
91         if (feature.getType().equals("disulfide bond"))
92         {
93           if (feature.getBegin() == rpos || feature.getEnd() == rpos)
94           {
95             if (tooltipText.length() > 6)
96             {
97               tooltipText.append("<br>");
98             }
99             tooltipText.append("disulfide bond " + feature.getBegin()
100                     + ":" + feature.getEnd());
101           }
102         }
103         else
104         {
105           if (tooltipText.length() > 6)
106           {
107             tooltipText.append("<br>");
108           }
109           // TODO: remove this hack to display link only features
110           boolean linkOnly = feature.getValue("linkonly") != null;
111           if (!linkOnly)
112           {
113             tooltipText.append(feature.getType() + " ");
114             if (rpos != 0)
115             {
116               // we are marking a positional feature
117               tooltipText.append(feature.begin);
118             }
119             if (feature.begin != feature.end)
120             {
121               tooltipText.append(" " + feature.end);
122             }
123
124             if (feature.getDescription() != null
125                     && !feature.description.equals(feature.getType()))
126             {
127               tmpString = feature.getDescription();
128               String tmp2up = tmpString.toUpperCase();
129               int startTag = tmp2up.indexOf("<HTML>");
130               if (startTag > -1)
131               {
132                 tmpString = tmpString.substring(startTag + 6);
133                 tmp2up = tmp2up.substring(startTag + 6);
134               }
135               int endTag = tmp2up.indexOf("</BODY>");
136               if (endTag > -1)
137               {
138                 tmpString = tmpString.substring(0, endTag);
139                 tmp2up = tmp2up.substring(0, endTag);
140               }
141               endTag = tmp2up.indexOf("</HTML>");
142               if (endTag > -1)
143               {
144                 tmpString = tmpString.substring(0, endTag);
145               }
146
147               if (startTag > -1)
148               {
149                 tooltipText.append("; " + tmpString);
150               }
151               else
152               {
153                 if (tmpString.indexOf("<") > -1
154                         || tmpString.indexOf(">") > -1)
155                 {
156                   // The description does not specify html is to
157                   // be used, so we must remove < > symbols
158                   tmpString = tmpString.replaceAll("<", "&lt;");
159                   tmpString = tmpString.replaceAll(">", "&gt;");
160
161                   tooltipText.append("; ");
162                   tooltipText.append(tmpString);
163
164                 }
165                 else
166                 {
167                   tooltipText.append("; " + tmpString);
168                 }
169               }
170             }
171             // check score should be shown
172             if (!Float.isNaN(feature.getScore()))
173             {
174               float[][] rng = (minmax == null) ? null : ((float[][]) minmax
175                       .get(feature.getType()));
176               if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
177               {
178                 tooltipText.append(" Score=" + feature.getScore());
179               }
180             }
181             if (feature.getValue("status") != null)
182             {
183               String status = feature.getValue("status").toString();
184               if (status.length() > 0)
185               {
186                 tooltipText.append("; (" + feature.getValue("status")
187                         + ")");
188               }
189             }
190           }
191         }
192         if (feature.links != null)
193         {
194           if (linkImageURL != null)
195           {
196             tooltipText.append(" <img src=\"" + linkImageURL + "\">");
197           }
198           else
199           {
200             for (String urlstring : feature.links)
201             {
202               try
203               {
204                 for (String[] urllink : createLinksFrom(null, urlstring))
205                 {
206                   tooltipText.append("<br/> <a href=\""
207                           + urllink[3]
208                           + "\" target=\""
209                           + urllink[0]
210                           + "\">"
211                           + (urllink[0].toLowerCase().equals(
212                                   urllink[1].toLowerCase()) ? urllink[0]
213                                   : (urllink[0] + ":" + urllink[1]))
214                           + "</a></br>");
215                 }
216               } catch (Exception x)
217               {
218                 System.err.println("problem when creating links from "
219                         + urlstring);
220                 x.printStackTrace();
221               }
222             }
223           }
224
225         }
226       }
227     }
228   }
229
230   /**
231    * 
232    * @param seq
233    * @param link
234    * @return String[][] { String[] { link target, link label, dynamic component
235    *         inserted (if any), url }}
236    */
237   public String[][] createLinksFrom(SequenceI seq, String link)
238   {
239     ArrayList<String[]> urlSets = new ArrayList<String[]>();
240     ArrayList<String> uniques = new ArrayList<String>();
241     UrlLink urlLink = new UrlLink(link);
242     if (!urlLink.isValid())
243     {
244       System.err.println(urlLink.getInvalidMessage());
245       return null;
246     }
247     final String target = urlLink.getTarget(); // link.substring(0,
248     // link.indexOf("|"));
249     final String label = urlLink.getLabel();
250     if (seq != null && urlLink.isDynamic())
251     {
252
253       // collect matching db-refs
254       DBRefEntry[] dbr = jalview.util.DBRefUtils.selectRefs(seq.getDBRefs(),
255               new String[] { target });
256       // collect id string too
257       String id = seq.getName();
258       String descr = seq.getDescription();
259       if (descr != null && descr.length() < 1)
260       {
261         descr = null;
262       }
263       if (dbr != null)
264       {
265         for (int r = 0; r < dbr.length; r++)
266         {
267           if (id != null && dbr[r].getAccessionId().equals(id))
268           {
269             // suppress duplicate link creation for the bare sequence ID
270             // string with this link
271             id = null;
272           }
273           // create Bare ID link for this RUL
274           String[] urls = urlLink.makeUrls(dbr[r].getAccessionId(), true);
275           if (urls != null)
276           {
277             for (int u = 0; u < urls.length; u += 2)
278             {
279               String unq = urls[u] + "|" + urls[u + 1];
280               if (!uniques.contains(unq))
281               {
282                 urlSets.add(new String[] { target, label, urls[u],
283                     urls[u + 1] });
284                 uniques.add(unq);
285               }
286             }
287           }
288         }
289       }
290       if (id != null)
291       {
292         // create Bare ID link for this RUL
293         String[] urls = urlLink.makeUrls(id, true);
294         if (urls != null)
295         {
296           for (int u = 0; u < urls.length; u += 2)
297           {
298             String unq = urls[u] + "|" + urls[u + 1];
299             if (!uniques.contains(unq))
300             {
301               urlSets.add(new String[] { target, label, urls[u],
302                   urls[u + 1] });
303               uniques.add(unq);
304             }
305           }
306         }
307       }
308       if (descr != null && urlLink.getRegexReplace() != null)
309       {
310         // create link for this URL from description only if regex matches
311         String[] urls = urlLink.makeUrls(descr, true);
312         if (urls != null)
313         {
314           for (int u = 0; u < urls.length; u += 2)
315           {
316             String unq = urls[u] + "|" + urls[u + 1];
317             if (!uniques.contains(unq))
318             {
319               urlSets.add(new String[] { target, label, urls[u],
320                   urls[u + 1] });
321               uniques.add(unq);
322             }
323           }
324         }
325       }
326
327     }
328     else
329     {
330       String unq = label + "|" + urlLink.getUrl_prefix();
331       if (!uniques.contains(unq))
332       {
333         uniques.add(unq);
334         // Add a non-dynamic link
335         urlSets.add(new String[] { target, label, null,
336             urlLink.getUrl_prefix() });
337       }
338     }
339
340     return urlSets.toArray(new String[][] {});
341   }
342
343   public void createTooltipAnnotationReport(final StringBuilder tip,
344           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
345           Hashtable minmax)
346   {
347     int maxWidth = createSequenceAnnotationReport(tip, sequence,
348             showDbRefs, showNpFeats, minmax, true);
349
350     if (maxWidth > 60)
351     {
352       tip.insert(0, "<table width=350 border=0><tr><td><i>");
353       tip.append("</i></td></tr></table>");
354     }
355   }
356
357   public int createSequenceAnnotationReport(final StringBuilder tip,
358           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
359           Hashtable minmax)
360   {
361     return createSequenceAnnotationReport(tip, sequence, showDbRefs,
362             showNpFeats, minmax, false);
363   }
364
365   /**
366    * Adds an html-formatted sequence annotation report to the provided string
367    * buffer, and returns the longest line length added
368    * 
369    * @param sb
370    * @param sequence
371    * @param showDbRefs
372    *          if true, include database references
373    * @param showNpFeats
374    *          if true, include non-positional sequence features
375    * @param minmax
376    * @param summary
377    *          if true, build a shortened summary report (for tooltip)
378    * @return
379    */
380   int createSequenceAnnotationReport(final StringBuilder sb,
381           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
382           Hashtable minmax, boolean summary)
383   {
384     String tmp;
385     sb.append("<i>");
386
387     int maxWidth = 0;
388     if (sequence.getDescription() != null)
389     {
390       tmp = sequence.getDescription();
391       sb.append("<br>").append(tmp);
392       maxWidth = Math.max(maxWidth, tmp.length());
393     }
394     SequenceI ds = sequence;
395     while (ds.getDatasetSequence() != null)
396     {
397       ds = ds.getDatasetSequence();
398     }
399     DBRefEntry[] dbrefs = ds.getDBRefs();
400     Arrays.sort(dbrefs, comparator);
401     if (showDbRefs && dbrefs != null)
402     {
403       boolean ellipsis = false;
404       String lastSource = null;
405       int countForSource = 0;
406       for (DBRefEntry ref : dbrefs)
407       {
408         String source = ref.getSource();
409         if (source == null)
410         {
411           // shouldn't happen
412           continue;
413         }
414         boolean sourceChanged = !source.equals(lastSource);
415         if (sourceChanged)
416         {
417           countForSource = 0;
418         }
419         lastSource = source;
420         countForSource++;
421         if (countForSource == 1 || !summary)
422         {
423           sb.append("<br>");
424         }
425         if (countForSource < 3 || !summary)
426         {
427           String accessionId = ref.getAccessionId();
428           int len = accessionId.length() + 1;
429           if (countForSource > 1 && summary)
430           {
431             sb.append(", ").append(accessionId);
432             len++;
433           }
434           else
435           {
436             sb.append(source).append(" ").append(accessionId);
437             len += source.length();
438           }
439           maxWidth = Math.max(maxWidth, len);
440         }
441         if (countForSource == 3 && summary)
442         {
443           sb.append(", ...");
444           ellipsis = true;
445         }
446       }
447       if (ellipsis) {
448         sb.append("<br>(Output Sequence Details to list all database references)");
449       }
450     }
451
452     // ADD NON POSITIONAL SEQUENCE INFO
453     SequenceFeature[] features = sequence.getSequenceFeatures();
454     if (showNpFeats && features != null)
455     {
456       for (int i = 0; i < features.length; i++)
457       {
458         if (features[i].begin == 0 && features[i].end == 0)
459         {
460           int sz = -sb.length();
461           List<SequenceFeature> tfeat = Collections
462                   .singletonList(features[i]);
463           appendFeatures(sb, 0, tfeat, minmax);
464           sz += sb.length();
465           maxWidth = Math.max(maxWidth, sz);
466         }
467       }
468     }
469     return maxWidth;
470   }
471 }