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