Merge branch 'releases/Release_2_11_3_Branch'
[jalview.git] / src / jalview / ext / ensembl / EnsemblMap.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.ext.ensembl;
22
23 import jalview.datamodel.AlignmentI;
24 import jalview.datamodel.DBRefSource;
25 import jalview.datamodel.GeneLociI;
26 import jalview.datamodel.GeneLocus;
27 import jalview.datamodel.Mapping;
28 import jalview.util.MapList;
29
30 import java.io.IOException;
31 import java.net.MalformedURLException;
32 import java.net.URL;
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38
39 import org.json.simple.parser.ParseException;
40
41 /**
42  * A client for the Ensembl REST service /map endpoint, to convert from
43  * coordinates of one genome assembly to another.
44  * <p>
45  * Note that species and assembly identifiers passed to this class must be valid
46  * in Ensembl. They are not case sensitive.
47  * 
48  * @author gmcarstairs
49  * @see https://rest.ensembl.org/documentation/info/assembly_map
50  * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml
51  * @see https://rest.ensembl.org/info/species?content-type=text/xml
52  */
53 public class EnsemblMap extends EnsemblRestClient
54 {
55   private static final String MAPPED = "mapped";
56
57   private static final String MAPPINGS = "mappings";
58
59   private static final String CDS = "cds";
60
61   private static final String CDNA = "cdna";
62
63   /**
64    * Default constructor (to use rest.ensembl.org)
65    */
66   public EnsemblMap()
67   {
68     super();
69   }
70
71   /**
72    * Constructor given the target domain to fetch data from
73    * 
74    * @param
75    */
76   public EnsemblMap(String domain)
77   {
78     super(domain);
79   }
80
81   @Override
82   public String getDbName()
83   {
84     return DBRefSource.ENSEMBL;
85   }
86
87   @Override
88   public AlignmentI getSequenceRecords(String queries) throws Exception
89   {
90     return null; // not used
91   }
92
93   /**
94    * Constructs a URL of the format <code>
95    * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
96    * </code>
97    * 
98    * @param species
99    * @param chromosome
100    * @param fromRef
101    * @param toRef
102    * @param startPos
103    * @param endPos
104    * @return
105    * @throws MalformedURLException
106    */
107   protected URL getAssemblyMapUrl(String species, String chromosome,
108           String fromRef, String toRef, int startPos, int endPos)
109           throws MalformedURLException
110   {
111     /*
112      * start-end might be reverse strand - present forwards to the service
113      */
114     boolean forward = startPos <= endPos;
115     int start = forward ? startPos : endPos;
116     int end = forward ? endPos : startPos;
117     String strand = forward ? "1" : "-1";
118     String url = String.format(
119             "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
120             getDomain(), species, fromRef, chromosome, start, end, strand,
121             toRef);
122     return new URL(url);
123   }
124
125   @Override
126   protected boolean useGetRequest()
127   {
128     return true;
129   }
130
131   @Override
132   protected URL getUrl(List<String> ids) throws MalformedURLException
133   {
134     return null; // not used
135   }
136
137   /**
138    * Calls the REST /map service to get the chromosomal coordinates (start/end)
139    * in 'toRef' that corresponding to the (start/end) queryRange in 'fromRef'
140    * 
141    * @param species
142    * @param chromosome
143    * @param fromRef
144    * @param toRef
145    * @param queryRange
146    * @return
147    * @see http://rest.ensemblgenomes.org/documentation/info/assembly_map
148    */
149   public int[] getAssemblyMapping(String species, String chromosome,
150           String fromRef, String toRef, int[] queryRange)
151   {
152     URL url = null;
153     try
154     {
155       url = getAssemblyMapUrl(species, chromosome, fromRef, toRef,
156               queryRange[0], queryRange[1]);
157       return (parseAssemblyMappingResponse(url));
158     } catch (Throwable t)
159     {
160       jalview.bin.Console
161               .outPrintln("Error calling " + url + ": " + t.getMessage());
162       return null;
163     }
164   }
165
166   /**
167    * Parses the JSON response from the /map/&lt;species&gt;/ REST service. The
168    * format is (with some fields omitted)
169    * 
170    * <pre>
171    *  {"mappings": 
172    *    [{
173    *       "original": {"end":45109016,"start":45051610},
174    *       "mapped"  : {"end":43186384,"start":43128978} 
175    *  }] }
176    * </pre>
177    * 
178    * @param br
179    * @return
180    */
181   @SuppressWarnings("unchecked")
182   protected int[] parseAssemblyMappingResponse(URL url)
183   {
184     int[] result = null;
185
186     try
187     {
188       Iterator<Object> rvals = (Iterator<Object>) getJSON(url, null, -1,
189               MODE_ITERATOR, MAPPINGS);
190       if (rvals == null)
191       {
192         return null;
193       }
194       while (rvals.hasNext())
195       {
196         // todo check for "mapped"
197         Map<String, Object> val = (Map<String, Object>) rvals.next();
198         Map<String, Object> mapped = (Map<String, Object>) val.get(MAPPED);
199         int start = Integer.parseInt(mapped.get("start").toString());
200         int end = Integer.parseInt(mapped.get("end").toString());
201         String strand = mapped.get("strand").toString();
202         if ("1".equals(strand))
203         {
204           result = new int[] { start, end };
205         }
206         else
207         {
208           result = new int[] { end, start };
209         }
210       }
211     } catch (IOException | ParseException | NumberFormatException e)
212     {
213       // ignore
214     }
215     return result;
216   }
217
218   /**
219    * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
220    * returned chromosomal coordinates, or returns null if the call fails
221    * 
222    * @param division
223    *          e.g. Ensembl, EnsemblMetazoa
224    * @param accession
225    *          e.g. ENST00000592782, Y55B1AR.1.1
226    * @param start
227    * @param end
228    * @return
229    */
230   public GeneLociI getCdsMapping(String division, String accession,
231           int start, int end)
232   {
233     return getIdMapping(division, accession, start, end, CDS);
234   }
235
236   /**
237    * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
238    * returned chromosomal coordinates, or returns null if the call fails
239    * 
240    * @param division
241    *          e.g. Ensembl, EnsemblMetazoa
242    * @param accession
243    *          e.g. ENST00000592782, Y55B1AR.1.1
244    * @param start
245    * @param end
246    * @return
247    */
248   public GeneLociI getCdnaMapping(String division, String accession,
249           int start, int end)
250   {
251     return getIdMapping(division, accession, start, end, CDNA);
252   }
253
254   GeneLociI getIdMapping(String division, String accession, int start,
255           int end, String cdsOrCdna)
256   {
257     URL url = null;
258     try
259     {
260       String domain = new EnsemblInfo().getDomain(division);
261       if (domain != null)
262       {
263         url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
264         return (parseIdMappingResponse(url, accession, domain));
265       }
266       return null;
267     } catch (Throwable t)
268     {
269       jalview.bin.Console
270               .outPrintln("Error calling " + url + ": " + t.getMessage());
271       return null;
272     }
273   }
274
275   /**
276    * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
277    * REST call is to either ensembl or ensemblgenomes, as determined from the
278    * division, e.g. Ensembl or EnsemblProtists.
279    * 
280    * @param domain
281    * @param accession
282    * @param start
283    * @param end
284    * @param cdsOrCdna
285    * @return
286    * @throws MalformedURLException
287    */
288   URL getIdMapUrl(String domain, String accession, int start, int end,
289           String cdsOrCdna) throws MalformedURLException
290   {
291     String url = String.format(
292             "%s/map/%s/%s/%d..%d?include_original_region=1&content-type=application/json",
293             domain, cdsOrCdna, accession, start, end);
294     return new URL(url);
295   }
296
297   /**
298    * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
299    * format is
300    * 
301    * <pre>
302    * {"mappings":
303    *   [
304    *    {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
305    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
306    *    {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
307    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
308    *   ]
309    * }
310    * </pre>
311    * 
312    * @param br
313    * @param accession
314    * @param domain
315    * @return
316    */
317   @SuppressWarnings("unchecked")
318   GeneLociI parseIdMappingResponse(URL url, String accession, String domain)
319   {
320
321     try
322     {
323       Iterator<Object> rvals = (Iterator<Object>) getJSON(url, null, -1,
324               MODE_ITERATOR, MAPPINGS);
325       if (rvals == null)
326       {
327         return null;
328       }
329       String assembly = null;
330       String chromosome = null;
331       int fromEnd = 0;
332       List<int[]> regions = new ArrayList<>();
333
334       while (rvals.hasNext())
335       {
336         Map<String, Object> val = (Map<String, Object>) rvals.next();
337         Map<String, Object> original = (Map<String, Object>) val
338                 .get("original");
339         fromEnd = Integer.parseInt(original.get("end").toString());
340
341         Map<String, Object> mapped = (Map<String, Object>) val.get(MAPPED);
342         int start = Integer.parseInt(mapped.get("start").toString());
343         int end = Integer.parseInt(mapped.get("end").toString());
344         String ass = mapped.get("assembly_name").toString();
345         if (assembly != null && !assembly.equals(ass))
346         {
347           jalview.bin.Console.errPrintln(
348                   "EnsemblMap found multiple assemblies - can't resolve");
349           return null;
350         }
351         assembly = ass;
352         String chr = mapped.get("seq_region_name").toString();
353         if (chromosome != null && !chromosome.equals(chr))
354         {
355           jalview.bin.Console.errPrintln(
356                   "EnsemblMap found multiple chromosomes - can't resolve");
357           return null;
358         }
359         chromosome = chr;
360         String strand = mapped.get("strand").toString();
361         if ("-1".equals(strand))
362         {
363           regions.add(new int[] { end, start });
364         }
365         else
366         {
367           regions.add(new int[] { start, end });
368         }
369       }
370
371       /*
372        * processed all mapped regions on chromosome, assemble the result,
373        * having first fetched the species id for the accession
374        */
375       final String species = new EnsemblLookup(domain)
376               .getSpecies(accession);
377       final String as = assembly;
378       final String chr = chromosome;
379       List<int[]> fromRange = Collections
380               .singletonList(new int[]
381               { 1, fromEnd });
382       Mapping mapping = new Mapping(new MapList(fromRange, regions, 1, 1));
383       return new GeneLocus(species == null ? "" : species, as, chr,
384               mapping);
385     } catch (IOException | ParseException | NumberFormatException e)
386     {
387       // ignore
388     }
389
390     return null;
391   }
392
393 }