JAL-1827 code and comment tidy up
[jalview.git] / src / jalview / ext / jmol / PDBFileWithJmol.java
index 1059021..d32a10d 100644 (file)
@@ -1,42 +1,55 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
- * Copyright (C) 2012 J Procter, AM Waterhouse, LM Lui, J Engelhardt, G Barton, M Clamp, S Searle
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
  * Jalview is free software: you can redistribute it and/or
  * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
  *  
  * Jalview is distributed in the hope that it will be useful, but 
  * WITHOUT ANY WARRANTY; without even the implied warranty 
  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
  * PURPOSE.  See the GNU General Public License for more details.
  * 
- * You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
  */
 package jalview.ext.jmol;
 
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.io.AlignFile;
+import jalview.io.FileParse;
+import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
+import jalview.util.MessageManager;
+
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
 import java.util.Map;
 
+import javajs.awt.Dimension;
+
 import org.jmol.api.JmolStatusListener;
 import org.jmol.api.JmolViewer;
-import org.jmol.constant.EnumCallback;
+import org.jmol.c.CBK;
+import org.jmol.c.STR;
 import org.jmol.modelset.Group;
 import org.jmol.modelset.Model;
 import org.jmol.modelset.ModelSet;
-import org.jmol.modelset.Polymer;
+import org.jmol.modelsetbio.BioModel;
 import org.jmol.modelsetbio.BioPolymer;
+import org.jmol.modelsetbio.Monomer;
 import org.jmol.viewer.Viewer;
-import org.openscience.jmol.app.JmolApp;
-
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.Annotation;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceI;
-import jalview.io.AlignFile;
 
 /**
  * Import and process PDB files with Jmol
@@ -47,20 +60,20 @@ import jalview.io.AlignFile;
 public class PDBFileWithJmol extends AlignFile implements
         JmolStatusListener
 {
-
-  JmolApp jmolApp = null;
-
   Viewer viewer = null;
 
-  public PDBFileWithJmol(String inFile, String type)
-          throws IOException
+  public PDBFileWithJmol(String inFile, String type) throws IOException
   {
     super(inFile, type);
   }
 
+  public PDBFileWithJmol(FileParse fp) throws IOException
+  {
+    super(fp);
+  }
+
   public PDBFileWithJmol()
   {
-    // TODO Auto-generated constructor stub
   }
 
   /**
@@ -71,26 +84,18 @@ public class PDBFileWithJmol extends AlignFile implements
   private Viewer getJmolData()
   {
     if (viewer == null)
-    { // note that -o -n -x are all implied
-      jmolApp = new JmolApp();
-      jmolApp.isDataOnly = true;
-      jmolApp.haveConsole = false;
-      jmolApp.haveDisplay = false;
-      jmolApp.exitUponCompletion = true;
+    {
       try
       {
         viewer = (Viewer) JmolViewer.allocateViewer(null, null, null, null,
-                null, jmolApp.commandOptions, this);
-        viewer.setScreenDimension(jmolApp.startupWidth,
-                jmolApp.startupHeight);
-        jmolApp.startViewer(viewer, null);
+                null, "-x -o -n", this);
+        // ensure the 'new' (DSSP) not 'old' (Ramachandran) SS method is used
+        viewer.setBooleanProperty("defaultStructureDSSP", true);
       } catch (ClassCastException x)
       {
-        throw new Error(
-                "Jmol version "
-                        + JmolViewer.getJmolVersion()
-                        + " is not compatible with this version of Jalview. Report this problem at issues.jalview.org",
-                x);
+        throw new Error(MessageManager.formatMessage(
+                "error.jmol_version_not_compatible_with_jalview_version",
+                new String[] { JmolViewer.getJmolVersion() }), x);
       }
     }
     return viewer;
@@ -110,174 +115,96 @@ public class PDBFileWithJmol extends AlignFile implements
     }
   }
 
-  /*
-   * (non-Javadoc)
+  /**
+   * Convert Jmol's secondary structure code to Jalview's, and stored it in the
+   * secondary structure arrays at the given sequence position
    * 
-   * @see jalview.io.AlignFile#parse()
+   * @param proteinStructureSubType
+   * @param pos
+   * @param secstr
+   * @param secstrcode
    */
