Merge branch 'develop' into bug/JAL-4290_headless_alignment_export_with_structure_ann...
[jalview.git] / src / jalview / bin / Commands.java
index 8312b29..b6a5a25 100644 (file)
@@ -1,5 +1,26 @@
+/*
+ * 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.
+ *  
+ * 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/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.bin;
 
+import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -12,12 +33,13 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+import javax.swing.SwingUtilities;
+
 import jalview.analysis.AlignmentUtils;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Jalview.ExitCode;
 import jalview.bin.argparser.Arg;
 import jalview.bin.argparser.ArgParser;
-import jalview.bin.argparser.ArgParser.Position;
 import jalview.bin.argparser.ArgValue;
 import jalview.bin.argparser.ArgValuesMap;
 import jalview.bin.argparser.SubVals;
@@ -47,14 +69,17 @@ import jalview.io.NewickFile;
 import jalview.io.exceptions.ImageOutputException;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureImportSettings.TFType;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.ColorUtils;
 import jalview.util.FileUtils;
 import jalview.util.HttpUtils;
 import jalview.util.ImageMaker;
 import jalview.util.ImageMaker.TYPE;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.StringUtils;
 import jalview.util.imagemaker.BitmapImageSizing;
 
 public class Commands
@@ -142,15 +167,19 @@ public class Commands
 
     }
 
-    // report errors
-    Console.debug("All errors from command line argument commands:\n"
-            + errorsToString());
+    // report errors - if any
+    String errorsRaised = errorsToString();
+    if (errorsRaised.trim().length() > 0)
+    {
+      Console.warn(
+              "The following errors and warnings occurred whilst processing files:\n"
+                      + errorsRaised);
+    }
     // gui errors reported in Jalview
 
     if (argParser.getBoolean(Arg.QUIT))
     {
-      Jalview.getInstance().exit(
-              "Exiting due to " + Arg.QUIT.argString() + " argument.",
+      Jalview.exit("Exiting due to " + Arg.QUIT.argString() + " argument.",
               ExitCode.OK);
       return true;
     }
@@ -178,19 +207,23 @@ public class Commands
       return true;
     }
 
-    boolean isError = false;
+    Boolean isError = Boolean.valueOf(false);
 
-    // set wrap scope here so it can be applied after structures are opened
+    // set wrap, showSSAnnotations, showAnnotations and hideTFrows scope here so
+    // it can be applied after structures are opened
     boolean wrap = false;
+    boolean showSSAnnotations = false;
+    boolean showAnnotations = false;
+    boolean hideTFrows = false;
+    AlignFrame af = null;
 
     if (avm.containsArg(Arg.APPEND) || avm.containsArg(Arg.OPEN))
     {
       commandArgsProvided = true;
-      long progress = -1;
+      final long progress = System.currentTimeMillis();
 
       boolean first = true;
       boolean progressBarSet = false;
-      AlignFrame af;
       // Combine the APPEND and OPEN files into one list, along with whether it
       // was APPEND or OPEN
       List<ArgValue> openAvList = new ArrayList<>();
@@ -212,10 +245,18 @@ public class Commands
           first = false;
           if (!headless && desktop != null)
           {
-            desktop.setProgressBar(
-                    MessageManager.getString(
-                            "status.processing_commandline_args"),
-                    progress = System.currentTimeMillis());
+            SwingUtilities.invokeLater(new Runnable()
+            {
+              @Override
+              public void run()
+              {
+                desktop.setProgressBar(
+                        MessageManager.getString(
+                                "status.processing_commandline_args"),
+                        progress);
+
+              }
+            });
             progressBarSet = true;
           }
         }
@@ -289,29 +330,22 @@ public class Commands
             }
           }
 
-          // colour alignment?
-          String colour = ArgParser.getFromSubValArgOrPref(avm, av,
-                  Arg.COLOUR, sv, null, "DEFAULT_COLOUR_PROT", "");
-          if ("" != colour)
+          // colour alignment
+          String colour = null;
+          if (avm.containsArg(Arg.COLOUR)
+                  || !(format == FileFormat.Jalview))
           {
-            ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
-                    af.getViewport(), af.getViewport().getAlignment(),
-                    colour);
-
-            if (cs == null && !"None".equals(colour))
-            {
-              addWarn("Couldn't parse '" + colour + "' as a colourscheme.");
-            }
-            else
-            {
-              af.changeColour(cs);
-            }
-            Jalview.testoutput(argParser, Arg.COLOUR, "zappo", colour);
+            colour = avm.getFromSubValArgOrPref(av, Arg.COLOUR, sv, null,
+                    "DEFAULT_COLOUR_PROT", null);
+          }
+          if (colour != null)
+          {
+            this.colourAlignFrame(af, colour);
           }
 
           // Change alignment frame title
-          String title = ArgParser.getFromSubValArgOrPref(avm, av,
-                  Arg.TITLE, sv, null, null, null);
+          String title = avm.getFromSubValArgOrPref(av, Arg.TITLE, sv, null,
+                  null, null);
           if (title != null)
           {
             af.setTitle(title);
@@ -319,7 +353,7 @@ public class Commands
           }
 
           // Add features
-          String featuresfile = ArgParser.getValueFromSubValOrArg(avm, av,
+          String featuresfile = avm.getValueFromSubValOrArg(av,
                   Arg.FEATURES, sv);
           if (featuresfile != null)
           {
@@ -330,8 +364,8 @@ public class Commands
           }
 
           // Add annotations from file
-          String annotationsfile = ArgParser.getValueFromSubValOrArg(avm,
-                  av, Arg.ANNOTATIONS, sv);
+          String annotationsfile = avm.getValueFromSubValOrArg(av,
+                  Arg.ANNOTATIONS, sv);
           if (annotationsfile != null)
           {
             af.loadJalviewDataFile(annotationsfile, null, null, null);
@@ -341,8 +375,8 @@ public class Commands
           }
 
           // Set or clear the sortbytree flag
-          boolean sortbytree = ArgParser.getBoolFromSubValOrArg(avm,
-                  Arg.SORTBYTREE, sv);
+          boolean sortbytree = avm.getBoolFromSubValOrArg(Arg.SORTBYTREE,
+                  sv);
           if (sortbytree)
           {
             af.getViewport().setSortByTree(true);
@@ -350,8 +384,7 @@ public class Commands
           }
 
           // Load tree from file
-          String treefile = ArgParser.getValueFromSubValOrArg(avm, av,
-                  Arg.TREE, sv);
+          String treefile = avm.getValueFromSubValOrArg(av, Arg.TREE, sv);
           if (treefile != null)
           {
             try
@@ -370,31 +403,20 @@ public class Commands
           }
 
           // Show secondary structure annotations?
-          boolean showSSAnnotations = ArgParser.getFromSubValArgOrPref(avm,
+          showSSAnnotations = avm.getFromSubValArgOrPref(
                   Arg.SHOWSSANNOTATIONS, av.getSubVals(), null,
                   "STRUCT_FROM_PDB", true);
-          af.setAnnotationsVisibility(showSSAnnotations, true, false);
-
           // Show sequence annotations?
-          boolean showAnnotations = ArgParser.getFromSubValArgOrPref(avm,
-                  Arg.SHOWANNOTATIONS, av.getSubVals(), null,
-                  "SHOW_ANNOTATIONS", true);
-          af.setAnnotationsVisibility(showAnnotations, false, true);
+          showAnnotations = avm.getFromSubValArgOrPref(Arg.SHOWANNOTATIONS,
+                  av.getSubVals(), null, "SHOW_ANNOTATIONS", true);
+          // hide the Temperature Factor row?
+          hideTFrows = (avm.getBoolean(Arg.NOTEMPFAC));
 
-          // show temperature factor annotations?
-          if (avm.getBoolean(Arg.NOTEMPFAC))
-          {
-            // do this better (annotation types?)
-            List<String> hideThese = new ArrayList<>();
-            hideThese.add("Temperature Factor");
-            hideThese.add(AlphaFoldAnnotationRowBuilder.LABEL);
-            AlignmentUtils.showOrHideSequenceAnnotations(
-                    af.getCurrentView().getAlignment(), hideThese, null,
-                    false, false);
-          }
+          // showSSAnnotations, showAnnotations, hideTFrows used after opening
+          // structure
 
           // wrap alignment? do this last for formatting reasons
-          wrap = ArgParser.getFromSubValArgOrPref(avm, Arg.WRAP, sv, null,
+          wrap = avm.getFromSubValArgOrPref(Arg.WRAP, sv, null,
                   "WRAP_ALIGNMENT", false);
           // af.setWrapFormat(wrap) is applied after structures are opened for
           // annotation reasons
@@ -416,9 +438,11 @@ public class Commands
         {
           Console.debug(
                   "Opening '" + openFile + "' in existing alignment frame");
+
           DataSourceType dst = HttpUtils.startsWithHttpOrHttps(openFile)
                   ? DataSourceType.URL
                   : DataSourceType.FILE;
+
           FileLoader fileLoader = new FileLoader(!headless);
           fileLoader.LoadFile(af.getCurrentView(), openFile, dst, null,
                   false);
@@ -447,15 +471,20 @@ public class Commands
     // open the structure (from same PDB file or given PDBfile)
     if (!avm.getBoolean(Arg.NOSTRUCTURE))
     {
-      AlignFrame af = afMap.get(id);
+      if (af == null)
+      {
+        af = afMap.get(id);
+      }
       if (avm.containsArg(Arg.STRUCTURE))
       {
         commandArgsProvided = true;
-        for (ArgValue av : avm.getArgValueList(Arg.STRUCTURE))
+        for (ArgValue structureAv : avm.getArgValueList(Arg.STRUCTURE))
         {
-          String val = av.getValue();
-          SubVals subVals = av.getSubVals();
-          SequenceI seq = getSpecifiedSequence(af, avm, av);
+          argParser.setStructureFilename(null);
+          String val = structureAv.getValue();
+          SubVals subVals = structureAv.getSubVals();
+          int argIndex = structureAv.getArgIndex();
+          SequenceI seq = getSpecifiedSequence(af, avm, structureAv);
           if (seq == null)
           {
             // Could not find sequence from subId, let's assume the first
@@ -470,35 +499,31 @@ public class Commands
                     + Arg.STRUCTURE.argString() + "=" + val);
             continue;
           }
+          String structureFilename = null;
           File structureFile = null;
           if (subVals.getContent() != null
                   && subVals.getContent().length() != 0)
           {
-            structureFile = new File(subVals.getContent());
+            structureFilename = subVals.getContent();
             Console.debug("Using structure file (from argument) '"
-                    + structureFile.getAbsolutePath() + "'");
+                    + structureFilename + "'");
+            structureFile = new File(structureFilename);
           }
-          // TRY THIS
-          /*
-           * PDBEntry fileEntry = new AssociatePdbFileWithSeq()
-           * .associatePdbWithSeq(selectedPdbFileName, DataSourceType.FILE,
-           * selectedSequence, true, Desktop.instance);
-           * 
-           * sViewer = launchStructureViewer(ssm, new PDBEntry[] { fileEntry }, ap, new
-           * SequenceI[] { selectedSequence });
-           * 
-           */
           /* THIS DOESN'T WORK */
           else if (seq.getAllPDBEntries() != null
                   && seq.getAllPDBEntries().size() > 0)
           {
             structureFile = new File(
                     seq.getAllPDBEntries().elementAt(0).getFile());
-            Console.debug("Using structure file (from sequence) '"
-                    + structureFile.getAbsolutePath() + "'");
+            if (structureFile != null)
+            {
+              Console.debug("Using structure file (from sequence) '"
+                      + structureFile.getAbsolutePath() + "'");
+            }
+            structureFilename = structureFile.getAbsolutePath();
           }
 
