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