JAL-629 --structureimage with formatting args.
[jalview.git] / src / jalview / bin / argparser / Arg.java
index a7fc0f7..2354cdd 100644 (file)
@@ -10,15 +10,16 @@ import java.util.List;
 import java.util.Locale;
 import java.util.stream.Collectors;
 
-import jalview.bin.Cache;
+import jalview.bin.argparser.Arg.Opt;
 import jalview.util.ChannelProperties;
+import jalview.util.Platform;
 
 public enum Arg
 {
 
   // Initialising arguments (BOOTSTRAP)
-  HELP(Type.HELP, "h", "Display this help statement", Opt.UNARY,
-          Opt.BOOTSTRAP, Opt.HASTYPE),
+  HELP(Type.HELP, "h", "Display basic help", Opt.UNARY, Opt.BOOTSTRAP,
+          Opt.HASTYPE, Opt.MULTI),
   /*
    * Other --help-type Args will be added by the static block.
    */
@@ -127,9 +128,6 @@ public enum Arg
                   + "none,\n" + "jmol,\n" + "chimera,\n" + "chimerax,\n"
                   + "pymol.",
           Opt.STRING, Opt.LINKED, Opt.MULTI),
-  STRUCTUREIMAGE(Type.STRUCTURE,
-          "Export an image of a 3D structure opened in JMOL", Opt.STRING,
-          Opt.LINKED, Opt.MULTI),
   NOTEMPFAC(Type.STRUCTURE,
           "Do not show the temperature factor annotation for the preceding --structure.",
           Opt.UNARY, Opt.LINKED, Opt.ALLOWALL, Opt.SECRET), // keep this secret
