Merge branch 'develop' into feature/JAL-3551Pymol
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 19 May 2020 16:27:32 +0000 (17:27 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Tue, 19 May 2020 16:27:32 +0000 (17:27 +0100)
Conflicts:
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/gui/AppJmol.java
src/jalview/gui/AppJmolBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/StructureViewerBase.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/project/Jalview2XML.java

20 files changed:
1  2 
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AppletJmol.java
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/appletgui/ExtJmol.java
src/jalview/appletgui/UserDefinedColours.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AppJmol.java
src/jalview/gui/AppJmolBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/Preferences.java
src/jalview/gui/StructureViewerBase.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/project/Jalview2XML.java
test/jalview/structure/StructureSelectionManagerTest.java

Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -414,7 -413,9 +413,10 @@@ public class SequenceFeatures implement
    public static void sortFeatures(List<? extends IntervalI> features,
            final boolean forwardStrand)
    {
-     IntervalI.sortIntervals(features, forwardStrand);
+     Collections.sort(features,
 -            forwardStrand ? IntervalI.COMPARE_BEGIN_ASC_END_ASC
++            forwardStrand
++                    ? IntervalI.COMPARE_BEGIN_ASC_END_DESC
+                     : IntervalI.COMPARE_END_DESC);
    }
  
    /**
@@@ -37,21 -58,9 +37,22 @@@ import org.jmol.api.JmolSelectionListen
  import org.jmol.api.JmolStatusListener;
  import org.jmol.api.JmolViewer;
  import org.jmol.c.CBK;
 -import org.jmol.script.T;
  import org.jmol.viewer.Viewer;
  
 +import jalview.api.FeatureRenderer;
++import jalview.bin.Cache;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.IProgressIndicator;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.io.DataSourceType;
 +import jalview.io.StructureFile;
 +import jalview.structure.AtomSpec;
 +import jalview.structure.StructureCommand;
 +import jalview.structure.StructureCommandI;
 +import jalview.structure.StructureSelectionManager;
 +import jalview.structures.models.AAStructureBindingModel;
 +
  public abstract class JalviewJmolBinding extends AAStructureBindingModel
          implements JmolStatusListener, JmolSelectionListener,
          ComponentListener
      return getViewerTitle("Jmol", true);
    }
  
 -  /**
 -   * prepare the view for a given set of models/chains. chainList contains
 -   * strings of the form 'pdbfilename:Chaincode'
 -   * 
 -   * @param chainList
 -   *          list of chains to make visible
 -   */
 -  public void centerViewer(Vector<String> chainList)
 -  {
 -    StringBuilder cmd = new StringBuilder(128);
 -    int mlength, p;
 -    for (String lbl : chainList)
 -    {
 -      mlength = 0;
 -      do
 -      {
 -        p = mlength;
 -        mlength = lbl.indexOf(":", p);
 -      } while (p < mlength && mlength < (lbl.length() - 2));
 -      // TODO: lookup each pdb id and recover proper model number for it.
 -      cmd.append(":" + lbl.substring(mlength + 1) + " /"
 -              + (1 + getModelNum(chainFile.get(lbl))) + " or ");
 -    }
 -    if (cmd.length() > 0)
 -    {
 -      cmd.setLength(cmd.length() - 4);
 -    }
 -    evalStateCommand("select *;restrict " + cmd + ";cartoon;center " + cmd);
 -  }
 -
 -  public void closeViewer()
 -  {
 -    // remove listeners for all structures in viewer
 -    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
 -    if (viewer != null)
 -    {
 -      viewer.dispose();
 -    }
 -    lastCommand = null;
 -    viewer = null;
 -    releaseUIResources();
 -  }
 -
 -  @Override
 -  public void colourByChain()
 -  {
 -    colourBySequence = false;
 -    // TODO: colour by chain should colour each chain distinctly across all
 -    // visible models
 -    // TODO: http://issues.jalview.org/browse/JAL-628
 -    evalStateCommand("select *;color chain");
 -  }
 -
 -  @Override
 -  public void colourByCharge()
 -  {
 -    colourBySequence = false;
 -    evalStateCommand("select *;color white;select ASP,GLU;color red;"
 -            + "select LYS,ARG;color blue;select CYS;color yellow");
 -  }
 -
 -  /**
 -   * superpose the structures associated with sequences in the alignment
 -   * according to their corresponding positions.
 -   */
 -  public void superposeStructures(AlignmentI alignment)
++  private String jmolScript(String script)
+   {
 -    superposeStructures(alignment, -1, null);
 -  }
++    Cache.log.debug(">>Jmol>> " + script);
++    String s = jmolViewer.evalStringQuiet(script);
++    Cache.log.debug("<<Jmol<< " + s);
 -  /**
 -   * superpose the structures associated with sequences in the alignment
 -   * according to their corresponding positions. ded)
 -   * 
 -   * @param refStructure
 -   *          - select which pdb file to use as reference (default is -1 - the
 -   *          first structure in the alignment)
 -   */
 -  public void superposeStructures(AlignmentI alignment, int refStructure)
 -  {
 -    superposeStructures(alignment, refStructure, null);
 -  }
 -
 -  /**
 -   * superpose the structures associated with sequences in the alignment
 -   * according to their corresponding positions. ded)
 -   * 
 -   * @param refStructure
 -   *          - select which pdb file to use as reference (default is -1 - the
 -   *          first structure in the alignment)
 -   * @param hiddenCols
 -   *          TODO
 -   */
 -  public void superposeStructures(AlignmentI alignment, int refStructure,
 -          HiddenColumns hiddenCols)
 -  {
 -    superposeStructures(new AlignmentI[] { alignment },
 -            new int[]
 -            { refStructure }, new HiddenColumns[] { hiddenCols });
++    return s;
+   }
 -  /**
 -   * {@inheritDoc}
 -   */
    @Override
 -  public String superposeStructures(AlignmentI[] _alignment,
 -          int[] _refStructure, HiddenColumns[] _hiddenCols)
 +  public List<String> executeCommand(StructureCommandI command,
 +          boolean getReply)
    {
 -    while (viewer.isScriptExecuting())
 -    {
 -      try
 -      {
 -        Thread.sleep(10);
 -      } catch (InterruptedException i)
 -      {
 -      }
 -    }
 -
 -    /*
 -     * get the distinct structure files modelled
 -     * (a file with multiple chains may map to multiple sequences)
 -     */
 -    String[] files = getStructureFiles();
 -    if (!waitForFileLoad(files))
 +    if (command == null)
      {
        return null;
      }
 -
 -    StringBuilder selectioncom = new StringBuilder(256);
 -    // In principle - nSeconds specifies the speed of animation for each
 -    // superposition - but is seems to behave weirdly, so we don't specify it.
 -    String nSeconds = " ";
 -    if (files.length > 10)
 -    {
 -      nSeconds = " 0.005 ";
 -    }
 -    else
 -    {
 -      nSeconds = " " + (2.0 / files.length) + " ";
 -      // if (nSeconds).substring(0,5)+" ";
 -    }
 -
 -    // see JAL-1345 - should really automatically turn off the animation for
 -    // large numbers of structures, but Jmol doesn't seem to allow that.
 -    // nSeconds = " ";
 -    // union of all aligned positions are collected together.
 -    for (int a = 0; a < _alignment.length; a++)
 -    {
 -      int refStructure = _refStructure[a];
 -      AlignmentI alignment = _alignment[a];
 -      HiddenColumns hiddenCols = _hiddenCols[a];
 -      if (a > 0 && selectioncom.length() > 0 && !selectioncom
 -              .substring(selectioncom.length() - 1).equals("|"))
 -      {
 -        selectioncom.append("|");
 -      }
 -      // process this alignment
 -      if (refStructure >= files.length)
 -      {
 -        System.err.println(
 -                "Invalid reference structure value " + refStructure);
 -        refStructure = -1;
 -      }
 -
 -      /*
 -       * 'matched' bit j will be set for visible alignment columns j where
 -       * all sequences have a residue with a mapping to the PDB structure
 -       */
 -      BitSet matched = new BitSet();
 -      for (int m = 0; m < alignment.getWidth(); m++)
 -      {
 -        if (hiddenCols == null || hiddenCols.isVisible(m))
 -        {
 -          matched.set(m);
 -        }
 -      }
 -
 -      SuperposeData[] structures = new SuperposeData[files.length];
 -      for (int f = 0; f < files.length; f++)
 -      {
 -        structures[f] = new SuperposeData(alignment.getWidth());
 -      }
 -
 -      /*
 -       * Calculate the superposable alignment columns ('matched'), and the
 -       * corresponding structure residue positions (structures.pdbResNo)
 -       */
 -      int candidateRefStructure = findSuperposableResidues(alignment,
 -              matched, structures);
 -      if (refStructure < 0)
 -      {
 -        /*
 -         * If no reference structure was specified, pick the first one that has
 -         * a mapping in the alignment
 -         */
 -        refStructure = candidateRefStructure;
 -      }
 -
 -      String[] selcom = new String[files.length];
 -      int nmatched = matched.cardinality();
 -      if (nmatched < 4)
 -      {
 -        return (MessageManager.formatMessage("label.insufficient_residues",
 -                nmatched));
 -      }
 -
 -      /*
 -       * generate select statements to select regions to superimpose structures
 -       */
 -      {
 -        // TODO extract method to construct selection statements
 -        for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
 -        {
 -          String chainCd = ":" + structures[pdbfnum].chain;
 -          int lpos = -1;
 -          boolean run = false;
 -          StringBuilder molsel = new StringBuilder();
 -          molsel.append("{");
 -
 -          int nextColumnMatch = matched.nextSetBit(0);
 -          while (nextColumnMatch != -1)
 -          {
 -            int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch];
 -            if (lpos != pdbResNo - 1)
 -            {
 -              // discontinuity
 -              if (lpos != -1)
 -              {
 -                molsel.append(lpos);
 -                molsel.append(chainCd);
 -                molsel.append("|");
 -              }
 -              run = false;
 -            }
 -            else
 -            {
 -              // continuous run - and lpos >-1
 -              if (!run)
 -              {
 -                // at the beginning, so add dash
 -                molsel.append(lpos);
 -                molsel.append("-");
 -              }
 -              run = true;
 -            }
 -            lpos = pdbResNo;
 -            nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
 -          }
 -          /*
 -           * add final selection phrase
 -           */
 -          if (lpos != -1)
 -          {
 -            molsel.append(lpos);
 -            molsel.append(chainCd);
 -            molsel.append("}");
 -          }
 -          if (molsel.length() > 1)
 -          {
 -            selcom[pdbfnum] = molsel.toString();
 -            selectioncom.append("((");
 -            selectioncom.append(selcom[pdbfnum].substring(1,
 -                    selcom[pdbfnum].length() - 1));
 -            selectioncom.append(" )& ");
 -            selectioncom.append(pdbfnum + 1);
 -            selectioncom.append(".1)");
 -            if (pdbfnum < files.length - 1)
 -            {
 -              selectioncom.append("|");
 -            }
 -          }
 -          else
 -          {
 -            selcom[pdbfnum] = null;
 -          }
 -        }
 -      }
 -      StringBuilder command = new StringBuilder(256);
 -      // command.append("set spinFps 10;\n");
 -
 -      for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
 -      {
 -        if (pdbfnum == refStructure || selcom[pdbfnum] == null
 -                || selcom[refStructure] == null)
 -        {
 -          continue;
 -        }
 -        command.append("echo ");
 -        command.append("\"Superposing (");
 -        command.append(structures[pdbfnum].pdbId);
 -        command.append(") against reference (");
 -        command.append(structures[refStructure].pdbId);
 -        command.append(")\";\ncompare " + nSeconds);
 -        command.append("{");
 -        command.append(Integer.toString(1 + pdbfnum));
 -        command.append(".1} {");
 -        command.append(Integer.toString(1 + refStructure));
 -        // conformation=1 excludes alternate locations for CA (JAL-1757)
 -        command.append(
 -                ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS ");
 -
 -        // for (int s = 0; s < 2; s++)
 -        // {
 -        // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]);
 -        // }
 -        command.append(selcom[pdbfnum]);
 -        command.append(selcom[refStructure]);
 -        command.append(" ROTATE TRANSLATE;\n");
 -      }
 -      if (selectioncom.length() > 0)
 -      {
 -        // TODO is performing selectioncom redundant here? is done later on
 -        // System.out.println("Select regions:\n" + selectioncom.toString());
 -        evalStateCommand("select *; cartoons off; backbone; select ("
 -                + selectioncom.toString() + "); cartoons; ");
 -        // selcom.append("; ribbons; ");
 -        String cmdString = command.toString();
 -        // System.out.println("Superimpose command(s):\n" + cmdString);
 -
 -        evalStateCommand(cmdString);
 -      }
 -    }
 -    if (selectioncom.length() > 0)
 -    {// finally, mark all regions that were superposed.
 -      if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
 -      {
 -        selectioncom.setLength(selectioncom.length() - 1);
 -      }
 -      // System.out.println("Select regions:\n" + selectioncom.toString());
 -      evalStateCommand("select *; cartoons off; backbone; select ("
 -              + selectioncom.toString() + "); cartoons; ");
 -      // evalStateCommand("select *; backbone; select "+selcom.toString()+";
 -      // cartoons; center "+selcom.toString());
 -    }
 -
 -    return null;
 -  }
 -
 -  public void evalStateCommand(String command)
 -  {
 +    String cmd = command.getCommand();
      jmolHistory(false);
 -    if (lastCommand == null || !lastCommand.equals(command))
 +    if (lastCommand == null || !lastCommand.equals(cmd))
      {
-       jmolViewer.evalStringQuiet(cmd + "\n");
 -      jmolScript(command + "\n");
++      jmolScript(cmd + "\n");
      }
      jmolHistory(true);
 -    lastCommand = command;
 -  }
 -
 -  Thread colourby = null;
 -
 -  /**
 -   * Sends a set of colour commands to the structure viewer
 -   * 
 -   * @param colourBySequenceCommands
 -   */
 -  @Override
 -  protected void colourBySequence(
 -          final StructureMappingcommandSet[] colourBySequenceCommands)
 -  {
 -    if (colourby != null)
 -    {
 -      colourby.interrupt();
 -      colourby = null;
 -    }
 -    Thread colourby = new Thread(new Runnable()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
 -        {
 -          for (String cbyseq : cpdbbyseq.commands)
 -          {
 -            executeWhenReady(cbyseq);
 -          }
 -        }
 -      }
 -    });
 -    colourby.start();
 -    this.colourby = colourby;
 -  }
 -
 -  /**
 -   * @param files
 -   * @param sr
 -   * @param viewPanel
 -   * @return
 -   */
 -  @Override
 -  protected StructureMappingcommandSet[] getColourBySequenceCommands(
 -          String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
 -  {
 -    return JmolCommands.getColourBySequenceCommand(getSsm(), files,
 -            getSequence(), sr, viewPanel);
 -  }
 -
 -  /**
 -   * @param command
 -   */
 -  protected void executeWhenReady(String command)
 -  {
 -    evalStateCommand(command);
 +    lastCommand = cmd;
 +    return null;
    }
  
    public void createImage(String file, String type, int quality)
  
      jmolHistory(false);
  
 +    StringBuilder selection = new StringBuilder(32);
      StringBuilder cmd = new StringBuilder(64);
 -    cmd.append("select " + pdbResNum); // +modelNum
 -
 -    resetLastRes.append("select " + pdbResNum); // +modelNum
 -
 -    cmd.append(":");
 -    resetLastRes.append(":");
 +    selection.append("select ").append(String.valueOf(pdbResNum));
 +    selection.append(":");
      if (!chain.equals(" "))
      {
 -      cmd.append(chain);
 -      resetLastRes.append(chain);
 -    }
 -    {
 -      cmd.append(" /" + (mdlNum + 1));
 -      resetLastRes.append("/" + (mdlNum + 1));
 +      selection.append(chain);
      }
 -    cmd.append(";wireframe 100;" + cmd.toString() + " and not hetero;");
 +    selection.append(" /").append(modelId);
  
 -    resetLastRes.append(";wireframe 0;" + resetLastRes.toString()
 -            + " and not hetero; spacefill 0;");
 +    cmd.append(selection).append(";wireframe 100;").append(selection)
 +            .append(" and not hetero;").append("spacefill 200;select none");
  
 -    cmd.append("spacefill 200;select none");
 +    resetLastRes.append(selection).append(";wireframe 0;").append(selection)
 +            .append(" and not hetero; spacefill 0;");
  
-     jmolViewer.evalStringQuiet(cmd.toString());
+     jmolScript(cmd.toString());
      jmolHistory(true);
 -
    }
  
 -  boolean debug = true;
 +  private boolean debug = true;
  
    private void jmolHistory(boolean enable)
    {
  
    }
  
 -  @Override
 -  public void setJalviewColourScheme(ColourSchemeI cs)
 -  {
 -    colourBySequence = false;
 -
 -    if (cs == null)
 -    {
 -      return;
 -    }
 -
 -    jmolHistory(false);
 -    StringBuilder command = new StringBuilder(128);
 -    command.append("select *;color white;");
 -    List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
 -            false);
 -    for (String resName : residueSet)
 -    {
 -      char res = resName.length() == 3
 -              ? ResidueProperties.getSingleCharacterCode(resName)
 -              : resName.charAt(0);
 -      Color col = cs.findColour(res, 0, null, null, 0f);
 -      command.append("select " + resName + ";color[" + col.getRed() + ","
 -              + col.getGreen() + "," + col.getBlue() + "];");
 -    }
 -
 -    evalStateCommand(command.toString());
 -    jmolHistory(true);
 -  }
 -
    public void showHelp()
    {
-     showUrl("http://jmol.sourceforge.net/docs/JmolUserGuide/", "jmolHelp");
+     showUrl("http://wiki.jmol.org"
+     // BH 2018 "http://jmol.sourceforge.net/docs/JmolUserGuide/"
+             , "jmolHelp");
    }
  
    /**
              htmlName + ((Object) this).toString(), documentBase, codeBase,
              commandOptions, this);
  
 -    viewer.setJmolStatusListener(this); // extends JmolCallbackListener
 +    jmolViewer.setJmolStatusListener(this); // extends JmolCallbackListener
  
-     console = createJmolConsole(consolePanel, buttonsToShow);
+     try
+     {
+       console = createJmolConsole(consolePanel, buttonsToShow);
+     } catch (Throwable e)
+     {
+       System.err.println("Could not create Jmol application console. "
+               + e.getMessage());
+       e.printStackTrace();
+     }
      if (consolePanel != null)
      {
        consolePanel.addComponentListener(this);
@@@ -48,403 -43,171 +48,422 @@@ import jalview.util.Comparison
   * @author JimP
   * 
   */
 -public class JmolCommands
 +public class JmolCommands extends StructureCommandsBase
  {
 +  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
 +          "select *; cartoons off; backbone");
 +
 +  private static final StructureCommand FOCUS_VIEW = new StructureCommand("zoom 0");
 +
 +  private static final StructureCommand COLOUR_ALL_WHITE = new StructureCommand(
 +          "select *;color white;");
 +
 +  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
 +          "select *;color white;select ASP,GLU;color red;"
 +                  + "select LYS,ARG;color blue;select CYS;color yellow");
 +
 +  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
 +          "select *;color chain");
 +
 +  private static final String PIPE = "|";
 +
 +  private static final String HYPHEN = "-";
 +
 +  private static final String COLON = ":";
 +
 +  private static final String SLASH = "/";
 +
 +  /**
 +   * {@inheritDoc}
 +   * 
 +   * @return
 +   */
 +  @Override
 +  public int getModelStartNo()
 +  {
 +    return 1;
 +  }
 +
 +  /**
 +   * Returns a string representation of the given colour suitable for inclusion
 +   * in Jmol commands
 +   * 
 +   * @param c
 +   * @return
 +   */
 +  protected String getColourString(Color c)
 +  {
 +    return c == null ? null
 +            : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(),
 +                    c.getBlue());
 +  }
 +