-          if (structureFile == null)
+          if (structureFilename == null || structureFile == null)
           {
             addWarn("Not provided structure file with '" + val + "'");
             continue;
@@ -514,6 +539,8 @@ public class Commands
           Console.debug("Using structure file "
                   + structureFile.getAbsolutePath());
 
+          argParser.setStructureFilename(structureFilename);
+
           // open structure view
           AlignmentPanel ap = af.alignPanel;
           if (headless)
@@ -525,10 +552,9 @@ public class Commands
           String structureFilepath = structureFile.getAbsolutePath();
 
           // get PAEMATRIX file and label from subvals or Arg.PAEMATRIX
-          String paeFilepath = ArgParser
-                  .getFromSubValArgOrPrefWithSubstitutions(argParser, avm,
-                          Arg.PAEMATRIX, Position.AFTER, av, subVals, null,
-                          null, null);
+          String paeFilepath = avm.getFromSubValArgOrPrefWithSubstitutions(
+                  argParser, Arg.PAEMATRIX, ArgValuesMap.Position.AFTER,
+                  structureAv, subVals, null, null, null);
           if (paeFilepath != null)
           {
             File paeFile = new File(paeFilepath);
@@ -545,19 +571,17 @@ public class Commands
           }
 
           // showing annotations from structure file or not
-          boolean ssFromStructure = ArgParser.getFromSubValArgOrPref(avm,
+          boolean ssFromStructure = avm.getFromSubValArgOrPref(
                   Arg.SHOWSSANNOTATIONS, subVals, null, "STRUCT_FROM_PDB",
                   true);
 
           // get TEMPFAC type from subvals or Arg.TEMPFAC in case user Adds
           // reference annotations
-          String tftString = ArgParser
-                  .getFromSubValArgOrPrefWithSubstitutions(argParser, avm,
-                          Arg.TEMPFAC, Position.AFTER, av, subVals, null,
-                          null, null);
-          boolean notempfac = ArgParser.getFromSubValArgOrPref(avm,
-                  Arg.NOTEMPFAC, subVals, null, "ADD_TEMPFACT_ANN", false,
-                  true);
+          String tftString = avm.getFromSubValArgOrPrefWithSubstitutions(
+                  argParser, Arg.TEMPFAC, ArgValuesMap.Position.AFTER,
+                  structureAv, subVals, null, null, null);
+          boolean notempfac = avm.getFromSubValArgOrPref(Arg.NOTEMPFAC,
+                  subVals, null, "ADD_TEMPFACT_ANN", false, true);
           TFType tft = notempfac ? null : TFType.DEFAULT;
           if (tftString != null && !notempfac)
           {
@@ -586,37 +610,40 @@ public class Commands
             }
           }
 
-          String sViewer = ArgParser.getFromSubValArgOrPref(avm,
-                  Arg.STRUCTUREVIEWER, Position.AFTER, av, subVals, null,
-                  null, "jmol");
-          ViewerType viewerType = ViewerType.getFromString(sViewer);
+          String sViewerName = avm.getFromSubValArgOrPref(
+                  Arg.STRUCTUREVIEWER, ArgValuesMap.Position.AFTER,
+                  structureAv, subVals, null, null, "jmol");
+          ViewerType viewerType = ViewerType.getFromString(sViewerName);
 
           // TODO use ssFromStructure
-          StructureViewer sv = StructureChooser
+          StructureViewer structureViewer = StructureChooser
                   .openStructureFileForSequence(null, null, ap, seq, false,
                           structureFilepath, tft, paeFilepath, false,
                           ssFromStructure, false, viewerType);
 
-          if (sv == null)
+          if (structureViewer == null)
           {
-            addError("Failed to import and open structure view for file '"
-                    + structureFile + "'.");
+            if (!StringUtils.equalsIgnoreCase(sViewerName, "none"))
+            {
+              addError("Failed to import and open structure view for file '"
+                      + structureFile + "'.");
+            }
             continue;
           }
           try
           {
             long tries = 1000;
-            while (sv.isBusy() && tries > 0)
+            while (structureViewer.isBusy() && tries > 0)
             {
               Thread.sleep(25);
-              if (sv.isBusy())
+              if (structureViewer.isBusy())
               {
                 tries--;
                 Console.debug(
                         "Waiting for viewer for " + structureFilepath);
               }
             }
-            if (tries == 0 && sv.isBusy())
+            if (tries == 0 && structureViewer.isBusy())
             {
               addWarn("Gave up waiting for structure viewer to load file '"
                       + structureFile
@@ -638,101 +665,226 @@ public class Commands
           {
             svMap.put(id, new ArrayList<>());
           }
-          svMap.get(id).add(sv);
+          svMap.get(id).add(structureViewer);
 
           Console.debug(
                   "Successfully opened viewer for " + structureFilepath);
-          String structureImageFilename = ArgParser.getValueFromSubValOrArg(
-                  avm, av, Arg.STRUCTUREIMAGE, subVals);
-          if (sv != null && structureImageFilename != null)
+
+          if (avm.containsArg(Arg.STRUCTUREIMAGE))
           {
-            ArgValue siAv = avm.getClosestNextArgValueOfArg(av,
-                    Arg.STRUCTUREIMAGE);
-            SubVals sisv = null;
-            if (structureImageFilename.equals(siAv.getValue()))
+            for (ArgValue structureImageArgValue : avm
+                    .getArgValueListFromSubValOrArg(structureAv,
+                            Arg.STRUCTUREIMAGE, subVals))
             {
-              sisv = siAv.getSubVals();
-            }
-            File structureImageFile = new File(structureImageFilename);
-            String width = ArgParser.getValueFromSubValOrArg(avm, av,
-                    Arg.STRUCTUREIMAGEWIDTH, sisv);
-            String height = ArgParser.getValueFromSubValOrArg(avm, av,
-                    Arg.STRUCTUREIMAGEHEIGHT, sisv);
-            String scale = ArgParser.getValueFromSubValOrArg(avm, av,
-                    Arg.STRUCTUREIMAGESCALE, sisv);
-            String renderer = ArgParser.getValueFromSubValOrArg(avm, av,
-                    Arg.STRUCTUREIMAGETEXTRENDERER, sisv);
-            String typeS = ArgParser.getValueFromSubValOrArg(avm, av,
-                    Arg.STRUCTUREIMAGETYPE, sisv);
-            if (typeS == null || typeS.length() == 0)
-            {
-              typeS = FileUtils.getExtension(structureImageFile);
-            }
-            TYPE imageType;
-            try
-            {
-              imageType = Enum.valueOf(TYPE.class,
-                      typeS.toUpperCase(Locale.ROOT));
-            } catch (IllegalArgumentException e)
-            {
-              addWarn("Do not know image format '" + typeS
-                      + "', using PNG");
-              imageType = TYPE.PNG;
-            }
-            BitmapImageSizing userBis = ImageMaker
-                    .parseScaleWidthHeightStrings(scale, width, height);
-            // TODO MAKE THIS VIEWER INDEPENDENT!!
-            switch (StructureViewer.getViewerType())
-            {
-            case JMOL:
-              JalviewStructureDisplayI sview = sv
-                      .getJalviewStructureDisplay();
-              if (sview instanceof AppJmol)
+              String structureImageFilename = argParser.makeSubstitutions(
+                      structureImageArgValue.getValue(), id, true);
+              if (structureViewer != null && structureImageFilename != null)
               {
-                AppJmol jmol = (AppJmol) sview;
+                SubVals structureImageSubVals = null;
+                structureImageSubVals = structureImageArgValue.getSubVals();
+                File structureImageFile = new File(structureImageFilename);
+                String width = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.WIDTH,
+                        structureImageSubVals);
+                String height = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.HEIGHT,
+                        structureImageSubVals);
+                String scale = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.SCALE,
+                        structureImageSubVals);
+                String renderer = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.TEXTRENDERER,
+                        structureImageSubVals);
+                String typeS = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.TYPE,
+                        structureImageSubVals);
+                if (typeS == null || typeS.length() == 0)
+                {
+                  typeS = FileUtils.getExtension(structureImageFile);
+                }
+                TYPE imageType;
                 try
                 {
-                  whatNext wn = this.checksBeforeWritingToFile(avm, subVals,
-                          false, structureImageFilename, "structure image");
-                  if (wn == whatNext.ERROR)
+                  imageType = Enum.valueOf(TYPE.class,
+                          typeS.toUpperCase(Locale.ROOT));
+                } catch (IllegalArgumentException e)
+                {
+                  addWarn("Do not know image format '" + typeS
+                          + "', using PNG");
+                  imageType = TYPE.PNG;
+                }
+                BitmapImageSizing userBis = ImageMaker
+                        .parseScaleWidthHeightStrings(scale, width, height);
+
+                /////
+                // DON'T TRY TO EXPORT IF VIEWER IS UNSUPPORTED
+                if (viewerType != ViewerType.JMOL)
+                {
+                  addWarn("Cannot export image for structure viewer "
+                          + viewerType.name() + " yet");
+                  continue;
+                }
+
+                /////
+                // Apply the temporary colourscheme to the linked alignment
+                // TODO: enhance for multiple linked alignments.
+
+                String imageColour = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.IMAGECOLOUR,
+                        structureImageSubVals);
+                ColourSchemeI originalColourScheme = this
+                        .getColourScheme(af);
+                this.colourAlignFrame(af, imageColour);
+
+                /////
+                // custom image background colour
+
+                String bgcolourstring = avm.getValueFromSubValOrArg(
+                        structureImageArgValue, Arg.BGCOLOUR,
+                        structureImageSubVals);
+                Color bgcolour = null;
+                if (bgcolourstring != null && bgcolourstring.length() > 0)
+                {
+                  bgcolour = ColorUtils.parseColourString(bgcolourstring);
+                  if (bgcolour == null)
                   {
-                    isError = true;
-                    continue;
+                    Console.warn(
+                            "Background colour string '" + bgcolourstring
+                                    + "' not recognised -- using default");
                   }
-                  else if (wn == whatNext.CONTINUE)
+                }
+
+                JalviewStructureDisplayI sview = structureViewer
+                        .getJalviewStructureDisplay();
+
+                File sessionToRestore = null;
+
+                List<StructureCommandI> extraCommands = new ArrayList<>();
+
+                if (extraCommands.size() > 0 || bgcolour != null)
+                {
+                  try
                   {
-                    continue;
+                    sessionToRestore = sview.saveSession();
+                  } catch (Throwable t)
+                  {
+                    Console.warn(
+                            "Unable to save temporary session file before custom structure view export operation.");
                   }
+                }
+
+                ////
+                // Do temporary ops
 
-                  Console.debug("Rendering image to " + structureImageFile);
+                if (bgcolour != null)
+                {
+                  sview.getBinding().setBackgroundColour(bgcolour);
+                }
+
+                sview.getBinding().executeCommands(extraCommands, false,
+                        "Executing Custom Commands");
+
+                // and export the view as an image
+                boolean success = this.checksBeforeWritingToFile(avm,
+                        subVals, false, structureImageFilename,
+                        "structure image", isError);
+
+                if (!success)
+                {
+                  continue;
+                }
+                Console.debug("Rendering image to " + structureImageFile);
+                //
+                // TODO - extend StructureViewer / Binding with makePDBImage so
+                // we can do this with every viewer
+                //
+
+                try
+                {
+                  // We don't expect class cast exception
+                  AppJmol jmol = (AppJmol) sview;
                   jmol.makePDBImage(structureImageFile, imageType, renderer,
                           userBis);
-                  Console.debug("Finished Rendering image to "
+                  Console.info("Exported structure image to "
                           + structureImageFile);
 
-                } catch (ImageOutputException ioexc)
+                  // RESTORE SESSION AFTER EXPORT IF NEED BE
+                  if (sessionToRestore != null)
+                  {
+                    Console.debug(
+                            "Restoring session from " + sessionToRestore);
+
+                    sview.getBinding().restoreSession(
+                            sessionToRestore.getAbsolutePath());
+
+                  }
+                } catch (ImageOutputException ioexec)
                 {
-                  addError("Unexpected error whilst exporting image to "
-                          + structureImageFile, ioexc);
+                  addError(
+                          "Unexpected error when restoring structure viewer session after custom view operations.");
                   isError = true;
                   continue;
+                } finally
+                {
+                  try
+                  {
+                    this.colourAlignFrame(af, originalColourScheme);
+                  } catch (Exception t)
+                  {
+                    addError(
+                            "Unexpected error when restoring colourscheme to alignment after temporary change for export.",
+                            t);
+                  }
                 }
-
               }
