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