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