-   @Deprecated
-   public String[] colourBySequence(StructureSelectionManager ssm,
-           String[] files,
-           SequenceI[][] sequence, SequenceRenderer sr,
-           AlignmentViewPanel viewPanel)
-   {
-     // TODO delete method
-     FeatureRenderer fr = viewPanel.getFeatureRenderer();
-     FeatureColourFinder finder = new FeatureColourFinder(fr);
-     AlignViewportI viewport = viewPanel.getAlignViewport();
-     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
-     AlignmentI al = viewport.getAlignment();
-     List<String> cset = new ArrayList<>();
-     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-     {
-       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
-       StringBuilder command = new StringBuilder(128);
-       List<String> str = new ArrayList<>();
-       if (mapping == null || mapping.length < 1)
-       {
-         continue;
-       }
-       for (int s = 0; s < sequence[pdbfnum].length; s++)
-       {
-         for (int sp, m = 0; m < mapping.length; m++)
-         {
-           if (mapping[m].getSequence() == sequence[pdbfnum][s]
-                   && (sp = al.findIndex(sequence[pdbfnum][s])) > -1)
-           {
-             int lastPos = StructureMapping.UNASSIGNED_VALUE;
-             SequenceI asp = al.getSequenceAt(sp);
-             for (int r = 0; r < asp.getLength(); r++)
-             {
-               // no mapping to gaps in sequence
-               if (Comparison.isGap(asp.getCharAt(r)))
-               {
-                 continue;
-               }
-               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
-               if (pos == lastPos)
-               {
-                 continue;
-               }
-               if (pos == StructureMapping.UNASSIGNED_VALUE)
-               {
-                 // terminate current colour op
-                 if (command.length() > 0
-                         && command.charAt(command.length() - 1) != ';')
-                 {
-                   command.append(";");
-                 }
-                 // reset lastPos
-                 lastPos = StructureMapping.UNASSIGNED_VALUE;
-                 continue;
-               }
-               lastPos = pos;
-               Color col = sr.getResidueColour(sequence[pdbfnum][s], r,
-                       finder);
-               /*
-                * shade hidden regions darker
-                */
-               if (!cs.isVisible(r))
-               {
-                 col = Color.GRAY;
-               }
-               String newSelcom = (mapping[m].getChain() != " "
-                       ? ":" + mapping[m].getChain()
-                       : "") + "/" + (pdbfnum + 1) + ".1" + ";color"
-                       + getColourString(col);
-               if (command.length() > newSelcom.length() && command
-                       .substring(command.length() - newSelcom.length())
-                       .equals(newSelcom))
-               {
-                 command = JmolCommands.condenseCommand(command, pos);
-                 continue;
-               }
-               // TODO: deal with case when buffer is too large for Jmol to parse
-               // - execute command and flush
-               if (command.length() > 0
-                       && command.charAt(command.length() - 1) != ';')
-               {
-                 command.append(";");
-               }
-               if (command.length() > 51200)
-               {
-                 // add another chunk
-                 str.add(command.toString());
-                 command.setLength(0);
-               }
-               command.append("select " + pos);
-               command.append(newSelcom);
-             }
-             // break;
-           }
-         }
-       }
-       {
-         // add final chunk
-         str.add(command.toString());
-         command.setLength(0);
-       }
-       cset.addAll(str);
-     }
-     return cset.toArray(new String[cset.size()]);
-   }
-   public static StringBuilder condenseCommand(StringBuilder command,
-           int pos)
-   {
-     // work back to last 'select'
-     int p = command.length(), q = p;
-     do
-     {
-       p -= 6;
-       if (p < 1)
-       {
-         p = 0;
-       }
-       ;
-     } while ((q = command.indexOf("select", p)) == -1 && p > 0);
-     StringBuilder sb = new StringBuilder(command.substring(0, q + 7));
-     command = command.delete(0, q + 7);
-     String start;
-     if (command.indexOf("-") > -1)
-     {
-       start = command.substring(0, command.indexOf("-"));
-     }
-     else
-     {
-       start = command.substring(0, command.indexOf(":"));
-     }
-     sb.append(start + "-" + pos + command.substring(command.indexOf(":")));
-     return sb;
-   }
 +  @Override
 +  public StructureCommandI colourByChain()
 +  {
 +    return COLOUR_BY_CHAIN;
 +  }
 +
 +  @Override
 +  public List<StructureCommandI> colourByCharge()
 +  {
 +    return Arrays.asList(COLOUR_BY_CHARGE);
 +  }
 +
 +  @Override
 +  public List<StructureCommandI> colourByResidues(Map<String, Color> colours)
 +  {
 +    List<StructureCommandI> cmds = super.colourByResidues(colours);
 +    cmds.add(0, COLOUR_ALL_WHITE);
 +    return cmds;
 +  }
 +
 +  @Override
 +  public StructureCommandI setBackgroundColour(Color col)
 +  {
 +    return new StructureCommand("background " + getColourString(col));
 +  }
 +
 +  @Override
 +  public StructureCommandI focusView()
 +  {
 +    return FOCUS_VIEW;
 +  }
 +
 +  @Override
 +  public List<StructureCommandI> showChains(List<String> toShow)
 +  {
 +    StringBuilder atomSpec = new StringBuilder(128);
 +    boolean first = true;
 +    for (String chain : toShow)
 +    {
 +      String[] tokens = chain.split(":");
 +      if (tokens.length == 2)
 +      {
 +        if (!first)
 +        {
 +          atomSpec.append(" or ");
 +        }
 +        first = false;
 +        atomSpec.append(":").append(tokens[1]).append(" /").append(tokens[0]);
 +      }
 +    }
 +
 +    String spec = atomSpec.toString();
 +    String command = "select *;restrict " + spec + ";cartoon;center "
 +            + spec;
 +    return Arrays.asList(new StructureCommand(command));
 +  }
  
    /**
 -   * Jmol utility which constructs the commands to colour chains by the given
 -   * alignment
 +   * Returns a command to superpose atoms in {@code atomSpec} to those in
 +   * {@code refAtoms}, restricted to alpha carbons only (Phosphorous for rna).
 +   * For example
 +   * 
 +   * <pre>
 +   * compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} 
 +   *         ATOMS {1-87:A}{2-54:A|61-94:A} ROTATE TRANSLATE 1.0;
 +   * </pre>
     * 
 -   * @returns Object[] { Object[] { <model being coloured>,
 +   * where {@code conformation=1} excludes ALTLOC atom locations, and 1.0 is the
 +   * time in seconds to animate the action. For this example, atoms in model 2
 +   * are moved towards atoms in model 1.
 +   * <p>
 +   * The two atomspecs should each be for one model only, but may have more than
 +   * one chain. The number of atoms specified should be the same for both
 +   * models, though if not, Jmol may make a 'best effort' at superposition.
     * 
 +   * @see https://chemapps.stolaf.edu/jmol/docs/#compare
     */
 -  public static StructureMappingcommandSet[] getColourBySequenceCommand(
 -          StructureSelectionManager ssm, String[] files,
 -          SequenceI[][] sequence, SequenceRenderer sr,
 +  @Override
 +  public List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
 +          AtomSpecModel atomSpec)
 +  {
 +    StringBuilder sb = new StringBuilder(64);
 +    String refModel = refAtoms.getModels().iterator().next();
 +    String model2 = atomSpec.getModels().iterator().next();
 +    sb.append(String.format("compare {%s.1} {%s.1}", model2, refModel));
 +    sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {");
 +
 +    /*
 +     * command examples don't include modelspec with atoms, getAtomSpec does;
 +     * it works, so leave it as it is for simplicity
 +     */
 +    sb.append(getAtomSpec(atomSpec, true)).append("}{");
 +    sb.append(getAtomSpec(refAtoms, true)).append("}");
 +    sb.append(" ROTATE TRANSLATE ");
 +    sb.append(getCommandSeparator());
 +
 +    /*
 +     * show residues used for superposition as ribbon
 +     */
 +    sb.append("select ").append(getAtomSpec(atomSpec, false)).append("|");
 +    sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator())
 +            .append("cartoons");
 +
 +    return Arrays.asList(new StructureCommand(sb.toString()));
 +  }
 +
 +  @Override
 +  public StructureCommandI openCommandFile(String path)
 +  {
 +    /*
 +     * https://chemapps.stolaf.edu/jmol/docs/#script
 +     * not currently used in Jalview
 +     */
 +    return new StructureCommand("script " + path);
 +  }
 +
 +  @Override
 +  public StructureCommandI saveSession(String filepath)
 +  {
 +    /*
 +     * https://chemapps.stolaf.edu/jmol/docs/#writemodel
 +     */
 +    return new StructureCommand("write STATE \"" + filepath + "\"");
 +  }
 +
 +  @Override
 +  protected StructureCommandI getColourCommand(String atomSpec, Color colour)
 +  {
 +    StringBuilder sb = new StringBuilder(atomSpec.length()+20);
 +    sb.append("select ").append(atomSpec).append(getCommandSeparator())
 +            .append("color").append(getColourString(colour));
 +    return new StructureCommand(sb.toString());
 +  }
 +
 +  @Override
 +  protected String getResidueSpec(String residue)
 +  {
 +    return residue;
 +  }
 +
 +  /**
 +   * Generates a Jmol atomspec string like
 +   * 
 +   * <pre>
 +   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
 +   * </pre>
 +   * 
 +   * Parameter {@code alphaOnly} is not used here - this restriction is made by
 +   * a separate clause in the {@code compare} (superposition) command.
 +   */
 +  @Override
 +  public String getAtomSpec(AtomSpecModel model, boolean alphaOnly)
 +  {
 +    StringBuilder sb = new StringBuilder(128);
 +
 +    boolean first = true;
 +    for (String modelNo : model.getModels())
 +    {
 +      for (String chain : model.getChains(modelNo))
 +      {
 +        for (int[] range : model.getRanges(modelNo, chain))
 +        {
 +          if (!first)
 +          {
 +            sb.append(PIPE);
 +          }
 +          first = false;
 +          if (range[0] == range[1])
 +          {
 +            sb.append(range[0]);
 +          }
 +          else
 +          {
 +            sb.append(range[0]).append(HYPHEN).append(range[1]);
 +          }
 +          sb.append(COLON).append(chain.trim()).append(SLASH);
 +          sb.append(String.valueOf(modelNo)).append(".1");
 +        }
 +      }
 +    }
 +
 +    return sb.toString();
 +  }
 +
 +  @Override
 +  public List<StructureCommandI> showBackbone()
 +  {
 +    return Arrays.asList(SHOW_BACKBONE);
 +  }
 +
 +  @Override
 +  public StructureCommandI loadFile(String file)
 +  {
 +    return null;
 +  }