-              break;
-            default:
-              addWarn("Cannot export image for structure viewer "
-                      + sv.getViewerType() + " yet");
-              continue;
             }
           }
+          argParser.setStructureFilename(null);
         }
       }
     }
 
+    if (af == null)
+    {
+      af = afMap.get(id);
+    }
+    // many of jalview's format/layout methods are only thread safe on the
+    // swingworker thread.
+    // all these methods should be on the alignViewController so it can
+    // coordinate such details
+    if (headless)
+    {
+      showOrHideAnnotations(af, showSSAnnotations, showAnnotations,
+              hideTFrows);
+    }
+    else
+    {
+      try
+      {
+        AlignFrame _af = af;
+        final boolean _showSSAnnotations = showSSAnnotations;
+        final boolean _showAnnotations = showAnnotations;
+        final boolean _hideTFrows = hideTFrows;
+        SwingUtilities.invokeAndWait(() -> {
+          showOrHideAnnotations(_af, _showSSAnnotations, _showAnnotations,
+                  _hideTFrows);
+        }
+
+        );
+      } catch (Exception x)
+      {
+        Console.warn(
+                "Unexpected exception adjusting annotation row visibility.",
+                x);
+      }
+    }
+
     if (wrap)
     {
-      AlignFrame af = afMap.get(id);
+      if (af == null)
+      {
+        af = afMap.get(id);
+      }
       if (af != null)
       {
         af.setWrapFormat(wrap, true);
@@ -743,42 +895,70 @@ public class Commands
     boolean doShading = avm.getBoolean(Arg.TEMPFAC_SHADING);
     if (doShading)
     {
-      AlignFrame af = afMap.get(id);
-      for (AlignmentAnnotation aa : af.alignPanel.getAlignment()
-              .findAnnotation(PDBChain.class.getName().toString()))
-      {
-        AnnotationColourGradient acg = new AnnotationColourGradient(aa,
-                af.alignPanel.av.getGlobalColourScheme(), 0);
-        acg.setSeqAssociated(true);
-        af.changeColour(acg);
-        Console.info("Changed colour " + acg.toString());
-      }
+    AlignFrame af = afMap.get(id);
+    for (AlignmentAnnotation aa : af.alignPanel.getAlignment()
+            .findAnnotation(PDBChain.class.getName().toString()))
+    {
+      AnnotationColourGradient acg = new AnnotationColourGradient(aa,
+              af.alignPanel.av.getGlobalColourScheme(), 0);
+      acg.setSeqAssociated(true);
+      af.changeColour(acg);
+      Console.info("Changed colour " + acg.toString());
+    }
     }
     */
 
     return theseArgsWereParsed && !isError;
   }
 
+  private static void showOrHideAnnotations(AlignFrame af,
+          boolean showSSAnnotations, boolean showAnnotations,
+          boolean hideTFrows)
+  {
+    af.setAnnotationsVisibility(showSSAnnotations, true, false);
+    af.setAnnotationsVisibility(showAnnotations, false, true);
+
+    // show temperature factor annotations?
+    if (hideTFrows)
+    {
+      // do this better (annotation types?)
+      List<String> hideThese = new ArrayList<>();
+      hideThese.add("Temperature Factor");
+      hideThese.add(AlphaFoldAnnotationRowBuilder.LABEL);
+      AlignmentUtils.showOrHideSequenceAnnotations(
+              af.getCurrentView().getAlignment(), hideThese, null, false,
+              false);
+    }
+  }
+
   protected void processGroovyScript(String id)
   {
     ArgValuesMap avm = argParser.getLinkedArgs(id);
     AlignFrame af = afMap.get(id);
 
-    if (af == null)
+    if (avm != null && !avm.containsArg(Arg.GROOVY))
     {
-      addWarn("Did not have an alignment window for id=" + id);
+      // nothing to do
       return;
     }
 
+    if (af == null)
+    {
+      addWarn("Groovy script does not have an alignment window.  Proceeding with caution!");
+    }
+
     if (avm.containsArg(Arg.GROOVY))
     {
-      String groovyscript = avm.getValue(Arg.GROOVY);
-      if (groovyscript != null)
+      for (ArgValue groovyAv : avm.getArgValueList(Arg.GROOVY))
       {
-        // Execute the groovy script after we've done all the rendering stuff
-        // and before any images or figures are generated.
-        Console.info("Executing script " + groovyscript);
-        Jalview.getInstance().executeGroovyScript(groovyscript, af);
+        String groovyscript = groovyAv.getValue();
+        if (groovyscript != null)
+        {
+          // Execute the groovy script after we've done all the rendering stuff
+          // and before any images or figures are generated.
+          Console.info("Executing script " + groovyscript);
+          Jalview.getInstance().executeGroovyScript(groovyscript, af);
+        }
       }
     }
   }
@@ -788,38 +968,45 @@ public class Commands
     ArgValuesMap avm = argParser.getLinkedArgs(id);
     AlignFrame af = afMap.get(id);
 
+    if (avm != null && !avm.containsArg(Arg.IMAGE))
+    {
+      // nothing to do
+      return true;
+    }
+
     if (af == null)
     {
-      addWarn("Did not have an alignment window for id=" + id);
+      addWarn("Do not have an alignment window to create image from (id="
+              + id + ").  Not proceeding.");
       return false;
     }
 
-    boolean isError = false;
+    Boolean isError = Boolean.valueOf(false);
     if (avm.containsArg(Arg.IMAGE))
     {
-      for (ArgValue av : avm.getArgValueList(Arg.IMAGE))
+      for (ArgValue imageAv : avm.getArgValueList(Arg.IMAGE))
       {
-        String val = av.getValue();
-        SubVals subVal = av.getSubVals();
-        String fileName = subVal.getContent();
+        String val = imageAv.getValue();
+        SubVals imageSubVals = imageAv.getSubVals();
+        String fileName = imageSubVals.getContent();
         File file = new File(fileName);
         String name = af.getName();
-        String renderer = ArgParser.getValueFromSubValOrArg(avm, av,
-                Arg.TEXTRENDERER, subVal);
+        String renderer = avm.getValueFromSubValOrArg(imageAv,
+                Arg.TEXTRENDERER, imageSubVals);
         if (renderer == null)
           renderer = "text";
         String type = "png"; // default
 
-        String scale = ArgParser.getValueFromSubValOrArg(avm, av, Arg.SCALE,
-                subVal);
-        String width = ArgParser.getValueFromSubValOrArg(avm, av, Arg.WIDTH,
-                subVal);
-        String height = ArgParser.getValueFromSubValOrArg(avm, av,
-                Arg.HEIGHT, subVal);
+        String scale = avm.getValueFromSubValOrArg(imageAv, Arg.SCALE,
+                imageSubVals);
+        String width = avm.getValueFromSubValOrArg(imageAv, Arg.WIDTH,
+                imageSubVals);
+        String height = avm.getValueFromSubValOrArg(imageAv, Arg.HEIGHT,
+                imageSubVals);
         BitmapImageSizing userBis = ImageMaker
                 .parseScaleWidthHeightStrings(scale, width, height);
 
-        type = ArgParser.getValueFromSubValOrArg(avm, av, Arg.TYPE, subVal);
+        type = avm.getValueFromSubValOrArg(imageAv, Arg.TYPE, imageSubVals);
         if (type == null && fileName != null)
         {
           for (String ext : new String[] { "svg", "png", "html", "eps" })
@@ -834,16 +1021,16 @@ public class Commands
         Cache.setPropsAreReadOnly(true);
         Cache.setProperty("EXPORT_EMBBED_BIOJSON", "false");
 
+        String imageColour = avm.getValueFromSubValOrArg(imageAv,
+                Arg.IMAGECOLOUR, imageSubVals);
+        ColourSchemeI originalColourScheme = this.getColourScheme(af);
+        this.colourAlignFrame(af, imageColour);
+
         Console.info("Writing " + file);
 
-        whatNext wn = this.checksBeforeWritingToFile(avm, subVal, false,
-                fileName, "image");
-        if (wn == whatNext.ERROR)
-        {
-          isError = true;
-          continue;
-        }
-        else if (wn == whatNext.CONTINUE)
+        boolean success = checksBeforeWritingToFile(avm, imageSubVals,
+                false, fileName, "image", isError);
+        if (!success)
         {
           continue;
         }
@@ -905,6 +1092,8 @@ public class Commands
                   ioex);
           isError = true;
         }
+
+        this.colourAlignFrame(af, originalColourScheme);
       }
     }
     return !isError;
