Merge remote-tracking branch
authorgmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 10 Jun 2015 14:23:33 +0000 (15:23 +0100)
committergmungoc <g.m.carstairs@dundee.ac.uk>
Wed, 10 Jun 2015 14:23:33 +0000 (15:23 +0100)
'origin/schema/JAL-1764_structureViewAttribute' into develop
JAL-1764 save structure viewer type in project file

1  2 
src/jalview/datamodel/StructureViewerModel.java
src/jalview/gui/Jalview2XML.java
src/jalview/jbgui/GStructureViewer.java

@@@ -27,8 -27,6 +27,11 @@@ public class StructureViewerMode
  
    private String stateData = "";
  
 +  private String viewId;
 +
++  // CHIMERA or JMOL (for projects from Jalview 2.9 on)
++  private String type;
++
    private Map<File, StructureData> fileData = new HashMap<File, StructureData>();
  
    public class StructureData
@@@ -38,6 -36,7 +41,6 @@@
      private String pdbId;
  
      private List<SequenceI> seqList;
 -
      // TODO and possibly a list of chains?
  
      /**
@@@ -86,7 -85,7 +89,7 @@@
  
    public StructureViewerModel(int x, int y, int width, int height,
            boolean alignWithPanel, boolean colourWithAlignPanel,
-           boolean colourByViewer, String viewId)
 -          boolean colourByViewer)
++          boolean colourByViewer, String viewId, String type)
    {
      this.x = x;
      this.y = y;
@@@ -95,7 -94,6 +98,8 @@@
      this.alignWithPanel = alignWithPanel;
      this.colourWithAlignPanel = colourWithAlignPanel;
      this.colourByViewer = colourByViewer;
 +    this.viewId = viewId;
++    this.type = type;
    }
  
    public int getX()
      this.fileData = fileData;
    }
  
 +  public String getViewId()
 +  {
 +    return this.viewId;
 +  }
++
++  public String getType()
++  {
++    return this.type;
++  }
  }
   */
  package jalview.gui;
  
 +import java.awt.Rectangle;
 +import java.io.BufferedReader;
 +import java.io.DataInputStream;
 +import java.io.DataOutputStream;
 +import java.io.File;
 +import java.io.FileInputStream;
 +import java.io.FileOutputStream;
 +import java.io.IOException;
 +import java.io.InputStreamReader;
 +import java.io.OutputStreamWriter;
 +import java.io.PrintWriter;
 +import java.lang.reflect.InvocationTargetException;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Enumeration;
 +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.StringTokenizer;
 +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.JOptionPane;
 +import javax.swing.SwingUtilities;
 +
 +import org.exolab.castor.xml.Marshaller;
 +import org.exolab.castor.xml.Unmarshaller;
 +
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
  import jalview.datamodel.AlignedCodonFrame;
@@@ -69,7 -30,6 +69,7 @@@ import jalview.datamodel.PDBEntry
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.StructureViewerModel;
  import jalview.datamodel.StructureViewerModel.StructureData;
 +import jalview.gui.StructureViewer.ViewerType;
  import jalview.schemabinding.version2.AlcodMap;
  import jalview.schemabinding.version2.AlcodonFrame;
  import jalview.schemabinding.version2.Annotation;