-  @Override
-  public void parse() throws IOException
+  protected void setSecondaryStructure(STR proteinStructureSubType,
+          int pos, char[] secstr, char[] secstrcode)
   {
-    Viewer jmd = getJmolData();
-    jmd.openReader(getDataName(), getDataName(), getReader());
-    waitForScript(jmd);
-    if (jmd.getModelCount() > 0)
+    switch (proteinStructureSubType)
     {
-      ModelSet ms = jmd.getModelSet();
-      String structs = ms.calculateStructures(null, true, false, true);
-      // System.out.println("Structs\n"+structs);
-      for (Model model : ms.getModels())
-      {
-        for (int _bp = 0, _bpc = model.getBioPolymerCount(); _bp < _bpc; _bp++)
-        {
-          Polymer bp = model.getBioPolymer(_bp);
-          if (bp instanceof BioPolymer)
-          {
-            BioPolymer biopoly = (BioPolymer) bp;
-            char _lastChainId = 0;
-            int[] groups = biopoly.getLeadAtomIndices();
-            Group[] bpgrp = biopoly.getGroups();
-            char seq[] = new char[groups.length], secstr[] = new char[groups.length], secstrcode[] = new char[groups.length];
-            int groupc = 0, len = 0, firstrnum = 1, lastrnum = 0;
-            do
-            {
-              if (groupc >= groups.length
-                      || ms.atoms[groups[groupc]].getChainID() != _lastChainId)
-              {
-                if (len > 0)
-                {
-                  char newseq[] = new char[len];
-                  System.arraycopy(seq, 0, newseq, 0, len);
-                  Annotation asecstr[] = new Annotation[len];
-                  for (int p = 0; p < len; p++)
-                  {
-                    if (secstr[p] >= 'A' && secstr[p] <= 'z')
-                    {
-                      asecstr[p] = new Annotation("" + secstr[p], null,
-                              secstrcode[p], Float.NaN);
-                    }
-                  }
-                  SequenceI sq = new Sequence("" + getDataName() + "|"
-                          + model.getModelTitle() + "|" + _lastChainId,
-                          newseq, firstrnum, lastrnum);
-                  PDBEntry pdbe = new PDBEntry();
-                  pdbe.setFile(getDataName());
-                  pdbe.setId(getDataName());
-                  sq.addPDBId(pdbe);
-                  seqs.add(sq);
-                  if (!(biopoly.isDna() || biopoly.isRna()))
-                  {
-                    AlignmentAnnotation ann = new AlignmentAnnotation(
-                            "Secondary Structure",
-                            "Secondary Structure from PDB File", asecstr);
-                    sq.addAlignmentAnnotation(ann);
-                    annotations.add(ann);
-                  }
-                }
-                len = 0;
-                firstrnum = 1;
-                lastrnum = 0;
-              }
-              if (groupc < groups.length)
-              {
-                if (len == 0)
-                {
-                  firstrnum = bpgrp[groupc].getResno();
-                  _lastChainId = bpgrp[groupc].getChainID();
-                }
-                else
-                {
-                  lastrnum = bpgrp[groupc].getResno();
-                }
-                seq[len] = bpgrp[groupc].getGroup1();
-                switch (bpgrp[groupc].getProteinStructureSubType())
-                {
-                case HELIX_310:
-                  if (secstr[len] == 0)
-                  {
-                    secstr[len] = '3';
-                  }
-                case HELIX_ALPHA:
-                  if (secstr[len] == 0)
-                  {
-                    secstr[len] = 'H';
-                  }
-                case HELIX_PI:
-                  if (secstr[len] == 0)
-                  {
-                    secstr[len] = 'P';
-                  }
-                case HELIX:
-                  if (secstr[len] == 0)
-                  {
-                    secstr[len] = 'H';
-                  }
-                  secstrcode[len] = 'H';
-                  break;
-                case SHEET:
-                  secstr[len] = 'E';
-                  secstrcode[len] = 'E';
-                  break;
-                default:
-                  secstr[len] = 0;
-                  secstrcode[len] = 0;
-                }
-                len++;
-              }
-            } while (groupc++ < groups.length);
-
-          }
-        }
-      }
+    case HELIX310:
+      secstr[pos] = '3';
+      break;
+    case HELIX:
+    case HELIXALPHA:
+      secstr[pos] = 'H';
+      break;
+    case HELIXPI:
+      secstr[pos] = 'P';
+      break;
+    case SHEET:
+      secstr[pos] = 'E';
+      break;
+    default:
+      secstr[pos] = 0;
+    }
 
-      /*
-       * lastScriptTermination = -9465; String dsspOut =
-       * jmd.evalString("calculate STRUCTURE"); if (dsspOut.equals("pending")) {
-       * while (lastScriptTermination == -9465) { try { Thread.sleep(50); }
-       * catch (Exception x) { } ; } } System.out.println(lastConsoleEcho);
-       */
+    switch (proteinStructureSubType)
+    {
+    case HELIX310:
+    case HELIXALPHA:
+    case HELIXPI:
+    case HELIX:
+      secstrcode[pos] = 'H';
+      break;
+    case SHEET:
+      secstrcode[pos] = 'E';
+      break;
+    default:
+      secstrcode[pos] = 0;
     }
   }
 