@@ -915,13 +1104,20 @@ public class Commands
     ArgValuesMap avm = argParser.getLinkedArgs(id);
     AlignFrame af = afMap.get(id);
 
+    if (avm != null && !avm.containsArg(Arg.OUTPUT))
+    {
+      // nothing to do
+      return true;
+    }
+
     if (af == null)
     {
-      addWarn("Did not have an alignment window for id=" + id);
+      addWarn("Do not have an alignment window (id=" + id
+              + ").  Not proceeding.");
       return false;
     }
 
-    boolean isError = false;
+    Boolean isError = Boolean.valueOf(false);
 
     if (avm.containsArg(Arg.OUTPUT))
     {
@@ -934,8 +1130,8 @@ public class Commands
         File file = new File(fileName);
 
         String name = af.getName();
-        String format = ArgParser.getValueFromSubValOrArg(avm, av,
-                Arg.FORMAT, subVals);
+        String format = avm.getValueFromSubValOrArg(av, Arg.FORMAT,
+                subVals);
         FileFormats ffs = FileFormats.getInstance();
         List<String> validFormats = ffs.getWritableFormats(false);
 
@@ -988,21 +1184,15 @@ public class Commands
           }
         }
 
-        whatNext wn = this.checksBeforeWritingToFile(avm, subVals, true,
-                fileName, ff.getName());
-        if (wn == whatNext.ERROR)
-        {
-          isError = true;
-          continue;
-        }
-        else if (wn == whatNext.CONTINUE)
+        boolean success = checksBeforeWritingToFile(avm, subVals, true,
+                fileName, ff.getName(), isError);
+        if (!success)
         {
           continue;
         }
 
