--- /dev/null
+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);
+ }
+ }
+}