-  /*
-   * (non-Javadoc)
+  /**
+   * Convert any non-standard peptide codes to their standard code table
+   * equivalent. (Initial version only does Selenomethionine MSE->MET.)
    * 
-   * @see jalview.io.AlignFile#print()
+   * @param threeLetterCode
+   * @param seq
+   * @param pos
+   */
+  protected void replaceNonCanonicalResidue(String threeLetterCode,
+          char[] seq, int pos)
+  {
+    String canonical = ResidueProperties
+            .getCanonicalAminoAcid(threeLetterCode);
+    if (canonical != null && !canonical.equalsIgnoreCase(threeLetterCode))
+    {
+      seq[pos] = ResidueProperties.getSingleCharacterCode(canonical);
+    }
+  }
+
+  /**
+   * Not implemented - returns null
    */
   @Override
   public String print()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented
+   */
   @Override
   public void setCallbackFunction(String callbackType,
           String callbackFunction)
   {
-    // TODO Auto-generated method stub
-
   }
 
-  /*
-   * @Override public void notifyCallback(EnumCallback type, Object[] data) {
-   * try { switch (type) { case ERROR: case SCRIPT:
-   * notifyScriptTermination((String) data[2], ((Integer) data[3]).intValue());
-   * break; case MESSAGE: sendConsoleMessage((data == null) ? ((String) null) :
-   * (String) data[1]); break; case LOADSTRUCT: notifyFileLoaded((String)
-   * data[1], (String) data[2], (String) data[3], (String) data[4], ((Integer)
-   * data[5]).intValue());
-   * 
-   * break; default: // System.err.println("Unhandled callback " + type + " " //
-   * + data[1].toString()); break; } } catch (Exception e) {
-   * System.err.println("Squashed Jmol callback handler error:");
-   * e.printStackTrace(); } }
-   */
-  public void notifyCallback(EnumCallback type, Object[] data)
+  @Override
+  public void notifyCallback(CBK cbType, Object[] data)
   {
     String strInfo = (data == null || data[1] == null ? null : data[1]
             .toString());
-    switch (type)
+    switch (cbType)
     {
     case ECHO:
       sendConsoleEcho(strInfo);
@@ -289,11 +216,16 @@ public class PDBFileWithJmol extends AlignFile implements
     case MEASURE:
       String mystatus = (String) data[3];
       if (mystatus.indexOf("Picked") >= 0
-              || mystatus.indexOf("Sequence") >= 0) // picking mode
+              || mystatus.indexOf("Sequence") >= 0)
+      {
+        // Picking mode
         sendConsoleMessage(strInfo);
+      }
       else if (mystatus.indexOf("Completed") >= 0)
+      {
         sendConsoleEcho(strInfo.substring(strInfo.lastIndexOf(",") + 2,
                 strInfo.length() - 1));
+      }
       break;
     case MESSAGE:
       sendConsoleMessage(data == null ? null : strInfo);
@@ -306,13 +238,6 @@ public class PDBFileWithJmol extends AlignFile implements
     }
   }
 
