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