++
++  /**
++   * Obsolete method, only referenced from
++   * jalview.javascript.MouseOverStructureListener
++   * 
++   * @param ssm
++   * @param files
++   * @param sequence
++   * @param sr
++   * @param viewPanel
++   * @return
++   */
++  @Deprecated
++  public String[] colourBySequence(StructureSelectionManager ssm,
++          String[] files, SequenceI[][] sequence, SequenceRenderer sr,
+           AlignmentViewPanel viewPanel)
+   {
++    // TODO delete method
++
+     FeatureRenderer fr = viewPanel.getFeatureRenderer();
+     FeatureColourFinder finder = new FeatureColourFinder(fr);
+     AlignViewportI viewport = viewPanel.getAlignViewport();
+     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+     AlignmentI al = viewport.getAlignment();
 -    List<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
++    List<String> cset = new ArrayList<>();
+     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+     {
+       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
 -      StringBuilder command = new StringBuilder();
 -      StructureMappingcommandSet smc;
 -      ArrayList<String> str = new ArrayList<String>();
++      StringBuilder command = new StringBuilder(128);
++      List<String> str = new ArrayList<>();
+       if (mapping == null || mapping.length < 1)
+       {
+         continue;
+       }
+       for (int s = 0; s < sequence[pdbfnum].length; s++)
+       {
+         for (int sp, m = 0; m < mapping.length; m++)
+         {
+           if (mapping[m].getSequence() == sequence[pdbfnum][s]
+                   && (sp = al.findIndex(sequence[pdbfnum][s])) > -1)
+           {
+             int lastPos = StructureMapping.UNASSIGNED_VALUE;
+             SequenceI asp = al.getSequenceAt(sp);
+             for (int r = 0; r < asp.getLength(); r++)
+             {
+               // no mapping to gaps in sequence
 -              if (jalview.util.Comparison.isGap(asp.getCharAt(r)))
++              if (Comparison.isGap(asp.getCharAt(r)))
+               {
+                 continue;
+               }
+               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
+               if (pos == lastPos)
+               {
+                 continue;
+               }
+               if (pos == StructureMapping.UNASSIGNED_VALUE)
+               {
+                 // terminate current colour op
+                 if (command.length() > 0
+                         && command.charAt(command.length() - 1) != ';')
+                 {
+                   command.append(";");
+                 }
+                 // reset lastPos
+                 lastPos = StructureMapping.UNASSIGNED_VALUE;
+                 continue;
+               }
+               lastPos = pos;
+               Color col = sr.getResidueColour(sequence[pdbfnum][s], r,
+                       finder);
+               /*
+                * shade hidden regions darker
+                */
+               if (!cs.isVisible(r))
+               {
+                 col = Color.GRAY;
+               }
 -              // todo JAL-3152 handle 'no chain' case without errors
 -              boolean hasChain = true || mapping[m].getChain() != " ";
 -                        String chainSpec = hasChain
++              String newSelcom = (mapping[m].getChain() != " "
+                       ? ":" + mapping[m].getChain()
 -                      : "";
 -                        String newSelcom = chainSpec + "/" + (pdbfnum + 1) + ".1" + ";color["
 -                      + col.getRed() + "," + col.getGreen() + ","
 -                      + col.getBlue() + "]";
++                      : "") + "/" + (pdbfnum + 1) + ".1" + ";color"
++                      + getColourString(col);
+               if (command.length() > newSelcom.length() && command
+                       .substring(command.length() - newSelcom.length())
+                       .equals(newSelcom))
+               {
+                 command = JmolCommands.condenseCommand(command, pos);
+                 continue;
+               }
+               // TODO: deal with case when buffer is too large for Jmol to parse
+               // - execute command and flush
+               if (command.length() > 0
+                       && command.charAt(command.length() - 1) != ';')
+               {
+                 command.append(";");
+               }
+               if (command.length() > 51200)
+               {
+                 // add another chunk
+                 str.add(command.toString());
+                 command.setLength(0);
+               }
+               command.append("select " + pos);
+               command.append(newSelcom);
+             }
+             // break;
+           }
+         }
+       }
+       {
+         // add final chunk
+         str.add(command.toString());
+         command.setLength(0);
+       }
 -      // Finally, add the command set ready to be returned.
 -      cset.add(new StructureMappingcommandSet(JmolCommands.class,
 -              files[pdbfnum], str.toArray(new String[str.size()])));
++      cset.addAll(str);
+     }
 -    return cset.toArray(new StructureMappingcommandSet[cset.size()]);
++    return cset.toArray(new String[cset.size()]);
+   }
 -  public static StringBuilder condenseCommand(StringBuilder command, int pos)