-  private void notifyFileLoaded(String string, String string2,
-          String string3, String string4, int intValue)
-  {
-    // TODO Auto-generated method stub
-
-  }
-
   String lastConsoleEcho = "";
 
   private void sendConsoleEcho(String string)
@@ -341,7 +266,7 @@ public class PDBFileWithJmol extends AlignFile implements
   }
 
   @Override
-  public boolean notifyEnabled(EnumCallback callbackPick)
+  public boolean notifyEnabled(CBK callbackPick)
   {
     switch (callbackPick)
     {
@@ -351,66 +276,396 @@ public class PDBFileWithJmol extends AlignFile implements
     case LOADSTRUCT:
     case ERROR:
       return true;
-    case MEASURE:
-    case PICK:
-    case HOVER:
-    case RESIZE:
-    case SYNC:
-    case CLICK:
-    case ANIMFRAME:
-    case MINIMIZATION:
+    default:
+      return false;
     }
-    return false;
   }
 
+  /**
+   * Not implemented - returns null
+   */
   @Override
   public String eval(String strEval)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented - returns null
+   */
   @Override
   public float[][] functionXY(String functionName, int x, int y)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented - returns null
+   */
   @Override
   public float[][][] functionXYZ(String functionName, int nx, int ny, int nz)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented - returns null
+   */
   @Override
