JAL-2422 basic proof of concept of ChimeraX opened/coloured by Jalview
[jalview.git] / src / jalview / ext / rbvi / chimera / AtomSpecModel.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.rbvi.chimera;
22
23 import jalview.util.IntRangeComparator;
24
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.TreeMap;
31
32 /**
33  * A class to model a Chimera atomspec pattern, for example
34  * 
35  * <pre>
36  * #0:15.A,28.A,54.A,63.A,70-72.A,83-84.A,97-98.A|#1:2.A,6.A,11.A,13-14.A,70.A,82.A,96-97.A
37  * </pre>
38  * 
39  * where
40  * <ul>
41  * <li>#0 is a model number</li>
42  * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
43  * <li>.A is a chain identifier</li>
44  * <li>residue ranges are separated by comma</li>
45  * <li>atomspecs for distinct models are separated by | (or)</li>
46  * </ul>
47  * 
48  * <pre>
49  * &#64;see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
50  * </pre>
51  */
52 public class AtomSpecModel
53 {
54   private Map<Integer, Map<String, List<int[]>>> atomSpec;
55
56   /**
57    * Constructor
58    */
59   public AtomSpecModel()
60   {
61     atomSpec = new TreeMap<>();
62   }
63
64   /**
65    * Adds one contiguous range to this atom spec
66    * 
67    * @param model
68    * @param startPos
69    * @param endPos
70    * @param chain
71    */
72   public void addRange(int model, int startPos, int endPos, String chain)
73   {
74     /*
75      * Get/initialize map of data for the colour and model
76      */
77     Map<String, List<int[]>> modelData = atomSpec.get(model);
78     if (modelData == null)
79     {
80       atomSpec.put(model, modelData = new TreeMap<>());
81     }
82
83     /*
84      * Get/initialize map of data for colour, model and chain
85      */
86     List<int[]> chainData = modelData.get(chain);
87     if (chainData == null)
88     {
89       chainData = new ArrayList<>();
90       modelData.put(chain, chainData);
91     }
92
93     /*
94      * Add the start/end positions
95      */
96     chainData.add(new int[] { startPos, endPos });
97     // TODO add intelligently, using a RangeList class
98   }
99
100   /**
101    * Returns the range(s) formatted as a Chimera atomspec
102    * 
103    * @return
104    */
105   public String getAtomSpec()
106   {
107     StringBuilder sb = new StringBuilder(128);
108     boolean firstModel = true;
109     for (Integer model : atomSpec.keySet())
110     {
111       if (!firstModel)
112       {
113         sb.append("|");
114       }
115       firstModel = false;
116       sb.append("#").append(model).append(":");
117
118       boolean firstPositionForModel = true;
119       final Map<String, List<int[]>> modelData = atomSpec.get(model);
120
121       for (String chain : modelData.keySet())
122       {
123         chain = " ".equals(chain) ? chain : chain.trim();
124
125         List<int[]> rangeList = modelData.get(chain);
126
127         /*
128          * sort ranges into ascending start position order
129          */
130         Collections.sort(rangeList, IntRangeComparator.ASCENDING);
131
132         int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
133         int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
134
135         Iterator<int[]> iterator = rangeList.iterator();
136         while (iterator.hasNext())
137         {
138           int[] range = iterator.next();
139           if (range[0] <= end + 1)
140           {
141             /*
142              * range overlaps or is contiguous with the last one
143              * - so just extend the end position, and carry on
144              * (unless this is the last in the list)
145              */
146             end = Math.max(end, range[1]);
147           }
148           else
149           {
150             /*
151              * we have a break so append the last range
152              */
153             appendRange(sb, start, end, chain, firstPositionForModel,
154                     false);
155             firstPositionForModel = false;
156             start = range[0];
157             end = range[1];
158           }
159         }
160
161         /*
162          * and append the last range
163          */
164         if (!rangeList.isEmpty())
165         {
166           appendRange(sb, start, end, chain, firstPositionForModel, false);
167           firstPositionForModel = false;
168         }
169       }
170     }
171     return sb.toString();
172   }
173
174   /**
175    * @param sb
176    * @param start
177    * @param end
178    * @param chain
179    * @param firstPositionForModel
180    */
181   protected void appendRange(StringBuilder sb, int start, int end,
182           String chain, boolean firstPositionForModel, boolean isChimeraX)
183   {
184     if (!firstPositionForModel)
185     {
186       sb.append(",");
187     }
188     if (end == start)
189     {
190       sb.append(start);
191     }
192     else
193     {
194       sb.append(start).append("-").append(end);
195     }
196
197     if (!isChimeraX)
198     {
199       sb.append(".");
200       if (!" ".equals(chain))
201       {
202         sb.append(chain);
203       }
204     }
205   }
206
207   /**
208    * Returns the range(s) formatted as a ChimeraX atomspec, for example
209    * <p>
210    * #1/A:2-20,30-40/B:10-20|#2/A:12-30
211    * 
212    * @return
213    */
214   public String getAtomSpecX()
215   {
216     StringBuilder sb = new StringBuilder(128);
217     boolean firstModel = true;
218     for (Integer model : atomSpec.keySet())
219     {
220       if (!firstModel)
221       {
222         sb.append("|");
223       }
224       firstModel = false;
225       sb.append("#").append(model);
226   
227       final Map<String, List<int[]>> modelData = atomSpec.get(model);
228   
229       for (String chain : modelData.keySet())
230       {
231         boolean firstPositionForChain = true;
232         chain = " ".equals(chain) ? chain : chain.trim();
233         sb.append("/").append(chain).append(":");
234         List<int[]> rangeList = modelData.get(chain);
235   
236         /*
237          * sort ranges into ascending start position order
238          */
239         Collections.sort(rangeList, IntRangeComparator.ASCENDING);
240   
241         int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
242         int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
243   
244         Iterator<int[]> iterator = rangeList.iterator();
245         while (iterator.hasNext())
246         {
247           int[] range = iterator.next();
248           if (range[0] <= end + 1)
249           {
250             /*
251              * range overlaps or is contiguous with the last one
252              * - so just extend the end position, and carry on
253              * (unless this is the last in the list)
254              */
255             end = Math.max(end, range[1]);
256           }
257           else
258           {
259             /*
260              * we have a break so append the last range
261              */
262             appendRange(sb, start, end, chain, firstPositionForChain, true);
263             start = range[0];
264             end = range[1];
265             firstPositionForChain = false;
266           }
267         }
268   
269         /*
270          * and append the last range
271          */
272         if (!rangeList.isEmpty())
273         {
274           appendRange(sb, start, end, chain, firstPositionForChain, true);
275         }
276         firstPositionForChain = false;
277       }
278     }
279     return sb.toString();
280   }
281 }