++  /**
++   * Helper method
++   * 
++   * @param command
++   * @param pos
++   * @return
++   */
++  @Deprecated
++  private static StringBuilder condenseCommand(
++          StringBuilder command,
++          int pos)
+   {
+     // work back to last 'select'
+     int p = command.length(), q = p;
+     do
+     {
+       p -= 6;
+       if (p < 1)
+       {
+         p = 0;
+       }
+       ;
+     } while ((q = command.indexOf("select", p)) == -1 && p > 0);
+     StringBuilder sb = new StringBuilder(command.substring(0, q + 7));
+     command = command.delete(0, q + 7);
+     String start;
+     if (command.indexOf("-") > -1)
+     {
+       start = command.substring(0, command.indexOf("-"));
+     }
+     else
+     {
+       start = command.substring(0, command.indexOf(":"));
+     }
+     sb.append(start + "-" + pos + command.substring(command.indexOf(":")));
+     return sb;
+   }
 -
  }
Simple merge
@@@ -36,20 -52,6 +36,21 @@@ import javax.swing.SwingUtilities
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.bin.Cache;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
++import jalview.gui.ImageExporter.ImageWriterI;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.structure.StructureCommand;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.BrowserLauncher;
 +import jalview.util.ImageMaker;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.ws.dbsources.Pdb;
 +
  public class AppJmol extends StructureViewerBase
  {
    // ms to wait for Jmol to load files
      return files;
    }
  