-  public String createImage(String fileName, String type,
+  public String createImage(String fileName, String imageType,
           Object text_or_bytes, int quality)
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented - returns null
+   */
   @Override
   public Map<String, Object> getRegistryInfo()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
+  /**
+   * Not implemented
+   */
   @Override
   public void showUrl(String url)
   {
-    // TODO Auto-generated method stub
+  }
 
+  /**
+   * Not implemented - returns null
+   */
+  @Override
+  public Dimension resizeInnerPanel(String data)
+  {
+    return null;
   }
 
   @Override
-  public void resizeInnerPanel(String data)
+  public Map<String, Object> getJSpecViewProperty(String arg0)
+  {
+    return null;
+  }
+
+  /**
+   * Calls the Jmol library to parse the PDB file, and then inspects the
+   * resulting object model to generate Jalview-style sequences, with secondary
+   * structure annotation added where available (i.e. where it has been computed
+   * by Jmol using DSSP).
+   * 
+   * @see jalview.io.AlignFile#parse()
+   */
+  @Override
+  public void parse() throws IOException
+  {
+    Viewer jmolModel = getJmolData();
+    jmolModel.openReader(getDataName(), getDataName(), getReader());
+    waitForScript(jmolModel);
+
+    /*
+     * Convert one or more Jmol Model objects to Jalview sequences
+     */
+    if (jmolModel.ms.mc > 0)
+    {
+      parseBiopolymers(jmolModel.ms);
+    }
+  }
+
+  /**
+   * Process the Jmol BioPolymer array and generate a Jalview sequence for each
+   * chain found (including any secondary structure annotation from DSSP)
+   * 
+   * @param ms
+   * @throws IOException
+   */
+  public void parseBiopolymers(ModelSet ms) throws IOException
+  {
+    int modelIndex = -1;
+    for (Model model : ms.am)
+    {
+      modelIndex++;
+      String modelTitle = (String) ms.getInfo(modelIndex, "title");
+
+      /*
+       * Chains can span BioPolymers, so first make a flattened list, 
+       * and then work out the lengths of chains present
+       */
+      List<Monomer> monomers = getMonomers(ms, (BioModel) model);
+      List<Integer> chainLengths = getChainLengths(monomers);
+
+      /*
+       * now chop up the Monomer list to make Jalview Sequences
+       */
+      int from = 0;
+      for (int length : chainLengths)
+      {
+        buildSequenceFromChain(monomers.subList(from, from + length), modelTitle);
+        from += length;
+      }
+    }
+  }
+
+  /**
+   * Helper method to construct a sequence for one chain and add it to the seqs
+   * list
+   * 
+   * @param monomers
+   *          a list of all monomers in the chain
+   * @param modelTitle
+   */
+  protected void buildSequenceFromChain(List<Monomer> monomers, String modelTitle)
   {
-    // TODO Auto-generated method stub
+    final int length = monomers.size();
+
+    /*
+     * arrays to hold sequence and secondary structure
+     */
+    char[] seq = new char[length];
+    char[] secstr = new char[length];
+    char[] secstrcode = new char[length];
+
+    /*
+     * populate the sequence and secondary structure arrays
+     */
+    extractJmolChainData(monomers, seq, secstr, secstrcode);
+
+    /*
+     * grab chain code and start position from first residue;
+     */
+    String chainId = monomers.get(0).chain.getIDStr();
+    int firstResNum = monomers.get(0).getResno();
+    if (firstResNum < 1)
+    {
+      // Jalview doesn't like residue < 1, so force this to 1
+      System.err.println("Converting chain " + chainId + " first RESNUM ("
+              + firstResNum + ") to 1");
+      firstResNum = 1;
+    }
+
+    /*
+     * convert any non-gap unknown residues to 'X'
+     */
+    convertNonGapCharacters(seq);
+
+    /*
+     * construct and add the Jalview sequence
+     */
+    String seqName = "" + getDataName() + "|" + modelTitle + "|"
+            + chainId;
+    SequenceI sq = new Sequence(seqName, seq, firstResNum, firstResNum + length - 1);
+    seqs.add(sq);
+
+    /*
+     * add secondary structure predictions (if any)
+     */
+    addSecondaryStructureAnnotation(modelTitle, sq, secstr, secstrcode,
+            chainId, firstResNum);
+
+    /*
+     * record the PDB id for the sequence
+     */
+    addPdbid(sq, chainId);
+  }
 
+  /**
+   * Scans the list of (Jmol) Monomer objects, and adds the residue for each to
+   * the sequence array, and any converted secondary structure prediction to the
+   * secondary structure arrays
+   * 
+   * @param monomers
+   * @param seq
+   * @param secstr
+   * @param secstrcode
+   */
+  protected void extractJmolChainData(List<Monomer> monomers, char[] seq,
+          char[] secstr, char[] secstrcode)
+  {
+    int pos = 0;
+    for (Monomer monomer : monomers)
+    {
+      seq[pos] = monomer.getGroup1();
+
+      /*
+       * JAL-1828 replace a modified amino acid with its standard
+       * equivalent (e.g. MSE with MET->M) to maximise sequence matching
+       */
+      replaceNonCanonicalResidue(monomer.getGroup3(), seq, pos);
+
+      /*
+       * if Jmol has derived a secondary structure prediction for
+       * this position, convert it to Jalview equivalent and save it
+       */
+      setSecondaryStructure(monomer.getProteinStructureSubType(), pos,
+              secstr, secstrcode);
+      pos++;
+    }
+  }
+
+  /**
+   * Helper method that adds an AlignmentAnnotation for secondary structure to
+   * the sequence, provided at least one secondary structure prediction has been
+   * made
+   * 
+   * @param modelTitle
+   * @param seq
+   * @param secstr
+   * @param secstrcode
+   * @param chainId
+   * @param firstResNum
+   * @return
+   */
+  protected void addSecondaryStructureAnnotation(String modelTitle,
+          SequenceI sq, char[] secstr, char[] secstrcode,
+          String chainId, int firstResNum)
+  {
+    char[] seq = sq.getSequence();
+    boolean ssFound = false;
+    Annotation asecstr[] = new Annotation[seq.length + firstResNum - 1];
+    for (int p = 0; p < seq.length; p++)
+    {
+      if (secstr[p] >= 'A' && secstr[p] <= 'z')
+      {
+        asecstr[p] = new Annotation(String.valueOf(secstr[p]), null,
+                secstrcode[p], Float.NaN);
+        ssFound = true;
+      }
+    }
+
+    if (ssFound)
+    {
+      String mt = modelTitle == null ? getDataName() : modelTitle;
+      mt += chainId;
+      AlignmentAnnotation ann = new AlignmentAnnotation(
+              "Secondary Structure", "Secondary Structure for " + mt,
+              asecstr);
+      ann.belowAlignment = true;
+      ann.visible = true;
+      ann.autoCalculated = false;
+      ann.setCalcId(getClass().getName());
+      ann.adjustForAlignment();
+      ann.validateRangeAndDisplay();
+      annotations.add(ann);
+      sq.addAlignmentAnnotation(ann);
+    }
+  }
+
+  /**
+   * Replace any non-gap miscellaneous characters with 'X'
+   * 
+   * @param seq
+   * @return
+   */
+  protected void convertNonGapCharacters(char[] seq)
+  {
+    boolean isNa = Comparison.areNucleotide(new char[][] { seq });
+    int[] cinds = isNa ? ResidueProperties.nucleotideIndex
+            : ResidueProperties.aaIndex;
+    int nonGap = isNa ? ResidueProperties.maxNucleotideIndex
+            : ResidueProperties.maxProteinIndex;
+
+    for (int p = 0; p < seq.length; p++)
+    {
+      if (cinds[seq[p]] == nonGap)
+      {
+        seq[p] = 'X';
+      }
+    }
+  }
+
+  /**
+   * Add a PDBEntry giving the source of PDB data to the sequence
+   * 
+   * @param sq
+   * @param chainId
+   */
+  protected void addPdbid(SequenceI sq, String chainId)
+  {
+    PDBEntry pdbe = new PDBEntry();
+    pdbe.setFile(getDataName());
+    pdbe.setId(getDataName());
+    pdbe.setProperty(new Hashtable());
+    pdbe.setChainCode(chainId);
+    sq.addPDBId(pdbe);
+  }
+
+  /**
+   * Scans the list of Monomers (residue models), inspecting the chain id for
+   * each, and returns an array whose length is the number of chains, and values
+   * the length of each chain
+   * 
+   * @param monomers
+   * @return
+   */
+  protected List<Integer> getChainLengths(List<Monomer> monomers)
+  {
+    List<Integer> chainLengths = new ArrayList<Integer>();
+    int lastChainId = -1;
+    int length = 0;
+
+    for (Monomer monomer : monomers)
+    {
+      int chainId = monomer.chain.chainID;
+      if (chainId != lastChainId && length > 0)
+      {
+        /*
+         * change of chain - record the length of the last one
+         */
+        chainLengths.add(length);
+        length = 0;
+      }
+      lastChainId = chainId;
+      length++;
+    }
+    if (length > 0)
+    {
+      /*
+       * record the length of the final chain
+       */
+      chainLengths.add(length);
+    }
+
+    return chainLengths;
+  }
+
+  /**
+   * Returns a flattened list of Monomer (residues) in order, across all
+   * BioPolymers in the model. This simplifies assembling chains which span
+   * BioPolymers. The result omits any alternate residues reported for the same
+   * sequence position (RESNUM value).
+   * 
+   * @param ms
+   * @param model
+   * @return
+   */
+  protected List<Monomer> getMonomers(ModelSet ms, BioModel model)
+  {
+    List<Monomer> result = new ArrayList<Monomer>();
+    int lastResNo = Integer.MIN_VALUE;
+
+    for (BioPolymer bp : model.bioPolymers)
+    {
+      for (int groupLeadAtoms : bp.getLeadAtomIndices())
+      {
+        Group group = ms.at[groupLeadAtoms].group;
+        if (group instanceof Monomer)
+        {
+          /*
+           * ignore alternate residue at same position
+           * example: 1ejg has residues A:LEU, B:ILE at RESNUM=25
+           */
+          int resNo = group.getResno();
+          if (lastResNo != resNo)
+          {
+            result.add((Monomer) group);
+          }
+          lastResNo = resNo;
+        }
+      }
+    }
+    return result;
   }
 
 }