@@@ -123,6 -83,44 +123,6 @@@ import jalview.ws.params.ArgumentI
  import jalview.ws.params.AutoCalcSetting;
  import jalview.ws.params.WsParamSetI;
  
 -import java.awt.Rectangle;
 -import java.io.BufferedReader;
 -import java.io.DataInputStream;
 -import java.io.DataOutputStream;
 -import java.io.File;
 -import java.io.FileInputStream;
 -import java.io.FileOutputStream;
 -import java.io.IOException;
 -import java.io.InputStreamReader;
 -import java.io.OutputStreamWriter;
 -import java.io.PrintWriter;
 -import java.lang.reflect.InvocationTargetException;
 -import java.net.MalformedURLException;
 -import java.net.URL;
 -import java.util.ArrayList;
 -import java.util.Enumeration;
 -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.StringTokenizer;
 -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.JOptionPane;
 -import javax.swing.SwingUtilities;
 -
 -import org.exolab.castor.xml.Unmarshaller;
 -
  /**
   * Write out the current jalview desktop state as a Jalview XML stream.
   * 
   */
  public class Jalview2XML
  {
 +  private static final String UTF_8 = "UTF-8";
 +
    /*
     * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
     * of sequence objects are created.
        // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
        // //////////////////////////////////////////////////
  
 -      Vector shortNames = new Vector();
 +      List<String> shortNames = new ArrayList<String>();
  
        // REVERSE ORDER
        for (int i = frames.length - 1; i > -1; i--)
            continue;
          }
  
 -        String shortName = af.getTitle();
 -
 -        if (shortName.indexOf(File.separatorChar) > -1)
 -        {
 -          shortName = shortName.substring(shortName
 -                  .lastIndexOf(File.separatorChar) + 1);
 -        }
 -
 -        int count = 1;
 -
 -        while (shortNames.contains(shortName))
 -        {
 -          if (shortName.endsWith("_" + (count - 1)))
 -          {
 -            shortName = shortName.substring(0, shortName.lastIndexOf("_"));
 -          }
 -
 -          shortName = shortName.concat("_" + count);
 -          count++;
 -        }
 -
 -        shortNames.addElement(shortName);
 -
 -        if (!shortName.endsWith(".xml"))
 -        {
 -          shortName = shortName + ".xml";
 -        }
 +        String shortName = makeFilename(af, shortNames);
  
          int ap, apSize = af.alignPanels.size();
  
      }
    }
  
 +  /**
 +   * Generates a distinct file name, based on the title of the AlignFrame, by
 +   * appending _n for increasing n until an unused name is generated. The new
 +   * name (without its extension) is added to the list.
 +   * 
 +   * @param af
 +   * @param namesUsed
 +   * @return the generated name, with .xml extension
 +   */
 +  protected String makeFilename(AlignFrame af, List<String> namesUsed)
 +  {
 +    String shortName = af.getTitle();
 +
 +    if (shortName.indexOf(File.separatorChar) > -1)
 +    {
 +      shortName = shortName.substring(shortName
 +              .lastIndexOf(File.separatorChar) + 1);
 +    }
 +
 +    int count = 1;
 +
 +    while (namesUsed.contains(shortName))
 +    {
 +      if (shortName.endsWith("_" + (count - 1)))
 +      {
 +        shortName = shortName.substring(0, shortName.lastIndexOf("_"));
 +      }
 +
 +      shortName = shortName.concat("_" + count);
 +      count++;
 +    }
 +
 +    namesUsed.add(shortName);
 +
 +    if (!shortName.endsWith(".xml"))
 +    {
 +      shortName = shortName + ".xml";
 +    }
 +    return shortName;
 +  }
 +
    // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
    public boolean saveAlignment(AlignFrame af, String jarFile,
            String fileName)
            jalview.datamodel.PDBEntry entry = (jalview.datamodel.PDBEntry) en
                    .nextElement();
  
 -          pdb.setId(entry.getId());
 +          String pdbId = entry.getId();
 +          pdb.setId(pdbId);
            pdb.setType(entry.getType());
  
            /*
                StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
                matchedFile = saveStructureState(ap, jds, pdb, entry,
                        viewIds, matchedFile, viewFrame);
 +              /*
 +               * Only store each structure viewer's state once in each XML
 +               * document. First time through only (storeDS==false)
 +               */
 +              String viewId = viewFrame.getViewId();
 +              if (!storeDS && !viewIds.contains(viewId))
 +              {
 +                viewIds.add(viewId);
 +                try
 +                {
-                   writeJarEntry(jout, "viewer_" + viewId,
++                  writeJarEntry(jout, getViewerJarEntryName(viewId),
 +                          viewFrame.getStateInfo().getBytes());
 +                } catch (IOException e)
 +                {
 +                  System.err.println("Error saving viewer state: "
 +                          + e.getMessage());
 +                }
 +              }
              }
            }
  
                pdbfiles = new ArrayList<String>();
              }
  
 -            if (!pdbfiles.contains(entry.getId()))
 +            if (!pdbfiles.contains(pdbId))
              {
 -              pdbfiles.add(entry.getId());
 -              DataInputStream dis = null;
 -              try
 -              {
 -                File file = new File(matchedFile);
 -                if (file.exists() && jout != null)
 -                {
 -                  byte[] data = new byte[(int) file.length()];
 -                  jout.putNextEntry(new JarEntry(entry.getId()));
 -                  dis = new DataInputStream(new FileInputStream(file));
 -                  dis.readFully(data);
 -
 -                  DataOutputStream dout = new DataOutputStream(jout);
 -                  dout.write(data, 0, data.length);
 -                  dout.flush();
 -                  jout.closeEntry();
 -                }
 -              } catch (Exception ex)
 -              {
 -                ex.printStackTrace();
 -              } finally
 -              {
 -                if (dis != null)
 -                {
 -                  try
 -                  {
 -                    dis.close();
 -                  } catch (IOException e)
 -                  {
 -                    // ignore
 -                  }
 -                }
 -              }
 -
 +              pdbfiles.add(pdbId);
 +              copyFileToJar(jout, matchedFile, pdbId);
              }
            }
  
 -          if (entry.getProperty() != null)
 +          if (entry.getProperty() != null && !entry.getProperty().isEmpty())
            {
              PdbentryItem item = new PdbentryItem();
              Hashtable properties = entry.getProperty();
          }
        }
      }
 +
 +    /*
 +     * Save associated Varna panels
 +     */
 +    if (Desktop.desktop != null)
 +    {
 +      for (JInternalFrame frame : Desktop.desktop.getAllFrames())
 +      {
 +        if (frame instanceof AppVarna)
 +        {
 +          AppVarna vp = (AppVarna) frame;
 +          if (vp.ap == ap)
 +          {
 +            // save Varna state
 +          }
 +        }
 +      }
 +    }
 +
      // SAVE ANNOTATIONS
      /**
       * store forward refs from an annotationRow to any groups
          JarEntry entry = new JarEntry(fileName);
          jout.putNextEntry(entry);
          PrintWriter pout = new PrintWriter(new OutputStreamWriter(jout,
 -                "UTF-8"));
 -        org.exolab.castor.xml.Marshaller marshaller = new org.exolab.castor.xml.Marshaller(
 -                pout);
 +                UTF_8));
 +        Marshaller marshaller = new Marshaller(pout);
          marshaller.marshal(object);
          pout.flush();
          jout.closeEntry();
    }
  
    /**
 +   * Copy the contents of a file to a new file added to the output jar
 +   * 
 +   * @param jout
 +   * @param infilePath
 +   * @param jarfileName
 +   */
 +  protected void copyFileToJar(JarOutputStream jout, String infilePath,
 +          String jarfileName)
 +  {
 +    DataInputStream dis = null;
 +    try
 +    {
 +      File file = new File(infilePath);
 +      if (file.exists() && jout != null)
 +      {
 +        dis = new DataInputStream(new FileInputStream(file));
 +        byte[] data = new byte[(int) file.length()];
 +        dis.readFully(data);
 +        writeJarEntry(jout, jarfileName, data);
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    } finally
 +    {
 +      if (dis != null)
 +      {
 +        try
 +        {
 +          dis.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Write the data to a new entry of given name in the output jar file
 +   * 
 +   * @param jout
 +   * @param jarfileName
 +   * @param data
 +   * @throws IOException
 +   */
 +  protected void writeJarEntry(JarOutputStream jout, String jarfileName,
 +          byte[] data) throws IOException
 +  {
 +    if (jout != null)
 +    {
 +      jout.putNextEntry(new JarEntry(jarfileName));
 +      DataOutputStream dout = new DataOutputStream(jout);
 +      dout.write(data, 0, data.length);
 +      dout.flush();
 +      jout.closeEntry();
 +    }
 +  }
 +
 +  /**
     * Save the state of a structure viewer
     * 
     * @param ap
            String matchedFile, StructureViewerBase viewFrame)
    {
      final AAStructureBindingModel bindingModel = viewFrame.getBinding();
 +
 +    /*
 +     * Look for any bindings for this viewer to the PDB file of interest
 +     * (including part matches excluding chain id)
 +     */
      for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
      {
        final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
                && !(entry.getId().length() > 4 && entry.getId()
                        .toLowerCase().startsWith(pdbId.toLowerCase())))
        {
 +        /*
 +         * not interested in a binding to a different PDB entry here
 +         */
          continue;
        }
        if (matchedFile == null)
        // can get at it if the ID
        // match is ambiguous (e.g.
        // 1QIP==1qipA)
 -      String statestring = viewFrame.getStateInfo();
  
        for (int smap = 0; smap < viewFrame.getBinding().getSequence()[peid].length; smap++)
        {
            state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
            state.setColourwithAlignPanel(viewFrame.isUsedforcolourby(ap));
            state.setColourByJmol(viewFrame.isColouredByViewer());
-           // FIXME add attribute to schema and enable next line
-           // state.setType(viewFrame.getViewerType().toString());
-           state.setContent(viewFrame.getViewerType().toString());
 -          /*
 -           * Only store each structure viewer's state once in each XML document.
 -           */
 -          if (!viewIds.contains(viewId))
 -          {
 -            viewIds.add(viewId);
 -            state.setContent(statestring.replaceAll("\n", ""));
 -          }
 -          else
 -          {
 -            state.setContent("# duplicate state");
 -          }
++          state.setType(viewFrame.getViewerType().toString());
            pdb.addStructureState(state);
          }
        }
  
          if (jarentry != null && jarentry.getName().endsWith(".xml"))
          {
 -          InputStreamReader in = new InputStreamReader(jin, "UTF-8");
 +          InputStreamReader in = new InputStreamReader(jin, UTF_8);
            JalviewModel object = new JalviewModel();
  
            Unmarshaller unmar = new Unmarshaller(object);
     */
    private final boolean updateLocalViews = false;
  
 +  /**
 +   * Returns the path to a temporary file holding the PDB file for the given PDB
 +   * id. The first time of asking, searches for a file of that name in the
 +   * Jalview project jar, and copies it to a new temporary file. Any repeat
 +   * requests just return the path to the file previously created.
 +   * 
 +   * @param jprovider
 +   * @param pdbId
 +   * @return
 +   */
    String loadPDBFile(jarInputStreamProvider jprovider, String pdbId)
    {
      if (alreadyLoadedPDB.containsKey(pdbId))
        return alreadyLoadedPDB.get(pdbId).toString();
      }
  
 +    String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb");
 +    if (tempFile != null)
 +    {
 +      alreadyLoadedPDB.put(pdbId, tempFile);
 +    }
 +    return tempFile;
 +  }
 +
 +  /**
 +   * Copies the jar entry of given name to a new temporary file and returns the
 +   * path to the file, or null if the entry is not found.
 +   * 
 +   * @param jprovider
 +   * @param jarEntryName
 +   * @param prefix
 +   *          a prefix for the temporary file name, must be at least three
 +   *          characters long
 +   * @return
 +   */
 +  protected String copyJarEntry(jarInputStreamProvider jprovider,
 +          String jarEntryName, String prefix)
 +  {
 +    BufferedReader in = null;
 +    PrintWriter out = null;
 +
      try
      {
        JarInputStream jin = jprovider.getJarInputStream();
        do
        {
          entry = jin.getNextJarEntry();
 -      } while (entry != null && !entry.getName().equals(pdbId));
 +      } while (entry != null && !entry.getName().equals(jarEntryName));
        if (entry != null)
        {
 -        BufferedReader in = new BufferedReader(new InputStreamReader(jin));
 -        File outFile = File.createTempFile("jalview_pdb", ".txt");
 +        in = new BufferedReader(new InputStreamReader(jin, UTF_8));
 +        File outFile = File.createTempFile(prefix, ".tmp");
          outFile.deleteOnExit();
 -        PrintWriter out = new PrintWriter(new FileOutputStream(outFile));
 +        out = new PrintWriter(new FileOutputStream(outFile));
          String data;
  
          while ((data = in.readLine()) != null)
          {
            out.println(data);
          }
 -        try
 -        {
 -          out.flush();
 -        } catch (Exception foo)
 -        {
 -        }
 -        ;
 -        out.close();
 +        out.flush();
          String t = outFile.getAbsolutePath();
 -        alreadyLoadedPDB.put(pdbId, t);
          return t;
        }
        else
        {
 -        warn("Couldn't find PDB file entry in Jalview Jar for " + pdbId);
 +        warn("Couldn't find entry in Jalview Jar for " + jarEntryName);
        }
      } catch (Exception ex)
      {
        ex.printStackTrace();
 +    } finally
 +    {
 +      if (in != null)
 +      {
 +        try
 +        {
 +          in.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +      if (out != null)
 +      {
 +        out.close();
 +      }
      }
  
      return null;
              }
              if (!structureViewers.containsKey(sviewid))
              {
 -              structureViewers.put(sviewid, new StructureViewerModel(x, y,
 -                      width, height, false, false, true));
 +              structureViewers.put(sviewid,
 +                      new StructureViewerModel(x, y, width, height, false,
-                               false, true, structureState.getViewId()));
++                              false, true, structureState.getViewId(),
++                              structureState.getType()));
                // Legacy pre-2.7 conversion JAL-823 :
                // do not assume any view has to be linked for colour by
                // sequence
      // Instantiate the associated structure views
      for (Entry<String, StructureViewerModel> entry : structureViewers
              .entrySet())
 +    {
 +      try
        {
 -      createOrLinkStructureViewer(entry, af, ap);
 +        createOrLinkStructureViewer(entry, af, ap, jprovider);
 +      } catch (Exception e)
 +      {
 +        System.err.println("Error loading structure viewer: "
 +                + e.getMessage());
 +        // failed - try the next one
 +      }
      }
    }
  
     * @param viewerData
     * @param af
     * @param ap
 +   * @param jprovider
     */
    protected void createOrLinkStructureViewer(
            Entry<String, StructureViewerModel> viewerData, AlignFrame af,
 -          AlignmentPanel ap)
 +          AlignmentPanel ap, jarInputStreamProvider jprovider)
    {
 -    final StructureViewerModel svattrib = viewerData.getValue();
 +    final StructureViewerModel stateData = viewerData.getValue();
  
      /*
       * Search for any viewer windows already open from other alignment views
  
      if (comp != null)
      {
 -      linkStructureViewer(ap, comp, svattrib);
 +      linkStructureViewer(ap, comp, stateData);
        return;
      }
  
      /*
-      * 2.8.2: stateData contains "chimera..." (session file name), or JMOL state
-      * string
-      * 
-      * 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
 -     * Pending an XML element for ViewerType, just check if stateData contains
 -     * "chimera" (part of the chimera session filename).
++     * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
 +     * "viewer_"+stateData.viewId
       */
-     // FIXME use stateData.getType() instead once schema updated
-     if (ViewerType.CHIMERA.toString().equals(stateData.getStateData())
-             || stateData.getStateData().indexOf("chimera") > -1)
 -    if (svattrib.getStateData().indexOf("chimera") > -1)
++    if (ViewerType.CHIMERA.toString().equals(stateData.getType()))
      {
 -      createChimeraViewer(viewerData, af);
 +      createChimeraViewer(viewerData, af, jprovider);
      }
      else
      {
 -      createJmolViewer(viewerData, af);
++      /*
++       * else Jmol (if pre-2.9, stateData contains JMOL state string)
++       */
 +      createJmolViewer(viewerData, af, jprovider);
      }
    }
  
    /**
     * Create a new Chimera viewer.
     * 
 -   * @param viewerData
 +   * @param data
     * @param af
 +   * @param jprovider
     */
 -  protected void createChimeraViewer(
 -          Entry<String, StructureViewerModel> viewerData, AlignFrame af)
 +  protected void createChimeraViewer(Entry<String, StructureViewerModel> viewerData,
 +          AlignFrame af,
 +          jarInputStreamProvider jprovider)
    {
 -    final StructureViewerModel data = viewerData.getValue();
 -    String chimeraSession = data.getStateData();
 -
 -    if (new File(chimeraSession).exists())
 -    {
 -      Set<Entry<File, StructureData>> fileData = data.getFileData()
 -              .entrySet();
 -      List<PDBEntry> pdbs = new ArrayList<PDBEntry>();
 -      List<SequenceI[]> allseqs = new ArrayList<SequenceI[]>();
 -      for (Entry<File, StructureData> pdb : fileData)
 -      {
 -        String filePath = pdb.getValue().getFilePath();
 -        String pdbId = pdb.getValue().getPdbId();
 -        // pdbs.add(new PDBEntry(filePath, pdbId));
 -        pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath));
 -        final List<SequenceI> seqList = pdb.getValue().getSeqList();
 -        SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]);
 -        allseqs.add(seqs);
 -      }
 +    StructureViewerModel data = viewerData.getValue();
 +    String chimeraSessionFile =  data.getStateData();
  
 -      boolean colourByChimera = data.isColourByViewer();
 -      boolean colourBySequence = data.isColourWithAlignPanel();
 -
 -      // TODO can/should this be done via StructureViewer (like Jmol)?
 -      final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]);
 -      final SequenceI[][] seqsArray = allseqs.toArray(new SequenceI[allseqs
 -              .size()][]);
 -      new ChimeraViewFrame(chimeraSession, af.alignPanel, pdbArray,
 -              seqsArray, colourByChimera, colourBySequence);
 -    }
 -    else
 -    {
 -      Cache.log.error("Chimera session file " + chimeraSession
 -              + " not found");
 -    }
 +    /*
 +     * Copy Chimera session from jar entry "viewer_"+viewId to a temporary file
 +     * 
 +     * Note this is the 'saved' viewId as in the project file XML, _not_ the
 +     * 'uniquified' sviewid used to reconstruct the viewer here
 +     */
 +    chimeraSessionFile = copyJarEntry(jprovider,
-             "viewer_" + data.getViewId(), "chimera");
++            getViewerJarEntryName(data.getViewId()), "chimera");
 +
 +    Set<Entry<File, StructureData>> fileData = data.getFileData()
 +            .entrySet();
 +    List<PDBEntry> pdbs = new ArrayList<PDBEntry>();
 +    List<SequenceI[]> allseqs = new ArrayList<SequenceI[]>();
 +    for (Entry<File, StructureData> pdb : fileData)
 +    {
 +      String filePath = pdb.getValue().getFilePath();
 +      String pdbId = pdb.getValue().getPdbId();
 +      // pdbs.add(new PDBEntry(filePath, pdbId));
 +      pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath));
 +      final List<SequenceI> seqList = pdb.getValue().getSeqList();
 +      SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]);
 +      allseqs.add(seqs);
 +    }
 +
 +    boolean colourByChimera = data.isColourByViewer();
 +    boolean colourBySequence = data.isColourWithAlignPanel();
 +
 +    // TODO use StructureViewer as a factory here, see JAL-1761
 +    final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]);
 +    final SequenceI[][] seqsArray = allseqs.toArray(new SequenceI[allseqs
 +            .size()][]);
 +    String newViewId = viewerData.getKey();
 +    new ChimeraViewFrame(chimeraSessionFile, af.alignPanel, pdbArray,
 +            seqsArray, colourByChimera, colourBySequence, newViewId);
    }
  
    /**
     * 
     * @param viewerData
     * @param af
 +   * @param jprovider
     */
    protected void createJmolViewer(
            final Entry<String, StructureViewerModel> viewerData,
 -          AlignFrame af)
 +          AlignFrame af, jarInputStreamProvider jprovider)
    {
      final StructureViewerModel svattrib = viewerData.getValue();
      String state = svattrib.getStateData();
 +
 +    /*
 +     * Pre-2.9: state element value is the Jmol state string
 +     * 
 +     * 2.9+: @type is "JMOL", state data is in a Jar file member named "viewer_"
 +     * + viewId
 +     */
-     // FIXME use getType once Castor regenerated for new attribute
-     if (ViewerType.JMOL.toString().equals(state /* svattrib.getType() */))
++    if (ViewerType.JMOL.toString().equals(svattrib.getType()))
 +    {
-       state = readJarEntry(jprovider, "viewer_" + svattrib.getViewId());
++      state = readJarEntry(jprovider,
++              getViewerJarEntryName(svattrib.getViewId()));
 +    }
 +
      List<String> pdbfilenames = new ArrayList<String>();
      List<SequenceI[]> seqmaps = new ArrayList<SequenceI[]>();
      List<String> pdbids = new ArrayList<String>();
              JalviewStructureDisplayI sview = null;
              try
              {
 -              // JAL-1333 note - we probably can't migrate Jmol views to UCSF
 -              // Chimera!
                sview = new StructureViewer(alf.alignPanel
                        .getStructureSelectionManager()).createView(
                        StructureViewer.ViewerType.JMOL, pdbf, id, sq,
    }
  
    /**
++   * Generates a name for the entry in the project jar file to hold state
++   * information for a structure viewer
++   * 
++   * @param viewId
++   * @return
++   */
++  protected String getViewerJarEntryName(String viewId)
++  {
++    return "viewer_" + viewId;
++  }
++
++  /**
     * Returns any open frame that matches given structure viewer data. The match
     * is based on the unique viewId, or (for older project versions) the frame's
     * geometry.
                  && ((StructureViewerBase) frame).getViewId()
                          .equals(sviewid))
          {
 -          comp = (AppJmol) frame;
 -          // todo: break?
 +          comp = (StructureViewerBase) frame;
 +          break; // break added in 2.9
          }
          /*
           * Otherwise test for matching position and size of viewer frame
                  && frame.getHeight() == svattrib.getHeight()
                  && frame.getWidth() == svattrib.getWidth())
          {
 -          comp = (AppJmol) frame;
 -          // todo: break?
 +          comp = (StructureViewerBase) frame;
 +          // no break in faint hope of an exact match on viewId
          }
        }
      }
     * @param viewerColouring
     */
    protected void linkStructureViewer(AlignmentPanel ap,
 -          StructureViewerBase viewer, StructureViewerModel svattrib)
 +          StructureViewerBase viewer, StructureViewerModel stateData)
    {
      // NOTE: if the jalview project is part of a shared session then
      // view synchronization should/could be done here.
  
 -    final boolean useinViewerSuperpos = svattrib.isAlignWithPanel();
 -    final boolean usetoColourbyseq = svattrib.isColourWithAlignPanel();
 -    final boolean viewerColouring = svattrib.isColourByViewer();
 -    Map<File, StructureData> oldFiles = svattrib.getFileData();
 +    final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
 +    final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
 +    final boolean viewerColouring = stateData.isColourByViewer();
 +    Map<File, StructureData> oldFiles = stateData.getFileData();
  
      /*
       * Add mapping for sequences in this view to an already open viewer
    {
      skipList = skipList2;
    }
 +
 +  /**
 +   * Reads the jar entry of given name and returns its contents, or null if the
 +   * entry is not found.
 +   * 
 +   * @param jprovider
 +   * @param jarEntryName
 +   * @return
 +   */
 +  protected String readJarEntry(jarInputStreamProvider jprovider,
 +          String jarEntryName)
 +  {
 +    String result = null;
 +    BufferedReader in = null;
 +
 +    try
 +    {
 +      /*
 +       * Reopen the jar input stream and traverse its entries to find a matching
 +       * name
 +       */
 +      JarInputStream jin = jprovider.getJarInputStream();
 +      JarEntry entry = null;
 +      do
 +      {
 +        entry = jin.getNextJarEntry();
 +      } while (entry != null && !entry.getName().equals(jarEntryName));
 +
 +      if (entry != null)
 +      {
 +        StringBuilder out = new StringBuilder(256);
 +        in = new BufferedReader(new InputStreamReader(jin, UTF_8));
 +        String data;
 +
 +        while ((data = in.readLine()) != null)
 +        {
 +          out.append(data);
 +        }
 +        result = out.toString();
 +      }
 +      else
 +      {
 +        warn("Couldn't find entry in Jalview Jar for " + jarEntryName);
 +      }
 +    } catch (Exception ex)
 +    {
 +      ex.printStackTrace();
 +    } finally
 +    {
 +      if (in != null)
 +      {
 +        try
 +        {
 +          in.close();
 +        } catch (IOException e)
 +        {
 +          // ignore
 +        }
 +      }
 +    }
 +  
 +    return result;
 +  }
  }
@@@ -348,6 -348,6 +348,7 @@@ public abstract class GStructureViewer 
      colourButtons.add(strandColour);
      colourButtons.add(turnColour);
      colourButtons.add(buriedColour);
++    colourButtons.add(purinePyrimidineColour);
      colourButtons.add(userColour);
      colourButtons.add(viewerColour);