+   /**
+    * Outputs the Jmol viewer image as an image file, after prompting the user to
+    * choose a file and (for EPS) choice of Text or Lineart character rendering
+    * (unless a preference for this is set)
+    * 
+    * @param type
+    */
 +  @Override
-   public void eps_actionPerformed()
-   {
-     makePDBImage(ImageMaker.TYPE.EPS);
-   }
-   @Override
-   public void png_actionPerformed()
-   {
-     makePDBImage(ImageMaker.TYPE.PNG);
-   }
-   void makePDBImage(ImageMaker.TYPE type)
+   public void makePDBImage(ImageMaker.TYPE type)
    {
      int width = getWidth();
      int height = getHeight();
-     ImageMaker im;
-     if (type == ImageMaker.TYPE.PNG)
+     ImageWriterI writer = new ImageWriterI()
      {
-       im = new ImageMaker(this, ImageMaker.TYPE.PNG,
-               "Make PNG image from view",
-               width, height, null, null, null, 0, false);
-     }
-     else if (type == ImageMaker.TYPE.EPS)
-     {
-       im = new ImageMaker(this, ImageMaker.TYPE.EPS,
-               "Make EPS file from view",
-               width, height, null, this.getTitle(), null, 0, false);
-     }
-     else
-     {
-       im = new jalview.util.ImageMaker(this,
-               ImageMaker.TYPE.SVG, "Make SVG file from PCA",
-               width, height, null, this.getTitle(), null, 0, false);
-     }
-     if (im.getGraphics() != null)
-     {
-       jmb.jmolViewer.renderScreenImage(im.getGraphics(), width, height);
-       im.writeImage();
-     }
+       @Override
+       public void exportImage(Graphics g) throws Exception
+       {
 -        jmb.viewer.renderScreenImage(g, width, height);
++        jmb.jmolViewer.renderScreenImage(g, width, height);
+       }
+     };
+     String view = MessageManager.getString("action.view").toLowerCase();
+     ImageExporter exporter = new ImageExporter(writer,
 -            jmb.getIProgressIndicator(), type, getTitle());