-        boolean backups = ArgParser.getFromSubValArgOrPref(avm, Arg.BACKUPS,
-                subVals, null,
-                Platform.isHeadless() ? null : BackupFiles.ENABLED,
+        boolean backups = avm.getFromSubValArgOrPref(Arg.BACKUPS, subVals,
+                null, Platform.isHeadless() ? null : BackupFiles.ENABLED,
                 !Platform.isHeadless());
 
         Console.info("Writing " + fileName);
@@ -1030,7 +1220,7 @@ public class Commands
           ArgValue av)
   {
     SubVals subVals = av.getSubVals();
-    ArgValue idAv = avm.getClosestNextArgValueOfArg(av, Arg.SEQID);
+    ArgValue idAv = avm.getClosestNextArgValueOfArg(av, Arg.SEQID, true);
     SequenceI seq = null;
     if (subVals == null && idAv == null)
       return null;
@@ -1090,6 +1280,47 @@ public class Commands
     return svs;
   }
 
+  private void colourAlignFrame(AlignFrame af, String colour)
+  {
+    // use string "none" to remove colour scheme
+    if (colour != null && "" != colour)
+    {
+      ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
+              af.getViewport(), af.getViewport().getAlignment(), colour);
+      if (cs == null && !StringUtils.equalsIgnoreCase(colour, "none"))
+      {
+        addWarn("Couldn't parse '" + colour + "' as a colourscheme.");
+      }
+      else
+      {
+        Jalview.testoutput(argParser, Arg.COLOUR, "zappo", colour);
+        colourAlignFrame(af, cs);
+      }
+    }
+  }
+
+  private void colourAlignFrame(AlignFrame af, ColourSchemeI cs)
+  {
+    try {
+    SwingUtilities.invokeAndWait(new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        // Note that cs == null removes colour scheme from af
+        af.changeColour(cs);
+      }
+    }); } catch (Exception x) {
+      Console.trace("Interrupted whilst waiting for colorAlignFrame action",x);
+      
+    }
+  }
+
+  private ColourSchemeI getColourScheme(AlignFrame af)
+  {
+    return af.getViewport().getGlobalColourScheme();
+  }
+
   private void addInfo(String errorMessage)
   {
     Console.info(errorMessage);
@@ -1113,19 +1344,14 @@ public class Commands
     errors.add(errorMessage);
   }
 
