Merge branch 'develop' into bug/JAL-2346annotationChoice
[jalview.git] / src / jalview / ext / rbvi / chimera / AtomSpecModel.java
diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java
new file mode 100644 (file)
index 0000000..d62cc3c
--- /dev/null
@@ -0,0 +1,180 @@
+package jalview.ext.rbvi.chimera;
+
+import jalview.util.RangeComparator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A class to model a Chimera atomspec pattern, for example
+ * 
+ * <pre>
+ * #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
+ * </pre>
+ * 
+ * where
+ * <ul>
+ * <li>#0 is a model number</li>
+ * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
+ * <li>.A is a chain identifier</li>
+ * <li>residue ranges are separated by comma</li>
+ * <li>atomspecs for distinct models are separated by | (or)</li>
+ * </ul>
+ * 
+ * <pre>
+ * @see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
+ * </pre>
+ */
+public class AtomSpecModel
+{
+  private Map<Integer, Map<String, List<int[]>>> atomSpec;
+
+  /**
+   * Constructor
+   */
+  public AtomSpecModel()
+  {
+    atomSpec = new TreeMap<Integer, Map<String, List<int[]>>>();
+  }
+
+  /**
+   * Adds one contiguous range to this atom spec
+   * 
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public void addRange(int model, int startPos, int endPos, String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour and model
+     */
+    Map<String, List<int[]>> modelData = atomSpec.get(model);
+    if (modelData == null)
+    {
+      atomSpec.put(model, modelData = new TreeMap<String, List<int[]>>());
+    }
+
+    /*
+     * Get/initialize map of data for colour, model and chain
+     */
+    List<int[]> chainData = modelData.get(chain);
+    if (chainData == null)
+    {
+      chainData = new ArrayList<int[]>();
+      modelData.put(chain, chainData);
+    }
+
+    /*
+     * Add the start/end positions
+     */
+    chainData.add(new int[] { startPos, endPos });
+    // TODO add intelligently, using a RangeList class
+  }
+
+  /**
+   * Returns the range(s) formatted as a Chimera atomspec
+   * 
+   * @return
+   */
+  public String getAtomSpec()
+  {
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (Integer model : atomSpec.keySet())
+    {
+      if (!firstModel)
+      {
+        sb.append("|");
+      }
+      firstModel = false;
+      sb.append("#").append(model).append(":");
+
+      boolean firstPositionForModel = true;
+      final Map<String, List<int[]>> modelData = atomSpec.get(model);
+
+      for (String chain : modelData.keySet())
+      {
+        chain = chain.trim();
+
+        List<int[]> rangeList = modelData.get(chain);
+
+        /*
+         * sort ranges into ascending start position order
+         */
+        Collections.sort(rangeList, new RangeComparator(true));
+
+        int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
+        int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
+
+        Iterator<int[]> iterator = rangeList.iterator();
+        while (iterator.hasNext())
+        {
+          int[] range = iterator.next();
+          if (range[0] <= end + 1)
+          {
+            /*
+             * range overlaps or is contiguous with the last one
+             * - so just extend the end position, and carry on
+             * (unless this is the last in the list)
+             */
+            end = Math.max(end, range[1]);
+          }
+          else
+          {
+            /*
+             * we have a break so append the last range
+             */
+            appendRange(sb, start, end, chain, firstPositionForModel);
+            firstPositionForModel = false;
+            start = range[0];
+            end = range[1];
+          }
+        }
+
+        /*
+         * and append the last range
+         */
+        if (!rangeList.isEmpty())
+        {
+          appendRange(sb, start, end, chain, firstPositionForModel);
+          firstPositionForModel = false;
+        }
+      }
+    }
+    return sb.toString();
+  }
+
+  /**
+   * @param sb
+   * @param start
+   * @param end
+   * @param chain
+   * @param firstPositionForModel
+   */
+  protected void appendRange(StringBuilder sb, int start, int end,
+          String chain, boolean firstPositionForModel)
+  {
+    if (!firstPositionForModel)
+    {
+      sb.append(",");
+    }
+    if (end == start)
+    {
+      sb.append(start);
+    }
+    else
+    {
+      sb.append(start).append("-").append(end);
+    }
+    if (chain.length() > 0)
+    {
+      sb.append(".").append(chain);
+    }
+  }
+}