++            getProgressIndicator(), type, getTitle());
+     exporter.doExport(null, this, width, height, view);
    }
  
    @Override
    {
      try
      {
-       BrowserLauncher
-               .openURL("http://jmol.sourceforge.net/docs/JmolUserGuide/");
+       BrowserLauncher // BH 2018
+               .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/");
      } catch (Exception ex)
      {
 +      System.err.println("Show Jmol help failed with: " + ex.getMessage());
      }
    }
  
   */
  package jalview.gui;
  
 +import java.awt.Container;
 +import java.io.File;
- import java.io.FileWriter;
- import java.io.IOException;
++import java.util.List;
 +import java.util.Map;
 +
 +import javax.swing.JComponent;
 +
 +import org.jmol.api.JmolAppConsoleInterface;
- import org.jmol.java.BS;
 +import org.openscience.jmol.app.jmolpanel.console.AppConsole;
 +
  import jalview.api.AlignmentViewPanel;
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
@@@ -40,6 -28,16 +38,8 @@@ import jalview.datamodel.SequenceI
  import jalview.ext.jmol.JalviewJmolBinding;
  import jalview.io.DataSourceType;
  import jalview.structure.StructureSelectionManager;
+ import jalview.util.Platform;
 -
 -import java.awt.Container;
 -import java.io.File;
 -import java.util.List;
 -import java.util.Map;
 -
 -import org.jmol.api.JmolAppConsoleInterface;
 -