-  private enum whatNext
-  {
-    OKAY, CONTINUE, ERROR;
-  }
-
-  private whatNext checksBeforeWritingToFile(ArgValuesMap avm,
+  private boolean checksBeforeWritingToFile(ArgValuesMap avm,
           SubVals subVal, boolean includeBackups, String filename,
-          String adjective)
+          String adjective, Boolean isError)
   {
     File file = new File(filename);
 
-    boolean overwrite = ArgParser.getFromSubValArgOrPref(avm, Arg.OVERWRITE,
-            subVal, null, "OVERWRITE_OUTPUT", false);
+    boolean overwrite = avm.getFromSubValArgOrPref(Arg.OVERWRITE, subVal,
+            null, "OVERWRITE_OUTPUT", false);
     boolean stdout = false;
     boolean backups = false;
     if (includeBackups)
@@ -1134,8 +1360,8 @@ public class Commands
       // backups. Use the Arg.BACKUPS or subval "backups" setting first,
       // otherwise if headless assume false, if not headless use the user
       // preference with default true.
-      backups = ArgParser.getFromSubValArgOrPref(avm, Arg.BACKUPS, subVal,
-              null, Platform.isHeadless() ? null : BackupFiles.ENABLED,
+      backups = avm.getFromSubValArgOrPref(Arg.BACKUPS, subVal, null,
+              Platform.isHeadless() ? null : BackupFiles.ENABLED,
               !Platform.isHeadless());
     }
 
@@ -1145,11 +1371,11 @@ public class Commands
               + Arg.OVERWRITE.argString()
               + (includeBackups ? " or " + Arg.BACKUPS.argString() : "")
               + " set");
-      return whatNext.CONTINUE;
+      return false;
     }
 
-    boolean mkdirs = ArgParser.getFromSubValArgOrPref(avm, Arg.MKDIRS,
-            subVal, null, "MKDIRS_OUTPUT", false);
+    boolean mkdirs = avm.getFromSubValArgOrPref(Arg.MKDIRS, subVal, null,
+            "MKDIRS_OUTPUT", false);
 
     if (!FileUtils.checkParentDir(file, mkdirs))
     {
@@ -1158,10 +1384,11 @@ public class Commands
               + "' does not exist for " + adjective + " file '" + filename
               + "'."
               + (mkdirs ? "" : "  Try using " + Arg.MKDIRS.argString()));
-      return whatNext.ERROR;
+      isError = true;
+      return false;
     }
 
-    return whatNext.OKAY;
+    return true;
   }
 
   public List<String> getErrors()
@@ -1174,8 +1401,9 @@ public class Commands
     StringBuilder sb = new StringBuilder();
     for (String error : errors)
     {
+      if (sb.length() > 0)
+        sb.append("\n");
       sb.append("- " + error);
-      sb.append("\n");
     }
     return sb.toString();
   }