Merge branch 'develop' into bug/JAL-4290_headless_alignment_export_with_structure_ann...
[jalview.git] / src / jalview / bin / Commands.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.bin;
22
23 import java.awt.Color;
24 import java.io.File;
25 import java.io.IOException;
26 import java.net.URISyntaxException;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35
36 import javax.swing.SwingUtilities;
37
38 import jalview.analysis.AlignmentUtils;
39 import jalview.api.structures.JalviewStructureDisplayI;
40 import jalview.bin.Jalview.ExitCode;
41 import jalview.bin.argparser.Arg;
42 import jalview.bin.argparser.ArgParser;
43 import jalview.bin.argparser.ArgValue;
44 import jalview.bin.argparser.ArgValuesMap;
45 import jalview.bin.argparser.SubVals;
46 import jalview.datamodel.AlignmentI;
47 import jalview.datamodel.SequenceI;
48 import jalview.datamodel.annotations.AlphaFoldAnnotationRowBuilder;
49 import jalview.gui.AlignFrame;
50 import jalview.gui.AlignmentPanel;
51 import jalview.gui.AppJmol;
52 import jalview.gui.Desktop;
53 import jalview.gui.Preferences;
54 import jalview.gui.StructureChooser;
55 import jalview.gui.StructureViewer;
56 import jalview.gui.StructureViewer.ViewerType;
57 import jalview.io.AppletFormatAdapter;
58 import jalview.io.BackupFiles;
59 import jalview.io.BioJsHTMLOutput;
60 import jalview.io.DataSourceType;
61 import jalview.io.FileFormat;
62 import jalview.io.FileFormatException;
63 import jalview.io.FileFormatI;
64 import jalview.io.FileFormats;
65 import jalview.io.FileLoader;
66 import jalview.io.HtmlSvgOutput;
67 import jalview.io.IdentifyFile;
68 import jalview.io.NewickFile;
69 import jalview.io.exceptions.ImageOutputException;
70 import jalview.schemes.ColourSchemeI;
71 import jalview.schemes.ColourSchemeProperty;
72 import jalview.structure.StructureCommandI;
73 import jalview.structure.StructureImportSettings.TFType;
74 import jalview.structure.StructureSelectionManager;
75 import jalview.util.ColorUtils;
76 import jalview.util.FileUtils;
77 import jalview.util.HttpUtils;
78 import jalview.util.ImageMaker;
79 import jalview.util.ImageMaker.TYPE;
80 import jalview.util.MessageManager;
81 import jalview.util.Platform;
82 import jalview.util.StringUtils;
83 import jalview.util.imagemaker.BitmapImageSizing;
84
85 public class Commands
86 {
87   Desktop desktop;
88
89   private boolean headless;
90
91   private ArgParser argParser;
92
93   private Map<String, AlignFrame> afMap;
94
95   private Map<String, List<StructureViewer>> svMap;
96
97   private boolean commandArgsProvided = false;
98
99   private boolean argsWereParsed = false;
100
101   private List<String> errors = new ArrayList<>();
102
103   public Commands(ArgParser argparser, boolean headless)
104   {
105     this(Desktop.instance, argparser, headless);
106   }
107
108   public Commands(Desktop d, ArgParser argparser, boolean h)
109   {
110     argParser = argparser;
111     headless = h;
112     desktop = d;
113     afMap = new HashMap<>();
114   }
115
116   protected boolean processArgs()
117   {
118     if (argParser == null)
119     {
120       return true;
121     }
122
123     boolean theseArgsWereParsed = false;
124
125     if (argParser != null && argParser.getLinkedIds() != null)
126     {
127       for (String id : argParser.getLinkedIds())
128       {
129         ArgValuesMap avm = argParser.getLinkedArgs(id);
130         theseArgsWereParsed = true;
131         boolean processLinkedOkay = processLinked(id);
132         theseArgsWereParsed &= processLinkedOkay;
133
134         processGroovyScript(id);
135
136         // wait around until alignFrame isn't busy
137         AlignFrame af = afMap.get(id);
138         while (af != null && af.getViewport().isCalcInProgress())
139         {
140           try
141           {
142             Thread.sleep(25);
143           } catch (Exception q)
144           {
145           }
146           ;
147         }
148
149         theseArgsWereParsed &= processImages(id);
150
151         if (processLinkedOkay)
152         {
153           theseArgsWereParsed &= processOutput(id);
154         }
155
156         // close ap
157         if (avm.getBoolean(Arg.CLOSE))
158         {
159           af = afMap.get(id);
160           if (af != null)
161           {
162             af.closeMenuItem_actionPerformed(true);
163           }
164         }
165
166       }
167
168     }
169
170     // report errors - if any
171     String errorsRaised = errorsToString();
172     if (errorsRaised.trim().length() > 0)
173     {
174       Console.warn(
175               "The following errors and warnings occurred whilst processing files:\n"
176                       + errorsRaised);
177     }
178     // gui errors reported in Jalview
179
180     if (argParser.getBoolean(Arg.QUIT))
181     {
182       Jalview.exit("Exiting due to " + Arg.QUIT.argString() + " argument.",
183               ExitCode.OK);
184       return true;
185     }
186     // carry on with jalview.bin.Jalview
187     argsWereParsed = theseArgsWereParsed;
188     return argsWereParsed;
189   }
190
191   public boolean commandArgsProvided()
192   {
193     return commandArgsProvided;
194   }
195
196   public boolean argsWereParsed()
197   {
198     return argsWereParsed;
199   }
200
201   protected boolean processLinked(String id)
202   {
203     boolean theseArgsWereParsed = false;
204     ArgValuesMap avm = argParser.getLinkedArgs(id);
205     if (avm == null)
206     {
207       return true;
208     }
209
210     Boolean isError = Boolean.valueOf(false);
211
212     // set wrap, showSSAnnotations, showAnnotations and hideTFrows scope here so
213     // it can be applied after structures are opened
214     boolean wrap = false;
215     boolean showSSAnnotations = false;
216     boolean showAnnotations = false;
217     boolean hideTFrows = false;
218     AlignFrame af = null;
219
220     if (avm.containsArg(Arg.APPEND) || avm.containsArg(Arg.OPEN))
221     {
222       commandArgsProvided = true;
223       final long progress = System.currentTimeMillis();
224
225       boolean first = true;
226       boolean progressBarSet = false;
227       // Combine the APPEND and OPEN files into one list, along with whether it
228       // was APPEND or OPEN
229       List<ArgValue> openAvList = new ArrayList<>();
230       openAvList.addAll(avm.getArgValueList(Arg.OPEN));
231       openAvList.addAll(avm.getArgValueList(Arg.APPEND));
232       // sort avlist based on av.getArgIndex()
233       Collections.sort(openAvList);
234       for (ArgValue av : openAvList)
235       {
236         Arg a = av.getArg();
237         SubVals sv = av.getSubVals();
238         String openFile = av.getValue();
239         if (openFile == null)
240           continue;
241
242         theseArgsWereParsed = true;
243         if (first)
244         {
245           first = false;
246           if (!headless && desktop != null)
247           {
248             SwingUtilities.invokeLater(new Runnable()
249             {
250               @Override
251               public void run()
252               {
253                 desktop.setProgressBar(
254                         MessageManager.getString(
255                                 "status.processing_commandline_args"),
256                         progress);
257
258               }
259             });
260             progressBarSet = true;
261           }
262         }
263
264         if (!Platform.isJS())
265         /**
266          * ignore in JavaScript -- can't just file existence - could load it?
267          * 
268          * @j2sIgnore
269          */
270         {
271           if (!HttpUtils.startsWithHttpOrHttps(openFile))
272           {
273             if (!(new File(openFile)).exists())
274             {
275               addError("Can't find file '" + openFile + "'");
276               isError = true;
277               continue;
278             }
279           }
280         }
281
282         DataSourceType protocol = AppletFormatAdapter
283                 .checkProtocol(openFile);
284
285         FileFormatI format = null;
286         try
287         {
288           format = new IdentifyFile().identify(openFile, protocol);
289         } catch (FileFormatException e1)
290         {
291           addError("Unknown file format for '" + openFile + "'");
292           isError = true;
293           continue;
294         }
295
296         af = afMap.get(id);
297         // When to open a new AlignFrame
298         if (af == null || "true".equals(av.getSubVal("new"))
299                 || a == Arg.OPEN || format == FileFormat.Jalview)
300         {
301           if (a == Arg.OPEN)
302           {
303             Jalview.testoutput(argParser, Arg.OPEN, "examples/uniref50.fa",
304                     openFile);
305           }
306
307           Console.debug(
308                   "Opening '" + openFile + "' in new alignment frame");
309           FileLoader fileLoader = new FileLoader(!headless);
310           boolean xception = false;
311           try
312           {
313             af = fileLoader.LoadFileWaitTillLoaded(openFile, protocol,
314                     format);
315           } catch (Throwable thr)
316           {
317             xception = true;
318             addError("Couldn't open '" + openFile + "' as " + format + " "
319                     + thr.getLocalizedMessage()
320                     + " (Enable debug for full stack trace)");
321             isError = true;
322             Console.debug("Exception when opening '" + openFile + "'", thr);
323           } finally
324           {
325             if (af == null && !xception)
326             {
327               addInfo("Ignoring '" + openFile
328                       + "' - no alignment data found.");
329               continue;
330             }
331           }
332
333           // colour alignment
334           String colour = null;
335           if (avm.containsArg(Arg.COLOUR)
336                   || !(format == FileFormat.Jalview))
337           {
338             colour = avm.getFromSubValArgOrPref(av, Arg.COLOUR, sv, null,
339                     "DEFAULT_COLOUR_PROT", null);
340           }
341           if (colour != null)
342           {
343             this.colourAlignFrame(af, colour);
344           }
345
346           // Change alignment frame title
347           String title = avm.getFromSubValArgOrPref(av, Arg.TITLE, sv, null,
348                   null, null);
349           if (title != null)
350           {
351             af.setTitle(title);
352             Jalview.testoutput(argParser, Arg.TITLE, "test title", title);
353           }
354
355           // Add features
356           String featuresfile = avm.getValueFromSubValOrArg(av,
357                   Arg.FEATURES, sv);
358           if (featuresfile != null)
359           {
360             af.parseFeaturesFile(featuresfile,
361                     AppletFormatAdapter.checkProtocol(featuresfile));
362             Jalview.testoutput(argParser, Arg.FEATURES,
363                     "examples/testdata/plantfdx.features", featuresfile);
364           }
365
366           // Add annotations from file
367           String annotationsfile = avm.getValueFromSubValOrArg(av,
368                   Arg.ANNOTATIONS, sv);
369           if (annotationsfile != null)
370           {
371             af.loadJalviewDataFile(annotationsfile, null, null, null);
372             Jalview.testoutput(argParser, Arg.ANNOTATIONS,
373                     "examples/testdata/plantfdx.annotations",
374                     annotationsfile);
375           }
376
377           // Set or clear the sortbytree flag
378           boolean sortbytree = avm.getBoolFromSubValOrArg(Arg.SORTBYTREE,
379                   sv);
380           if (sortbytree)
381           {
382             af.getViewport().setSortByTree(true);
383             Jalview.testoutput(argParser, Arg.SORTBYTREE);
384           }
385
386           // Load tree from file
387           String treefile = avm.getValueFromSubValOrArg(av, Arg.TREE, sv);
388           if (treefile != null)
389           {
390             try
391             {
392               NewickFile nf = new NewickFile(treefile,
393                       AppletFormatAdapter.checkProtocol(treefile));
394               af.getViewport().setCurrentTree(
395                       af.showNewickTree(nf, treefile).getTree());
396               Jalview.testoutput(argParser, Arg.TREE,
397                       "examples/testdata/uniref50_test_tree", treefile);
398             } catch (IOException e)
399             {
400               addError("Couldn't add tree " + treefile, e);
401               isError = true;
402             }
403           }
404
405           // Show secondary structure annotations?
406           showSSAnnotations = avm.getFromSubValArgOrPref(
407                   Arg.SHOWSSANNOTATIONS, av.getSubVals(), null,
408                   "STRUCT_FROM_PDB", true);
409           // Show sequence annotations?
410           showAnnotations = avm.getFromSubValArgOrPref(Arg.SHOWANNOTATIONS,
411                   av.getSubVals(), null, "SHOW_ANNOTATIONS", true);
412           // hide the Temperature Factor row?
413           hideTFrows = (avm.getBoolean(Arg.NOTEMPFAC));
414
415           // showSSAnnotations, showAnnotations, hideTFrows used after opening
416           // structure
417
418           // wrap alignment? do this last for formatting reasons
419           wrap = avm.getFromSubValArgOrPref(Arg.WRAP, sv, null,
420                   "WRAP_ALIGNMENT", false);
421           // af.setWrapFormat(wrap) is applied after structures are opened for
422           // annotation reasons
423
424           // store the AlignFrame for this id
425           afMap.put(id, af);
426
427           // is it its own structure file?
428           if (format.isStructureFile())
429           {
430             StructureSelectionManager ssm = StructureSelectionManager
431                     .getStructureSelectionManager(Desktop.instance);
432             SequenceI seq = af.alignPanel.getAlignment().getSequenceAt(0);
433             ssm.computeMapping(false, new SequenceI[] { seq }, null,
434                     openFile, DataSourceType.FILE, null, null, null, false);
435           }
436         }
437         else
438         {
439           Console.debug(
440                   "Opening '" + openFile + "' in existing alignment frame");
441
442           DataSourceType dst = HttpUtils.startsWithHttpOrHttps(openFile)
443                   ? DataSourceType.URL
444                   : DataSourceType.FILE;
445
446           FileLoader fileLoader = new FileLoader(!headless);
447           fileLoader.LoadFile(af.getCurrentView(), openFile, dst, null,
448                   false);
449         }
450
451         Console.debug("Command " + Arg.APPEND + " executed successfully!");
452
453       }
454       if (first) // first=true means nothing opened
455       {
456         if (headless)
457         {
458           Jalview.exit("Could not open any files in headless mode",
459                   ExitCode.NO_FILES);
460         }
461         else
462         {
463           Console.info("No more files to open");
464         }
465       }
466       if (progressBarSet && desktop != null)
467         desktop.setProgressBar(null, progress);
468
469     }
470
471     // open the structure (from same PDB file or given PDBfile)
472     if (!avm.getBoolean(Arg.NOSTRUCTURE))
473     {
474       if (af == null)
475       {
476         af = afMap.get(id);
477       }
478       if (avm.containsArg(Arg.STRUCTURE))
479       {
480         commandArgsProvided = true;
481         for (ArgValue structureAv : avm.getArgValueList(Arg.STRUCTURE))
482         {
483           argParser.setStructureFilename(null);
484           String val = structureAv.getValue();
485           SubVals subVals = structureAv.getSubVals();
486           int argIndex = structureAv.getArgIndex();
487           SequenceI seq = getSpecifiedSequence(af, avm, structureAv);
488           if (seq == null)
489           {
490             // Could not find sequence from subId, let's assume the first
491             // sequence in the alignframe
492             AlignmentI al = af.getCurrentView().getAlignment();
493             seq = al.getSequenceAt(0);
494           }
495
496           if (seq == null)
497           {
498             addWarn("Could not find sequence for argument "
499                     + Arg.STRUCTURE.argString() + "=" + val);
500             continue;
501           }
502           String structureFilename = null;
503           File structureFile = null;
504           if (subVals.getContent() != null
505                   && subVals.getContent().length() != 0)
506           {
507             structureFilename = subVals.getContent();
508             Console.debug("Using structure file (from argument) '"
509                     + structureFilename + "'");
510             structureFile = new File(structureFilename);
511           }
512           /* THIS DOESN'T WORK */
513           else if (seq.getAllPDBEntries() != null
514                   && seq.getAllPDBEntries().size() > 0)
515           {
516             structureFile = new File(
517                     seq.getAllPDBEntries().elementAt(0).getFile());
518             if (structureFile != null)
519             {
520               Console.debug("Using structure file (from sequence) '"
521                       + structureFile.getAbsolutePath() + "'");
522             }
523             structureFilename = structureFile.getAbsolutePath();
524           }
525
526           if (structureFilename == null || structureFile == null)
527           {
528             addWarn("Not provided structure file with '" + val + "'");
529             continue;
530           }
531
532           if (!structureFile.exists())
533           {
534             addWarn("Structure file '" + structureFile.getAbsoluteFile()
535                     + "' not found.");
536             continue;
537           }
538
539           Console.debug("Using structure file "
540                   + structureFile.getAbsolutePath());
541
542           argParser.setStructureFilename(structureFilename);
543
544           // open structure view
545           AlignmentPanel ap = af.alignPanel;
546           if (headless)
547           {
548             Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
549                     StructureViewer.ViewerType.JMOL.toString());
550           }
551
552           String structureFilepath = structureFile.getAbsolutePath();
553
554           // get PAEMATRIX file and label from subvals or Arg.PAEMATRIX
555           String paeFilepath = avm.getFromSubValArgOrPrefWithSubstitutions(
556                   argParser, Arg.PAEMATRIX, ArgValuesMap.Position.AFTER,
557                   structureAv, subVals, null, null, null);
558           if (paeFilepath != null)
559           {
560             File paeFile = new File(paeFilepath);
561
562             try
563             {
564               paeFilepath = paeFile.getCanonicalPath();
565             } catch (IOException e)
566             {
567               paeFilepath = paeFile.getAbsolutePath();
568               addWarn("Problem with the PAE file path: '"
569                       + paeFile.getPath() + "'");
570             }
571           }
572
573           // showing annotations from structure file or not
574           boolean ssFromStructure = avm.getFromSubValArgOrPref(
575                   Arg.SHOWSSANNOTATIONS, subVals, null, "STRUCT_FROM_PDB",
576                   true);
577
578           // get TEMPFAC type from subvals or Arg.TEMPFAC in case user Adds
579           // reference annotations
580           String tftString = avm.getFromSubValArgOrPrefWithSubstitutions(
581                   argParser, Arg.TEMPFAC, ArgValuesMap.Position.AFTER,
582                   structureAv, subVals, null, null, null);
583           boolean notempfac = avm.getFromSubValArgOrPref(Arg.NOTEMPFAC,
584                   subVals, null, "ADD_TEMPFACT_ANN", false, true);
585           TFType tft = notempfac ? null : TFType.DEFAULT;
586           if (tftString != null && !notempfac)
587           {
588             // get kind of temperature factor annotation
589             try
590             {
591               tft = TFType.valueOf(tftString.toUpperCase(Locale.ROOT));
592               Console.debug("Obtained Temperature Factor type of '" + tft
593                       + "' for structure '" + structureFilepath + "'");
594             } catch (IllegalArgumentException e)
595             {
596               // Just an error message!
597               StringBuilder sb = new StringBuilder().append("Cannot set ")
598                       .append(Arg.TEMPFAC.argString()).append(" to '")
599                       .append(tft)
600                       .append("', ignoring.  Valid values are: ");
601               Iterator<TFType> it = Arrays.stream(TFType.values())
602                       .iterator();
603               while (it.hasNext())
604               {
605                 sb.append(it.next().toString().toLowerCase(Locale.ROOT));
606                 if (it.hasNext())
607                   sb.append(", ");
608               }
609               addWarn(sb.toString());
610             }
611           }
612
613           String sViewerName = avm.getFromSubValArgOrPref(
614                   Arg.STRUCTUREVIEWER, ArgValuesMap.Position.AFTER,
615                   structureAv, subVals, null, null, "jmol");
616           ViewerType viewerType = ViewerType.getFromString(sViewerName);
617
618           // TODO use ssFromStructure
619           StructureViewer structureViewer = StructureChooser
620                   .openStructureFileForSequence(null, null, ap, seq, false,
621                           structureFilepath, tft, paeFilepath, false,
622                           ssFromStructure, false, viewerType);
623
624           if (structureViewer == null)
625           {
626             if (!StringUtils.equalsIgnoreCase(sViewerName, "none"))
627             {
628               addError("Failed to import and open structure view for file '"
629                       + structureFile + "'.");
630             }
631             continue;
632           }
633           try
634           {
635             long tries = 1000;
636             while (structureViewer.isBusy() && tries > 0)
637             {
638               Thread.sleep(25);
639               if (structureViewer.isBusy())
640               {
641                 tries--;
642                 Console.debug(
643                         "Waiting for viewer for " + structureFilepath);
644               }
645             }
646             if (tries == 0 && structureViewer.isBusy())
647             {
648               addWarn("Gave up waiting for structure viewer to load file '"
649                       + structureFile
650                       + "'. Something may have gone wrong.");
651             }
652           } catch (Exception x)
653           {
654             addError("Exception whilst waiting for structure viewer "
655                     + structureFilepath, x);
656             isError = true;
657           }
658
659           // add StructureViewer to svMap list
660           if (svMap == null)
661           {
662             svMap = new HashMap<>();
663           }
664           if (svMap.get(id) == null)
665           {
666             svMap.put(id, new ArrayList<>());
667           }
668           svMap.get(id).add(structureViewer);
669
670           Console.debug(
671                   "Successfully opened viewer for " + structureFilepath);
672
673           if (avm.containsArg(Arg.STRUCTUREIMAGE))
674           {
675             for (ArgValue structureImageArgValue : avm
676                     .getArgValueListFromSubValOrArg(structureAv,
677                             Arg.STRUCTUREIMAGE, subVals))
678             {
679               String structureImageFilename = argParser.makeSubstitutions(
680                       structureImageArgValue.getValue(), id, true);
681               if (structureViewer != null && structureImageFilename != null)
682               {
683                 SubVals structureImageSubVals = null;
684                 structureImageSubVals = structureImageArgValue.getSubVals();
685                 File structureImageFile = new File(structureImageFilename);
686                 String width = avm.getValueFromSubValOrArg(
687                         structureImageArgValue, Arg.WIDTH,
688                         structureImageSubVals);
689                 String height = avm.getValueFromSubValOrArg(
690                         structureImageArgValue, Arg.HEIGHT,
691                         structureImageSubVals);
692                 String scale = avm.getValueFromSubValOrArg(
693                         structureImageArgValue, Arg.SCALE,
694                         structureImageSubVals);
695                 String renderer = avm.getValueFromSubValOrArg(
696                         structureImageArgValue, Arg.TEXTRENDERER,
697                         structureImageSubVals);
698                 String typeS = avm.getValueFromSubValOrArg(
699                         structureImageArgValue, Arg.TYPE,
700                         structureImageSubVals);
701                 if (typeS == null || typeS.length() == 0)
702                 {
703                   typeS = FileUtils.getExtension(structureImageFile);
704                 }
705                 TYPE imageType;
706                 try
707                 {
708                   imageType = Enum.valueOf(TYPE.class,
709                           typeS.toUpperCase(Locale.ROOT));
710                 } catch (IllegalArgumentException e)
711                 {
712                   addWarn("Do not know image format '" + typeS
713                           + "', using PNG");
714                   imageType = TYPE.PNG;
715                 }
716                 BitmapImageSizing userBis = ImageMaker
717                         .parseScaleWidthHeightStrings(scale, width, height);
718
719                 /////
720                 // DON'T TRY TO EXPORT IF VIEWER IS UNSUPPORTED
721                 if (viewerType != ViewerType.JMOL)
722                 {
723                   addWarn("Cannot export image for structure viewer "
724                           + viewerType.name() + " yet");
725                   continue;
726                 }
727
728                 /////
729                 // Apply the temporary colourscheme to the linked alignment
730                 // TODO: enhance for multiple linked alignments.
731
732                 String imageColour = avm.getValueFromSubValOrArg(
733                         structureImageArgValue, Arg.IMAGECOLOUR,
734                         structureImageSubVals);
735                 ColourSchemeI originalColourScheme = this
736                         .getColourScheme(af);
737                 this.colourAlignFrame(af, imageColour);
738
739                 /////
740                 // custom image background colour
741
742                 String bgcolourstring = avm.getValueFromSubValOrArg(
743                         structureImageArgValue, Arg.BGCOLOUR,
744                         structureImageSubVals);
745                 Color bgcolour = null;
746                 if (bgcolourstring != null && bgcolourstring.length() > 0)
747                 {
748                   bgcolour = ColorUtils.parseColourString(bgcolourstring);
749                   if (bgcolour == null)
750                   {
751                     Console.warn(
752                             "Background colour string '" + bgcolourstring
753                                     + "' not recognised -- using default");
754                   }
755                 }
756
757                 JalviewStructureDisplayI sview = structureViewer
758                         .getJalviewStructureDisplay();
759
760                 File sessionToRestore = null;
761
762                 List<StructureCommandI> extraCommands = new ArrayList<>();
763
764                 if (extraCommands.size() > 0 || bgcolour != null)
765                 {
766                   try
767                   {
768                     sessionToRestore = sview.saveSession();
769                   } catch (Throwable t)
770                   {
771                     Console.warn(
772                             "Unable to save temporary session file before custom structure view export operation.");
773                   }
774                 }
775
776                 ////
777                 // Do temporary ops
778
779                 if (bgcolour != null)
780                 {
781                   sview.getBinding().setBackgroundColour(bgcolour);
782                 }
783
784                 sview.getBinding().executeCommands(extraCommands, false,
785                         "Executing Custom Commands");
786
787                 // and export the view as an image
788                 boolean success = this.checksBeforeWritingToFile(avm,
789                         subVals, false, structureImageFilename,
790                         "structure image", isError);
791
792                 if (!success)
793                 {
794                   continue;
795                 }
796                 Console.debug("Rendering image to " + structureImageFile);
797                 //
798                 // TODO - extend StructureViewer / Binding with makePDBImage so
799                 // we can do this with every viewer
800                 //
801
802                 try
803                 {
804                   // We don't expect class cast exception
805                   AppJmol jmol = (AppJmol) sview;
806                   jmol.makePDBImage(structureImageFile, imageType, renderer,
807                           userBis);
808                   Console.info("Exported structure image to "
809                           + structureImageFile);
810
811                   // RESTORE SESSION AFTER EXPORT IF NEED BE
812                   if (sessionToRestore != null)
813                   {
814                     Console.debug(
815                             "Restoring session from " + sessionToRestore);
816
817                     sview.getBinding().restoreSession(
818                             sessionToRestore.getAbsolutePath());
819
820                   }
821                 } catch (ImageOutputException ioexec)
822                 {
823                   addError(
824                           "Unexpected error when restoring structure viewer session after custom view operations.");
825                   isError = true;
826                   continue;
827                 } finally
828                 {
829                   try
830                   {
831                     this.colourAlignFrame(af, originalColourScheme);
832                   } catch (Exception t)
833                   {
834                     addError(
835                             "Unexpected error when restoring colourscheme to alignment after temporary change for export.",
836                             t);
837                   }
838                 }
839               }
840             }
841           }
842           argParser.setStructureFilename(null);
843         }
844       }
845     }
846
847     if (af == null)
848     {
849       af = afMap.get(id);
850     }
851     // many of jalview's format/layout methods are only thread safe on the
852     // swingworker thread.
853     // all these methods should be on the alignViewController so it can
854     // coordinate such details
855     if (headless)
856     {
857       showOrHideAnnotations(af, showSSAnnotations, showAnnotations,
858               hideTFrows);
859     }
860     else
861     {
862       try
863       {
864         AlignFrame _af = af;
865         final boolean _showSSAnnotations = showSSAnnotations;
866         final boolean _showAnnotations = showAnnotations;
867         final boolean _hideTFrows = hideTFrows;
868         SwingUtilities.invokeAndWait(() -> {
869           showOrHideAnnotations(_af, _showSSAnnotations, _showAnnotations,
870                   _hideTFrows);
871         }
872
873         );
874       } catch (Exception x)
875       {
876         Console.warn(
877                 "Unexpected exception adjusting annotation row visibility.",
878                 x);
879       }
880     }
881
882     if (wrap)
883     {
884       if (af == null)
885       {
886         af = afMap.get(id);
887       }
888       if (af != null)
889       {
890         af.setWrapFormat(wrap, true);
891       }
892     }
893
894     /*
895     boolean doShading = avm.getBoolean(Arg.TEMPFAC_SHADING);
896     if (doShading)
897     {
898     AlignFrame af = afMap.get(id);
899     for (AlignmentAnnotation aa : af.alignPanel.getAlignment()
900             .findAnnotation(PDBChain.class.getName().toString()))
901     {
902       AnnotationColourGradient acg = new AnnotationColourGradient(aa,
903               af.alignPanel.av.getGlobalColourScheme(), 0);
904       acg.setSeqAssociated(true);
905       af.changeColour(acg);
906       Console.info("Changed colour " + acg.toString());
907     }
908     }
909     */
910
911     return theseArgsWereParsed && !isError;
912   }
913
914   private static void showOrHideAnnotations(AlignFrame af,
915           boolean showSSAnnotations, boolean showAnnotations,
916           boolean hideTFrows)
917   {
918     af.setAnnotationsVisibility(showSSAnnotations, true, false);
919     af.setAnnotationsVisibility(showAnnotations, false, true);
920
921     // show temperature factor annotations?
922     if (hideTFrows)
923     {
924       // do this better (annotation types?)
925       List<String> hideThese = new ArrayList<>();
926       hideThese.add("Temperature Factor");
927       hideThese.add(AlphaFoldAnnotationRowBuilder.LABEL);
928       AlignmentUtils.showOrHideSequenceAnnotations(
929               af.getCurrentView().getAlignment(), hideThese, null, false,
930               false);
931     }
932   }
933
934   protected void processGroovyScript(String id)
935   {
936     ArgValuesMap avm = argParser.getLinkedArgs(id);
937     AlignFrame af = afMap.get(id);
938
939     if (avm != null && !avm.containsArg(Arg.GROOVY))
940     {
941       // nothing to do
942       return;
943     }
944
945     if (af == null)
946     {
947       addWarn("Groovy script does not have an alignment window.  Proceeding with caution!");
948     }
949
950     if (avm.containsArg(Arg.GROOVY))
951     {
952       for (ArgValue groovyAv : avm.getArgValueList(Arg.GROOVY))
953       {
954         String groovyscript = groovyAv.getValue();
955         if (groovyscript != null)
956         {
957           // Execute the groovy script after we've done all the rendering stuff
958           // and before any images or figures are generated.
959           Console.info("Executing script " + groovyscript);
960           Jalview.getInstance().executeGroovyScript(groovyscript, af);
961         }
962       }
963     }
964   }
965
966   protected boolean processImages(String id)
967   {
968     ArgValuesMap avm = argParser.getLinkedArgs(id);
969     AlignFrame af = afMap.get(id);
970
971     if (avm != null && !avm.containsArg(Arg.IMAGE))
972     {
973       // nothing to do
974       return true;
975     }
976
977     if (af == null)
978     {
979       addWarn("Do not have an alignment window to create image from (id="
980               + id + ").  Not proceeding.");
981       return false;
982     }
983
984     Boolean isError = Boolean.valueOf(false);
985     if (avm.containsArg(Arg.IMAGE))
986     {
987       for (ArgValue imageAv : avm.getArgValueList(Arg.IMAGE))
988       {
989         String val = imageAv.getValue();
990         SubVals imageSubVals = imageAv.getSubVals();
991         String fileName = imageSubVals.getContent();
992         File file = new File(fileName);
993         String name = af.getName();
994         String renderer = avm.getValueFromSubValOrArg(imageAv,
995                 Arg.TEXTRENDERER, imageSubVals);
996         if (renderer == null)
997           renderer = "text";
998         String type = "png"; // default
999
1000         String scale = avm.getValueFromSubValOrArg(imageAv, Arg.SCALE,
1001                 imageSubVals);
1002         String width = avm.getValueFromSubValOrArg(imageAv, Arg.WIDTH,
1003                 imageSubVals);
1004         String height = avm.getValueFromSubValOrArg(imageAv, Arg.HEIGHT,
1005                 imageSubVals);
1006         BitmapImageSizing userBis = ImageMaker
1007                 .parseScaleWidthHeightStrings(scale, width, height);
1008
1009         type = avm.getValueFromSubValOrArg(imageAv, Arg.TYPE, imageSubVals);
1010         if (type == null && fileName != null)
1011         {
1012           for (String ext : new String[] { "svg", "png", "html", "eps" })
1013           {
1014             if (fileName.toLowerCase(Locale.ROOT).endsWith("." + ext))
1015             {
1016               type = ext;
1017             }
1018           }
1019         }
1020         // for moment we disable JSON export
1021         Cache.setPropsAreReadOnly(true);
1022         Cache.setProperty("EXPORT_EMBBED_BIOJSON", "false");
1023
1024         String imageColour = avm.getValueFromSubValOrArg(imageAv,
1025                 Arg.IMAGECOLOUR, imageSubVals);
1026         ColourSchemeI originalColourScheme = this.getColourScheme(af);
1027         this.colourAlignFrame(af, imageColour);
1028
1029         Console.info("Writing " + file);
1030
1031         boolean success = checksBeforeWritingToFile(avm, imageSubVals,
1032                 false, fileName, "image", isError);
1033         if (!success)
1034         {
1035           continue;
1036         }
1037
1038         try
1039         {
1040           switch (type)
1041           {
1042
1043           case "svg":
1044             Console.debug("Outputting type '" + type + "' to " + fileName);
1045             af.createSVG(file, renderer);
1046             break;
1047
1048           case "png":
1049             Console.debug("Outputting type '" + type + "' to " + fileName);
1050             af.createPNG(file, null, userBis);
1051             break;
1052
1053           case "html":
1054             Console.debug("Outputting type '" + type + "' to " + fileName);
1055             HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
1056             htmlSVG.exportHTML(fileName, renderer);
1057             break;
1058
1059           case "biojs":
1060             Console.debug(
1061                     "Outputting BioJS MSA Viwer HTML file: " + fileName);
1062             try
1063             {
1064               BioJsHTMLOutput.refreshVersionInfo(
1065                       BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
1066             } catch (URISyntaxException e)
1067             {
1068               e.printStackTrace();
1069             }
1070             BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
1071             bjs.exportHTML(fileName);
1072             break;
1073
1074           case "eps":
1075             Console.debug("Outputting EPS file: " + fileName);
1076             af.createEPS(file, renderer);
1077             break;
1078
1079           case "imagemap":
1080             Console.debug("Outputting ImageMap file: " + fileName);
1081             af.createImageMap(file, name);
1082             break;
1083
1084           default:
1085             addWarn(Arg.IMAGE.argString() + " type '" + type
1086                     + "' not known. Ignoring");
1087             break;
1088           }
1089         } catch (Exception ioex)
1090         {
1091           addError("Unexpected error during export to '" + fileName + "'",
1092                   ioex);
1093           isError = true;
1094         }
1095
1096         this.colourAlignFrame(af, originalColourScheme);
1097       }
1098     }
1099     return !isError;
1100   }
1101
1102   protected boolean processOutput(String id)
1103   {
1104     ArgValuesMap avm = argParser.getLinkedArgs(id);
1105     AlignFrame af = afMap.get(id);
1106
1107     if (avm != null && !avm.containsArg(Arg.OUTPUT))
1108     {
1109       // nothing to do
1110       return true;
1111     }
1112
1113     if (af == null)
1114     {
1115       addWarn("Do not have an alignment window (id=" + id
1116               + ").  Not proceeding.");
1117       return false;
1118     }
1119
1120     Boolean isError = Boolean.valueOf(false);
1121
1122     if (avm.containsArg(Arg.OUTPUT))
1123     {
1124       for (ArgValue av : avm.getArgValueList(Arg.OUTPUT))
1125       {
1126         String val = av.getValue();
1127         SubVals subVals = av.getSubVals();
1128         String fileName = subVals.getContent();
1129         boolean stdout = ArgParser.STDOUTFILENAME.equals(fileName);
1130         File file = new File(fileName);
1131
1132         String name = af.getName();
1133         String format = avm.getValueFromSubValOrArg(av, Arg.FORMAT,
1134                 subVals);
1135         FileFormats ffs = FileFormats.getInstance();
1136         List<String> validFormats = ffs.getWritableFormats(false);
1137
1138         FileFormatI ff = null;
1139         if (format == null && fileName != null)
1140         {
1141           FORMAT: for (String fname : validFormats)
1142           {
1143             FileFormatI tff = ffs.forName(fname);
1144             String[] extensions = tff.getExtensions().split(",");
1145             for (String ext : extensions)
1146             {
1147               if (fileName.toLowerCase(Locale.ROOT).endsWith("." + ext))
1148               {
1149                 ff = tff;
1150                 format = ff.getName();
1151                 break FORMAT;
1152               }
1153             }
1154           }
1155         }
1156         if (ff == null && format != null)
1157         {
1158           ff = ffs.forName(format);
1159         }
1160         if (ff == null)
1161         {
1162           if (stdout)
1163           {
1164             ff = FileFormat.Fasta;
1165           }
1166           else
1167           {
1168             StringBuilder validSB = new StringBuilder();
1169             for (String f : validFormats)
1170             {
1171               if (validSB.length() > 0)
1172                 validSB.append(", ");
1173               validSB.append(f);
1174               FileFormatI tff = ffs.forName(f);
1175               validSB.append(" (");
1176               validSB.append(tff.getExtensions());
1177               validSB.append(")");
1178             }
1179
1180             addError("No valid format specified for "
1181                     + Arg.OUTPUT.argString() + ". Valid formats are "
1182                     + validSB.toString() + ".");
1183             continue;
1184           }
1185         }
1186
1187         boolean success = checksBeforeWritingToFile(avm, subVals, true,
1188                 fileName, ff.getName(), isError);
1189         if (!success)
1190         {
1191           continue;
1192         }
1193
1194         boolean backups = avm.getFromSubValArgOrPref(Arg.BACKUPS, subVals,
1195                 null, Platform.isHeadless() ? null : BackupFiles.ENABLED,
1196                 !Platform.isHeadless());
1197
1198         Console.info("Writing " + fileName);
1199
1200         af.saveAlignment(fileName, ff, stdout, backups);
1201         if (af.isSaveAlignmentSuccessful())
1202         {
1203           Console.debug("Written alignment '" + name + "' in "
1204                   + ff.getName() + " format to '" + file + "'");
1205         }
1206         else
1207         {
1208           addError("Error writing file '" + file + "' in " + ff.getName()
1209                   + " format!");
1210           isError = true;
1211           continue;
1212         }
1213
1214       }
1215     }
1216     return !isError;
1217   }
1218
1219   private SequenceI getSpecifiedSequence(AlignFrame af, ArgValuesMap avm,
1220           ArgValue av)
1221   {
1222     SubVals subVals = av.getSubVals();
1223     ArgValue idAv = avm.getClosestNextArgValueOfArg(av, Arg.SEQID, true);
1224     SequenceI seq = null;
1225     if (subVals == null && idAv == null)
1226       return null;
1227     if (af == null || af.getCurrentView() == null)
1228     {
1229       return null;
1230     }
1231     AlignmentI al = af.getCurrentView().getAlignment();
1232     if (al == null)
1233     {
1234       return null;
1235     }
1236     if (subVals != null)
1237     {
1238       if (subVals.has(Arg.SEQID.getName()))
1239       {
1240         seq = al.findName(subVals.get(Arg.SEQID.getName()));
1241       }
1242       else if (-1 < subVals.getIndex()
1243               && subVals.getIndex() < al.getSequences().size())
1244       {
1245         seq = al.getSequenceAt(subVals.getIndex());
1246       }
1247     }
1248     if (seq == null && idAv != null)
1249     {
1250       seq = al.findName(idAv.getValue());
1251     }
1252     return seq;
1253   }
1254
1255   public AlignFrame[] getAlignFrames()
1256   {
1257     AlignFrame[] afs = null;
1258     if (afMap != null)
1259     {
1260       afs = (AlignFrame[]) afMap.values().toArray();
1261     }
1262
1263     return afs;
1264   }
1265
1266   public List<StructureViewer> getStructureViewers()
1267   {
1268     List<StructureViewer> svs = null;
1269     if (svMap != null)
1270     {
1271       for (List<StructureViewer> svList : svMap.values())
1272       {
1273         if (svs == null)
1274         {
1275           svs = new ArrayList<>();
1276         }
1277         svs.addAll(svList);
1278       }
1279     }
1280     return svs;
1281   }
1282
1283   private void colourAlignFrame(AlignFrame af, String colour)
1284   {
1285     // use string "none" to remove colour scheme
1286     if (colour != null && "" != colour)
1287     {
1288       ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
1289               af.getViewport(), af.getViewport().getAlignment(), colour);
1290       if (cs == null && !StringUtils.equalsIgnoreCase(colour, "none"))
1291       {
1292         addWarn("Couldn't parse '" + colour + "' as a colourscheme.");
1293       }
1294       else
1295       {
1296         Jalview.testoutput(argParser, Arg.COLOUR, "zappo", colour);
1297         colourAlignFrame(af, cs);
1298       }
1299     }
1300   }
1301
1302   private void colourAlignFrame(AlignFrame af, ColourSchemeI cs)
1303   {
1304     try {
1305     SwingUtilities.invokeAndWait(new Runnable()
1306     {
1307       @Override
1308       public void run()
1309       {
1310         // Note that cs == null removes colour scheme from af
1311         af.changeColour(cs);
1312       }
1313     }); } catch (Exception x) {
1314       Console.trace("Interrupted whilst waiting for colorAlignFrame action",x);
1315       
1316     }
1317   }
1318
1319   private ColourSchemeI getColourScheme(AlignFrame af)
1320   {
1321     return af.getViewport().getGlobalColourScheme();
1322   }
1323
1324   private void addInfo(String errorMessage)
1325   {
1326     Console.info(errorMessage);
1327     errors.add(errorMessage);
1328   }
1329
1330   private void addWarn(String errorMessage)
1331   {
1332     Console.warn(errorMessage);
1333     errors.add(errorMessage);
1334   }
1335
1336   private void addError(String errorMessage)
1337   {
1338     addError(errorMessage, null);
1339   }
1340
1341   private void addError(String errorMessage, Exception e)
1342   {
1343     Console.error(errorMessage, e);
1344     errors.add(errorMessage);
1345   }
1346
1347   private boolean checksBeforeWritingToFile(ArgValuesMap avm,
1348           SubVals subVal, boolean includeBackups, String filename,
1349           String adjective, Boolean isError)
1350   {
1351     File file = new File(filename);
1352
1353     boolean overwrite = avm.getFromSubValArgOrPref(Arg.OVERWRITE, subVal,
1354             null, "OVERWRITE_OUTPUT", false);
1355     boolean stdout = false;
1356     boolean backups = false;
1357     if (includeBackups)
1358     {
1359       stdout = ArgParser.STDOUTFILENAME.equals(filename);
1360       // backups. Use the Arg.BACKUPS or subval "backups" setting first,
1361       // otherwise if headless assume false, if not headless use the user
1362       // preference with default true.
1363       backups = avm.getFromSubValArgOrPref(Arg.BACKUPS, subVal, null,
1364               Platform.isHeadless() ? null : BackupFiles.ENABLED,
1365               !Platform.isHeadless());
1366     }
1367
1368     if (file.exists() && !(overwrite || backups || stdout))
1369     {
1370       addWarn("Won't overwrite file '" + filename + "' without "
1371               + Arg.OVERWRITE.argString()
1372               + (includeBackups ? " or " + Arg.BACKUPS.argString() : "")
1373               + " set");
1374       return false;
1375     }
1376
1377     boolean mkdirs = avm.getFromSubValArgOrPref(Arg.MKDIRS, subVal, null,
1378             "MKDIRS_OUTPUT", false);
1379
1380     if (!FileUtils.checkParentDir(file, mkdirs))
1381     {
1382       addError("Directory '"
1383               + FileUtils.getParentDir(file).getAbsolutePath()
1384               + "' does not exist for " + adjective + " file '" + filename
1385               + "'."
1386               + (mkdirs ? "" : "  Try using " + Arg.MKDIRS.argString()));
1387       isError = true;
1388       return false;
1389     }
1390
1391     return true;
1392   }
1393
1394   public List<String> getErrors()
1395   {
1396     return errors;
1397   }
1398
1399   public String errorsToString()
1400   {
1401     StringBuilder sb = new StringBuilder();
1402     for (String error : errors)
1403     {
1404       if (sb.length() > 0)
1405         sb.append("\n");
1406       sb.append("- " + error);
1407     }
1408     return sb.toString();
1409   }
1410 }