+ import javajs.util.BS;
  
  public class AppJmolBinding extends JalviewJmolBinding
  {
        @Override
        public void run()
        {
 -        appJmolWindow.updateTitleAndMenus();
 -        // initiates a colourbySequence
 -        // via seqColour_ActionPerformed.
 -        appJmolWindow.revalidate();
 +        JalviewStructureDisplayI theViewer = getViewer();
++        // invokes colourbySequence() via seqColour_ActionPerformed()
 +        theViewer.updateTitleAndMenus();
 +        ((JComponent) theViewer).revalidate();
        }
      });
    }
    protected JmolAppConsoleInterface createJmolConsole(
            Container consolePanel, String buttonsToShow)
    {
 -    viewer.setJmolCallbackListener(this);
 -    return null;//BH can't do this yet. new AppConsole(viewer, consolePanel, buttonsToShow);
 +    jmolViewer.setJmolCallbackListener(this);
-     return new AppConsole(jmolViewer, consolePanel, buttonsToShow);
++    // BH comment: can't do this yet [for JS only, or generally?]
++    return Platform.isJS() ? null
++            : new AppConsole(jmolViewer, consolePanel, buttonsToShow);
    }
  
    @Override
      return null;
    }
  
-   /**
-    * Overrides the default method to save a session to file, in order to
-    * guarantee it is done synchronously. Jmol command 'write STATE path' would
-    * execute asynchronously, so instead we get the state and write it directly
-    * here.
-    */
--  @Override
-   protected void saveSession(File f)
 -  public JalviewStructureDisplayI getViewer()
 -  {
 -    return appJmolWindow;
 -  }
 -
 -  @Override
 -  public jalview.api.FeatureRenderer getFeatureRenderer(
 -          AlignmentViewPanel alignment)
 -  {
 -    AlignmentPanel ap = (alignment == null)
 -            ? appJmolWindow.getAlignmentPanel()
 -            : (AlignmentPanel) alignment;
 -    if (ap.av.isShowSequenceFeatures())
 -    {
 -      return ap.av.getAlignPanel().getSeqPanel().seqCanvas.fr;
 -    }
 -
 -    return null;
 -  }
 -
+   @SuppressWarnings("unused")
+   public void cacheFiles(List<File> files)
    {
-     String state = jmolViewer.getStateInfo();
-     if (state != null)
+     if (files == null)
      {
-       try
-       {
-         FileWriter fw = new FileWriter(f);
-         fw.write(state);
-         fw.close();
-       } catch (IOException e)
-       {
-         Cache.log.error("Error writing Jmol state: " + e.toString());
-       }
+       return;
      }
-     else
+     for (File f : files)
      {
-       Cache.log.error("Error requesting Jmol state to save");
+       Platform.cacheFileData(f);
      }
    }
  }
@@@ -36,20 -57,6 +36,21 @@@ import javax.swing.JMenuItem
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.api.FeatureRenderer;
 +import jalview.bin.Cache;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
 +import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.io.DataSourceType;
 +import jalview.io.StructureFile;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.BrowserLauncher;
++import jalview.util.ImageMaker.TYPE;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +
  /**
   * GUI elements for handling an external chimera display
   * 
@@@ -520,18 -607,110 +521,11 @@@ public class ChimeraViewFrame extends S
      worker = null;
    }
  
 -  /**
 -   * Fetch PDB data and save to a local file. Returns the full path to the file,
 -   * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
 -   * 
 -   * @param processingEntry
 -   * @return
 -   * @throws Exception
 -   */
 -
 -  private void stashFoundChains(StructureFile pdb, String file)
 -  {
 -    for (int i = 0; i < pdb.getChains().size(); i++)
 -    {
 -      String chid = new String(
 -              pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
 -      jmb.getChainNames().add(chid);
 -      jmb.getChainFile().put(chid, file);
 -    }
 -  }
 -
 -  private String fetchPdbFile(PDBEntry processingEntry) throws Exception
 -  {
 -    String filePath = null;
 -    Pdb pdbclient = new Pdb();
 -    AlignmentI pdbseq = null;
 -    String pdbid = processingEntry.getId();
 -    long handle = System.currentTimeMillis()
 -            + Thread.currentThread().hashCode();
 -
 -    /*
 -     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
 -     */
 -    String msg = MessageManager.formatMessage("status.fetching_pdb",
 -            new Object[]
 -            { pdbid });
 -    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 -    // long hdl = startProgressBar(MessageManager.formatMessage(
 -    // "status.fetching_pdb", new Object[]
 -    // { pdbid }));
 -    try
 -    {
 -      pdbseq = pdbclient.getSequenceRecords(pdbid);
 -    } catch (OutOfMemoryError oomerror)
 -    {
 -      new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
 -    } finally
 -    {
 -      msg = pdbid + " " + MessageManager.getString("label.state_completed");
 -      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
 -      // stopProgressBar(msg, hdl);
 -    }
 -    /*
 -     * If PDB data were saved and are not invalid (empty alignment), return the
 -     * file path.
 -     */
 -    if (pdbseq != null && pdbseq.getHeight() > 0)
 -    {
 -      // just use the file name from the first sequence's first PDBEntry
 -      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
 -              .elementAt(0).getFile()).getAbsolutePath();
 -      processingEntry.setFile(filePath);
 -    }
 -    return filePath;
 -  }
 -
 -  /**
 -   * Convenience method to update the progress bar if there is one. Be sure to
 -   * call stopProgressBar with the returned handle to remove the message.
 -   * 
 -   * @param msg
 -   * @param handle
 -   */
 -  public long startProgressBar(String msg)
 -  {
 -    // TODO would rather have startProgress/stopProgress as the
 -    // IProgressIndicator interface
 -    long tm = random.nextLong();
 -    if (progressBar != null)
 -    {
 -      progressBar.setProgressBar(msg, tm);
 -    }
 -    return tm;
 -  }
 -
 -  /**
 -   * End the progress bar with the specified handle, leaving a message (if not
 -   * null) on the status bar
 -   * 
 -   * @param msg
 -   * @param handle
 -   */
 -  public void stopProgressBar(String msg, long handle)
 -  {
 -    if (progressBar != null)
 -    {
 -      progressBar.setProgressBar(msg, handle);
 -    }
 -  }
 -
    @Override
-   public void eps_actionPerformed()
+   public void makePDBImage(TYPE imageType)
    {
-     throw new Error(MessageManager
-             .getString("error.eps_generation_not_implemented"));
-   }
-   @Override
-   public void png_actionPerformed()
-   {
-     throw new Error(MessageManager
-             .getString("error.png_generation_not_implemented"));
+     throw new UnsupportedOperationException(
+             "Image export for Chimera is not implemented");
    }
  
    @Override
Simple merge
@@@ -46,24 -64,6 +45,25 @@@ import javax.swing.JRadioButtonMenuItem
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
 +import jalview.api.AlignmentViewPanel;
 +import jalview.bin.Cache;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.PDBEntry;
 +import jalview.datamodel.SequenceI;