@@ -145,11 +143,11 @@ public enum Arg
           Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS, Opt.ALLOWALL,
           Opt.REQUIREINPUT, Opt.OUTPUT, Opt.PRIMARY),
   TYPE(Type.IMAGE,
-          "Set the image format for the preceding --image to name. Valid values for name are: svg,\n"
-                  + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
+          "Set the image format for the preceding --image. Valid values are:\n"
+                  + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   TEXTRENDERER(Type.IMAGE,
-          "Sets whether text in a vector image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values for name are:\n"
+          "Sets whether text in a vector image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values are:\n"
                   + "text,\n" + "lineart.",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   SCALE(Type.IMAGE,
@@ -161,6 +159,28 @@ public enum Arg
   HEIGHT(Type.IMAGE,
           "Sets a height for bitmap image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --scale and --width then the smallest scaling will be used (scale, width and height provide bounds for the image).",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGE(Type.IMAGE,
+          "Export an image of a 3D structure opened in JMOL", Opt.STRING,
+          Opt.LINKED, Opt.MULTI),
+
+  STRUCTUREIMAGETYPE(Type.IMAGE,
+          "Set the structure image format for the preceding --structureimage. Valid values are:\n"
+                  + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGETEXTRENDERER(Type.IMAGE,
+          "Sets whether text in a vector structure image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values are:\n"
+                  + "text,\n" + "lineart.",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGESCALE(Type.IMAGE,
+          "Sets a scaling for bitmap structure image format (PNG). Should be given as a floating point number. If used in conjunction with --structureimagewidth and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGEWIDTH(Type.IMAGE,
+          "Sets a width for bitmap structure image format (PNG) with the height maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGEHEIGHT(Type.IMAGE,
+          "Sets a height for bitmap structure image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimagewidth then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+
   OUTPUT(Type.OUTPUT,
           "Export the open alignment to file filename. The format name is specified by the subval modifier format=name, a following --format name argument or guessed from the file extension. Valid format names (and file extensions) are:\n"
                   + "fasta (fa, fasta, mfa, fastq),\n" + "pfam (pfam),\n"
@@ -196,8 +216,9 @@ public enum Arg
           "Move on to a new alignment window. This will ensure --append will start a new alignment window and other linked arguments will apply to the new alignment window.",
           Opt.UNARY, Opt.MULTI, Opt.NOACTION, Opt.INCREMENTDEFAULTCOUNTER),
   SUBSTITUTIONS(Type.FLOW,
-          "The following argument values allow (or don't allow) subsituting filename parts. This is initially true. Valid substitutions are {basename} - the filename-without-extension of the currently --opened file (or first --appended file),\n"
-                  + "{dirname}, - the directory (folder) name of the currently --opened file (or first --appended file),\n"
+          "The following argument values allow (or don't allow) subsituting filename parts. This is initially true. Valid substitutions are:\n"
+                  + "{basename} - the filename-without-extension of the currently --opened file (or first --appended file),\n"
+                  + "{dirname} - the directory (folder) name of the currently --opened file (or first --appended file),\n"
                   + "{argfilebasename} - the filename-without-extension of the current --argfile,\n"
                   + "{argfiledirname} - the directory (folder) name of the current --argfile,\n"
                   + "{n} - the value of the index counter (starting at 0).\n"
@@ -248,11 +269,11 @@ public enum Arg
   JVMMEMPC(Type.CONFIG,
           "Limit maximum heap size (memory) to PERCENT% of total physical memory detected. This defaults to 90 if total physical memory can be detected.\n"
                   + "The equals sign (\"=\") separator must be used with no spaces.",
-          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING),
+          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
   JVMMEMMAX(Type.CONFIG,
           "Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m), gigabytes(g) or if you're lucky enough, terabytes(t). This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected.\n"
                   + "The equals sign (\"=\") separator must be used with no spaces.",
-          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING),
+          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
 
   ;
 
@@ -306,24 +327,33 @@ public enum Arg
     HELP, // This Arg is a --help type arg
     PRIMARY, // This Arg is the main Arg for its type
     HASTYPE, // This Arg can have an Arg.Type assigned to it (and no value)
+    FIRST, // Move this arg to the first in usage statement (within type)
+    LAST, // Move this arg to the end in usage statement (within type)
   }
 
   public static enum Type
   {
     // Type restricts argument to certain usage output
-    CONFIG("Arguments user to configure "
+    HELP, // --help
+    CONFIG("arguments used to configure "
             + ChannelProperties.getProperty("app_name") + " from startup"),
-    OPENING("Arguments used to open and format alignments"),
-    STRUCTURE("Arguments used to add and format 3D structure data"),
-    PROCESS("Arguments used to process an alignment once opened"),
-    OUTPUT("Arguments used to save data from a processed alignment"),
-    IMAGE("Arguments used to export an image of an alignment"),
-    FLOW("Arguments that control processing of the other arguments"),
-    ALL("All arguments"), // mostly just a place-holder for --help-all
-    HELP("Arguments to provide help text");
+    OPENING("arguments used to open and format alignments"),
+    STRUCTURE("arguments used to add and format 3D structure data"),
+    PROCESS("arguments used to process an alignment once opened"),
+    OUTPUT("arguments used to save data from a processed alignment"),
+    IMAGE("arguments used to export an image of an alignment"),
+    FLOW("arguments that control processing of the other arguments"), //
+    ALL("all arguments"), // mostly just a place-holder for --help-all
+    NONE, // mostly a place-holder for --help
+    INVALID;
 
     private String description;
 
+    private Type()
+    {
+      description = null;
+    }
+
     private Type(String description)
     {
       this.description = description;
@@ -335,15 +365,6 @@ public enum Arg
     }
   }
 
-  static
-  {
-    for (Type t : EnumSet.allOf(Type.class))
-    {
-      String type = t.name();
-
-    }
-  }
-
   private final String[] argNames;
 
   private Opt[] argOptions;
@@ -499,130 +520,255 @@ public enum Arg
     return usage(null);
   }
 
-  public static final String usage(List<Arg> helpArgs)
+  public static final void appendUsageGeneral(StringBuilder sb,
+          int maxArgLength)
   {
-    StringBuilder sb = new StringBuilder();
-
-    sb.append(ChannelProperties.getProperty("app_name"));
-    String version = Cache.getDefault("VERSION", null);
-    if (version != null)
+    for (Type t : EnumSet.allOf(Type.class))
     {
-      sb.append(" version ");
-      sb.append(Cache.getDefault("VERSION", "unknown"));
+      if (t.description() != null)
+      {
+        StringBuilder argSb = new StringBuilder();
+        argSb.append(Arg.HELP.argString()).append(ArgParser.SINGLEDASH)
+                .append(t.name().toLowerCase(Locale.ROOT));
+        appendArgAndDescription(sb, argSb.toString(),
+                "Help for " + t.description(), null, maxArgLength);
+        sb.append(System.lineSeparator());
+      }
     }
-    sb.append(System.lineSeparator());
-    sb.append("Usage: jalview [files...] [args]");
+  }
+
+  public static final String usage(List<Type> types)
+  {
+    StringBuilder sb = new StringBuilder();
+
+    sb.append("usage: jalview [" + Arg.HEADLESS.argString() + "] [["
+            + Arg.OPEN.argString() + "/" + Arg.APPEND.argString()
+            + "] file(s)] [args]");
     sb.append(System.lineSeparator());
     sb.append(System.lineSeparator());
 
-    // PROGRAM THIS!
-    Type type = Type.HELP;
-    Iterator<Arg> typeArgs = Arg.getAllOfType(type);
-
-    int maxArgLength = 0;
-    for (Arg a : EnumSet.allOf(Arg.class))
+    if (types == null || types.contains(null))
     {
-      if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
-        continue;
-
-      StringBuilder argSb = new StringBuilder();
-      argSb.append(a.hasOption(Opt.BOOLEAN) ? booleanArgString(a)
-              : a.argString());
-      if (a.hasOption(Opt.STRING))
-        argSb.append("=value");
-      if (argSb.length() > maxArgLength)
-        maxArgLength = argSb.length();
-    }
+      // always show --help
+      appendArgAndDescription(sb, null, "Display this basic help", Arg.HELP,
+              DESCRIPTIONINDENT);
+      sb.append(System.lineSeparator());
 
-    // might want to sort these
-    for (Arg a : EnumSet.allOf(Arg.class))
+      appendUsageGeneral(sb, DESCRIPTIONINDENT);
+    }
+    else
     {
-      if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
-        continue;
-      StringBuilder argSb = new StringBuilder();
-      argSb.append(a.hasOption(Opt.BOOLEAN) ? booleanArgString(a)
-              : a.argString());
-      if (a.hasOption(Opt.STRING))
-        argSb.append("=value");
-      Iterator<String> descLines = null;
-      if (a.getDescription() != null)
+      List<Arg> args = argsSortedForDisplay(types);
+
+      /*
+       * just use a set maxArgLength of DESCRIPTIONINDENT
+       
+      int maxArgLength = 0;
+      for (Arg a : args)
       {
-        descLines = Arrays.stream(a.getDescription().split("\\n"))
-                .iterator();
+        if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
+          continue;
+      
+        String argS = argDisplayString(a);
+        if (argS.length() > maxArgLength)
+          maxArgLength = argS.length();
       }
-      sb.append(String.format("%-" + maxArgLength + "s", argSb.toString()));
-      boolean first = true;
-      if (descLines != null)
+      */
+      int maxArgLength = DESCRIPTIONINDENT;
+
+      // always show --help
+      appendArgAndDescription(sb, null, null, Arg.HELP, maxArgLength);
+      sb.append(System.lineSeparator());
+
+      if ((args.contains(Arg.HELP) && types.contains(Type.ALL)))
+      {
+        appendUsageGeneral(sb, maxArgLength);
+      }
+
+      Iterator<Arg> argsI = args.iterator();
+      Type typeSection = null;
+      while (argsI.hasNext())
       {
-        while (descLines.hasNext())
+        Arg a = argsI.next();
+
+        if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET)
+                || a == Arg.HELP)
+        {
+          continue;
+        }
+
+        if (a.getType() != typeSection)
+        {
+          typeSection = a.getType();
+          String typeDescription = a.getType().description();
+          if (typeDescription != null && typeDescription.length() > 0)
+          {
+            // typeDescription = typeDescription.substring(0,
+            // 1).toUpperCase(Locale.ROOT) + typeDescription.substring(1);
+            typeDescription = typeDescription.toUpperCase(Locale.ROOT);
+            sb.append(typeDescription);
+            sb.append(System.lineSeparator());
+            sb.append(System.lineSeparator());
+          }
+        }
+
+        appendArgUsage(sb, a, maxArgLength);
+
+        if (argsI.hasNext())
         {
-          if (first)
-            sb.append(" - ");
-          else
-            sb.append(" ".repeat(maxArgLength + 3));
-          sb.append(descLines.next());
           sb.append(System.lineSeparator());
-          first = false;
         }
       }
+    }
+    return sb.toString();
+  }
 
-      List<String> options = new ArrayList<>();
+  private static void appendArgUsage(StringBuilder sb, Arg a,
+          int maxArgLength)
+  {
+    boolean first = appendArgAndDescription(sb, null, null, a,
+            maxArgLength);
+    List<String> options = new ArrayList<>();
 
-      if (a.hasOption(Opt.BOOLEAN))
-      {
-        options.add("default " + (a.getDefaultBoolValue() ? a.argString()
-                : a.negateArgString()));
-      }
+    if (a.hasOption(Opt.BOOLEAN))
+    {
+      options.add("default " + (a.getDefaultBoolValue() ? a.argString()
+              : a.negateArgString()));
+    }
 
-      if (a.hasOption(Opt.MULTI))
-      {
-        options.add("multiple");
-      }
+    if (a.hasOption(Opt.MULTI))
+    {
+      options.add("multiple");
+    }
 
-      if (a.hasOption(Opt.LINKED))
-      {
-        options.add("can be linked");
-      }
+    if (a.hasOption(Opt.LINKED))
+    {
+      options.add("can be linked");
+    }
 
-      if (a.hasOption(Opt.GLOB))
-      {
-        options.add("allows file globs");
-      }
+    if (a.hasOption(Opt.GLOB))
+    {
+      options.add("allows file globs");
+    }
 
-      if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
-      {
-        options.add("allows substitutions");
-      }
+    if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
+    {
+      options.add("allows substitutions");
+    }
+
+    if (a.hasOption(Opt.ALLOWALL))
+    {
+      options.add("can be applied to all linked arguments");
+    }
+
+    if (a.hasOption(Opt.PRIVATE))
+    {
+      options.add("for internal use only");
+    }
+
+    if (a.hasOption(Opt.SECRET))
+    {
+      options.add("for development use only");
+    }
 
-      if (a.hasOption(Opt.ALLOWALL))
+    if (options.size() > 0)
+    {
+      if (first)
       {
-        options.add("can be applied to all linked arguments");
+        sb.append(ARGDESCRIPTIONSEPARATOR);
       }
-
-      if (a.hasOption(Opt.PRIVATE))
+      else
       {
-        options.add("for internal use only");
+        sb.append(String.format("%-"
+                + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
+                ""));
       }
+      sb.append("(");
+      sb.append(String.join("; ", options));
+      sb.append(')');
+      sb.append(System.lineSeparator());
+    }
+  }
+
+  public static String argDisplayString(Arg a)
+  {
+    StringBuilder argSb = new StringBuilder();
+    argSb.append(
+            a.hasOption(Opt.BOOLEAN) ? booleanArgString(a) : a.argString());
+    if (a.hasOption(Opt.STRING))
+      argSb.append("=value");
+    return argSb.toString();
+  }
+
+  public static boolean appendArgAndDescription(StringBuilder sb,
+          String aString, String description, Arg a, int maxArgLength)
+  {
+    return appendArgAndDescription(sb, aString, description, a,
+            maxArgLength, Platform.consoleWidth());
+  }
+
+  public static boolean appendArgAndDescription(StringBuilder sb,
+          String aString, String description, Arg a, int maxArgLength,
+          int maxLength)
+  {
+    if (aString == null && a != null)
+    {
+      aString = argDisplayString(a);
+    }
+    if (description == null && a != null)
+    {
+      description = a.getDescription();
+    }
+    sb.append(String.format("%-" + maxArgLength + "s", aString));
+    if (aString.length() > maxArgLength)
+    {
+      sb.append(System.lineSeparator());
+      sb.append(String.format("%-" + maxArgLength + "s", ""));
+    }
 
-      if (a.hasOption(Opt.SECRET))
+    int descLength = maxLength - maxArgLength
+            - ARGDESCRIPTIONSEPARATOR.length();
+    // reformat the descriptions lines to the right width
+    Iterator<String> descLines = null;
+    if (description != null)
+    {
+      descLines = Arrays.stream(description.split("\\n")).iterator();
+    }
+    List<String> splitDescLinesList = new ArrayList<>();
+    while (descLines != null && descLines.hasNext())
+    {
+      String line = descLines.next();
+      while (line.length() > descLength)
       {
-        options.add("for development use only");
+        int splitIndex = line.lastIndexOf(" ", descLength);
+        splitDescLinesList.add(line.substring(0, splitIndex));
+        line = line.substring(splitIndex + 1);
       }
+      splitDescLinesList.add(line);
+    }
 
-      if (options.size() > 0)
+    Iterator<String> splitDescLines = splitDescLinesList.iterator();
+    boolean first = true;
+    if (splitDescLines != null)
+    {
+      while (splitDescLines.hasNext())
       {
         if (first)
-          sb.append(" - ");
+        {
+          sb.append(ARGDESCRIPTIONSEPARATOR);
+        }
         else
-          sb.append(" ".repeat(maxArgLength + 3));
-        sb.append("(");
-        sb.append(String.join("; ", options));
-        sb.append(')');
+        {
+          sb.append(String.format("%-"
+                  + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
+                  ""));
+        }
+        sb.append(splitDescLines.next());
         sb.append(System.lineSeparator());
+        first = false;
       }
-      sb.append(System.lineSeparator());
     }
-    return sb.toString();
+    return first;
   }
 
   protected static Iterator<Arg> getAllOfType(Type type)
@@ -645,11 +791,11 @@ public enum Arg
     }).iterator();
   }
 
-  private static List<Arg> sortForDisplay(Type... types)
+  private static List<Arg> argsSortedForDisplay(List<Type> types)
   {
     List<Arg> argsToSort;
     // if no types provided, do all
-    if (types == null || types.length == 0)
+    if (types == null || types.size() == 0 || types.contains(Type.ALL))
     {
       argsToSort = Arrays
               .asList(EnumSet.allOf(Arg.class).toArray(new Arg[] {}));
@@ -659,42 +805,59 @@ public enum Arg
       argsToSort = new ArrayList<>();
       for (Type type : types)
       {
+        if (type == null)
+          continue;
         Arg.getAllOfType(type).forEachRemaining(a -> argsToSort.add(a));
       }
     }
 
-    Collections.sort(argsToSort, new ArgComparator());
+    Collections.sort(argsToSort, new ArgDisplayComparator());
     return argsToSort;
   }
 
-  protected int compareForDisplay(Arg other)
+  private static final String ARGDESCRIPTIONSEPARATOR = " - ";
+
+  private static final int DESCRIPTIONINDENT = 20;
+
+}
+
+class ArgDisplayComparator implements Comparator<Arg>
+{
+  private int compareArgOpts(Arg a, Arg b, Opt o)
   {
-    if (other == null)
-      return 1;
-    // first compare types (in order of appearance)
-    int i = this.getType().compareTo(other.getType());
-    if (i == 0)
-    {
-      // next prioritise primary arguments
-      i = this.hasOption(Opt.PRIMARY)
-              ? (other.hasOption(Opt.PRIMARY) ? 0 : 1)
-              : (other.hasOption(Opt.PRIMARY) ? -1 : 0);
-      if (i == 0)
-      {
-        // finally order of appearance in enum declarations
-        i = this.compareTo(other);
-      }
-    }
+    int i = a.hasOption(o) ? (b.hasOption(o) ? 0 : -1)
+            : (b.hasOption(o) ? 1 : 0);
     return i;
   }
 
-}
+  private int compareForDisplay(Arg a, Arg b)
+  {
+    if (b == null)
+      return -1;
+    // first compare types (in enum order)
+    int i = a.getType().compareTo(b.getType());
+    if (i != 0)
+      return i;
+    // do Opt.LAST next (oddly). Reversed args important!
+    i = compareArgOpts(b, a, Opt.LAST);
+    if (i != 0)
+      return i;
+    // priority order
+    Opt[] optOrder = { Opt.HELP, Opt.FIRST, Opt.PRIMARY, Opt.STRING,
+        Opt.BOOLEAN };
+    for (Opt o : optOrder)
+    {
+      i = compareArgOpts(a, b, o);
+      if (i != 0)
+        return i;
+    }
+    // finally order of appearance in enum declarations
+    return a.compareTo(b);
+  }
 
-class ArgComparator implements Comparator<Arg>
-{
   @Override
   public int compare(Arg a, Arg b)
   {
-    return a.compareForDisplay(b);
+    return compareForDisplay(a, b);
   }
 }
\ No newline at end of file