++import jalview.gui.JalviewColourChooser.ColourChooserListener;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.gui.ViewSelectionMenu.ViewSetProvider;
 +import jalview.io.DataSourceType;
 +import jalview.io.JalviewFileChooser;
 +import jalview.io.JalviewFileView;
 +import jalview.jbgui.GStructureViewer;
 +import jalview.schemes.ColourSchemeI;
 +import jalview.schemes.ColourSchemes;
 +import jalview.structure.StructureMapping;
 +import jalview.structures.models.AAStructureBindingModel;
 +import jalview.util.MessageManager;
 +import jalview.ws.dbsources.Pdb;
 +
  /**
   * Base class with common functionality for JMol, Chimera or other structure
   * viewers.
@@@ -800,16 -810,23 +800,23 @@@ public abstract class StructureViewerBa
      return reply;
    }
  
+   /**
+    * Opens a colour chooser dialog, and applies the chosen colour to the
+    * background of the structure viewer
+    */
    @Override
 -  public void background_actionPerformed(ActionEvent actionEvent)
 +  public void background_actionPerformed()
    {
-     Color col = JColorChooser.showDialog(this,
-             MessageManager.getString("label.select_background_colour"),
-             null);
-     if (col != null)
+     String ttl = MessageManager.getString("label.select_background_colour");
+     ColourChooserListener listener = new ColourChooserListener()
      {
-       getBinding().setBackgroundColour(col);
-     }
+       @Override
+       public void colourSelected(Color c)
+       {
+         getBinding().setBackgroundColour(c);
+       }
+     };
+     JalviewColourChooser.showColourChooser(this, ttl, null, listener);
    }
  
    @Override
    }
  
    @Override
 -  public void pdbFile_actionPerformed(ActionEvent actionEvent)
 +  public void pdbFile_actionPerformed()
    {
+     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
      JalviewFileChooser chooser = new JalviewFileChooser(
              Cache.getProperty("LAST_DIRECTORY"));
  
@@@ -71,21 -87,6 +71,22 @@@ import javax.swing.event.ChangeListener
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  
 +import jalview.bin.Cache;
 +import jalview.fts.core.FTSDataColumnPreferences;
 +import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
 +import jalview.fts.service.pdb.PDBFTSRestClient;
 +import jalview.gui.Desktop;
 +import jalview.gui.JalviewBooleanRadioButtons;
 +import jalview.gui.JvOptionPane;
 +import jalview.gui.JvSwingUtils;
 +import jalview.gui.StructureViewer.ViewerType;
 +import jalview.io.BackupFilenameParts;
 +import jalview.io.BackupFiles;
 +import jalview.io.BackupFilesPresetEntry;
 +import jalview.io.IntKeyStringValueEntry;
 +import jalview.util.MessageManager;
++import jalview.util.Platform;
 +
  /**
   * Base class for the Preferences panel.
   * 
@@@ -1327,9 -1354,20 +1365,20 @@@ public class GPreferences extends JPane
      ypos += lineSpacing;
      FTSDataColumnPreferences docFieldPref = new FTSDataColumnPreferences(
              PreferenceSource.PREFERENCES, PDBFTSRestClient.getInstance());
 -    docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120));
 +    docFieldPref.setBounds(new Rectangle(10, ypos, 470, 120));
      structureTab.add(docFieldPref);
  
+     /*
+      * hide Chimera options in JalviewJS
+      */
+     if (Platform.isJS()) 
+     {
 -      pathLabel.setVisible(false);
 -      chimeraPath.setVisible(false);
++      structureViewerPathLabel.setVisible(false);
++      structureViewerPath.setVisible(false);
+       viewerLabel.setVisible(false);
+       structViewer.setVisible(false);
+     }
+     
      return structureTab;
    }
  
   */
  package jalview.jbgui;
  
--import jalview.api.structures.JalviewStructureDisplayI;
--import jalview.gui.ColourMenuHelper.ColourChangeListener;
- import jalview.util.MessageManager;
 -import jalview.util.ImageMaker.TYPE;
 -import jalview.util.MessageManager;
 -
  import java.awt.BorderLayout;
  import java.awt.GridLayout;
  import java.awt.event.ActionEvent;
@@@ -37,6 -38,7 +33,12 @@@ import javax.swing.JMenuItem
  import javax.swing.JPanel;
  import javax.swing.JRadioButtonMenuItem;
  
++import jalview.api.structures.JalviewStructureDisplayI;
++import jalview.gui.ColourMenuHelper.ColourChangeListener;
++import jalview.util.ImageMaker.TYPE;
++import jalview.util.MessageManager;
++
+ @SuppressWarnings("serial")
  public abstract class GStructureViewer extends JInternalFrame
          implements JalviewStructureDisplayI, ColourChangeListener
  {
@@@ -24,55 -24,6 +24,56 @@@ import static jalview.math.RotatableMat
  import static jalview.math.RotatableMatrix.Axis.Y;
  import static jalview.math.RotatableMatrix.Axis.Z;
  
 +import java.awt.Color;
 +import java.awt.Font;
 +import java.awt.Rectangle;
 +import java.io.BufferedReader;
++import java.io.ByteArrayInputStream;
 +import java.io.DataOutputStream;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.FileOutputStream;
 +import java.io.IOException;
 +import java.io.InputStream;
 +import java.io.InputStreamReader;
 +import java.io.OutputStream;
 +import java.io.OutputStreamWriter;
 +import java.io.PrintWriter;
 +import java.lang.reflect.InvocationTargetException;
 +import java.math.BigInteger;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Collections;
 +import java.util.Enumeration;
 +import java.util.GregorianCalendar;
 +import java.util.HashMap;
 +import java.util.HashSet;
 +import java.util.Hashtable;
 +import java.util.IdentityHashMap;
 +import java.util.Iterator;
 +import java.util.LinkedHashMap;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Map.Entry;
 +import java.util.Set;
 +import java.util.Vector;
 +import java.util.jar.JarEntry;
 +import java.util.jar.JarInputStream;
 +import java.util.jar.JarOutputStream;
 +
 +import javax.swing.JInternalFrame;
 +import javax.swing.SwingUtilities;
 +import javax.xml.bind.JAXBContext;
 +import javax.xml.bind.JAXBElement;
 +import javax.xml.bind.Marshaller;
 +import javax.xml.datatype.DatatypeConfigurationException;
 +import javax.xml.datatype.DatatypeFactory;
 +import javax.xml.datatype.XMLGregorianCalendar;
 +import javax.xml.stream.XMLInputFactory;
 +import javax.xml.stream.XMLStreamReader;
 +
  import jalview.analysis.Conservation;
  import jalview.analysis.PCA;
  import jalview.analysis.scoremodels.ScoreModels;