Merge branch 'develop' into features/r2_11_2_alphafold/JAL-629
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 31 Aug 2023 15:10:57 +0000 (16:10 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 31 Aug 2023 15:10:57 +0000 (16:10 +0100)
81 files changed:
THIRDPARTYLIBS
build.gradle
examples/testdata/7WKP-rna1.xml [new file with mode: 0644]
gradle.properties
help/help/html/features/clarguments-reference.html
help/help/html/privacy.html
help/markdown/releases/release-2_11_2_7.md [new file with mode: 0644]
help/markdown/releases/release-2_11_3_0.md
help/markdown/whatsnew/whatsnew-2_11_2_7.md [new file with mode: 0644]
help/markdown/whatsnew/whatsnew-2_11_3_0.md
j11lib/JGoogleAnalytics_0.3.jar [deleted file]
j11lib/jfreesvg-2.1.jar [deleted file]
j11lib/jfreesvg-3.4.3.jar [new file with mode: 0644]
j8lib/JGoogleAnalytics_0.3.jar [deleted file]
j8lib/jfreesvg-2.1.jar [deleted file]
j8lib/jfreesvg-3.4.3.jar [new file with mode: 0644]
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AlignSeq.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/AverageDistanceEngine.java
src/jalview/analytics/Plausible.java [new file with mode: 0644]
src/jalview/api/RotatableCanvasI.java
src/jalview/bin/Cache.java
src/jalview/bin/Commands.java
src/jalview/bin/Console.java
src/jalview/bin/Jalview.java
src/jalview/bin/argparser/Arg.java
src/jalview/bin/argparser/ArgParser.java
src/jalview/bin/argparser/BootstrapArgs.java
src/jalview/datamodel/ContactListImpl.java
src/jalview/datamodel/ContactMatrixI.java
src/jalview/datamodel/DBRefSource.java
src/jalview/datamodel/GroupSet.java
src/jalview/datamodel/Sequence.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/AppJmol.java
src/jalview/gui/Desktop.java
src/jalview/gui/ImageExporter.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/TreeCanvas.java
src/jalview/gui/TreePanel.java
src/jalview/io/HTMLOutput.java
src/jalview/io/RnamlFile.java
src/jalview/io/exceptions/ImageOutputException.java [new file with mode: 0644]
src/jalview/jbgui/GAlignFrame.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/renderer/ContactMapRenderer.java
src/jalview/util/ColorUtils.java
src/jalview/util/Comparison.java
src/jalview/util/HttpUtils.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/workers/AlignCalcManager.java
src/jalview/ws/datamodel/alphafold/PAEContactMatrix.java
src/jalview/ws/sifts/SiftsClient.java
test/jalview/analysis/AverageDistanceEngineTest.java
test/jalview/analysis/TestAlignSeq.java
test/jalview/bin/CommandLineOperationsNG.java
test/jalview/bin/CommandsTest.java
test/jalview/bin/CommandsTest2.java
test/jalview/datamodel/PAEContactMatrixTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/io/RNAMLfileTest.java
test/jalview/io/cache/JvCacheableInputBoxTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/ws/sifts/SiftsClientTest.java
utils/biotools/Jalview.json [new file with mode: 0644]
utils/biotools/README.md [new file with mode: 0644]
utils/debian/build_gradle.patch
utils/debian/debian_build.gradle
utils/install4j/install4j10_template.install4j
utils/jalviewjs/chromium_test/jalview_bin_Jalview-stderr.html [new file with mode: 0644]

index 3ec73d3..a2710f4 100644 (file)
@@ -42,7 +42,7 @@ jetty-http-9.2.10.v20150310.jar
 jetty-io-9.2.10.v20150310.jar
 jetty-server-9.2.10.v20150310.jar
 jetty-util-9.2.10.v20150310.jar
-jfreesvg-2.1.jar       GPL v3 licensed library from the JFree suite - http://www.jfree.org/jfreesvg/
+jfreesvg-3.4.3.jar     GPL v3 licensed library from the JFree suite - last release with Java 1.8 compatibility http://www.jfree.org/jfreesvg/
 JGoogleAnalytics_0.3.jar       APL 2.0 License - http://code.google.com/p/jgoogleanalytics/
 jhall.jar
 Jmol-NO_LOG4J-14.31.53.jar     GPL/LGPLv2 built manually from commit https://github.com/BobHanson/Jmol-SwingJS/commit/a6a2fb767e3fc2a73e72d926a11fd93a0e4c9f23 (excluded jspecview/application to compile)
index ca599a8..7ad6f99 100644 (file)
@@ -10,6 +10,10 @@ import org.gradle.plugins.ide.eclipse.model.Output
 import org.gradle.plugins.ide.eclipse.model.Library
 import java.security.MessageDigest
 import java.util.regex.Matcher
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
 import groovy.transform.ExternalizeMethods
 import groovy.util.XmlParser
 import groovy.xml.XmlUtil
@@ -568,11 +572,16 @@ ext {
   jalviewjsJ2sAltSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_alt_settings}")
   jalviewjsJ2sProps = null
   jalviewjsJ2sPlugin = jalviewjs_j2s_plugin
+  jalviewjsStderrLaunchFilename = "${jalviewjsSiteDir}/"+(file(jalviewjs_stderr_launch).getName())
 
   eclipseWorkspace = null
   eclipseBinary = string("")
   eclipseVersion = string("")
   eclipseDebug = false
+
+  jalviewjsChromiumUserDir = "${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}"
+  jalviewjsChromiumProfileDir = "${ext.jalviewjsChromiumUserDir}/${jalviewjs_chromium_profile_name}"
+
   // ENDEXT
 }
 
@@ -4201,9 +4210,157 @@ task eclipseAutoBuildTask {
 }
 
 
+task jalviewjsCopyStderrLaunchFile(type: Copy) {
+  from file(jalviewjs_stderr_launch)
+  into jalviewjsSiteDir
+
+  inputs.file jalviewjs_stderr_launch
+  outputs.file jalviewjsStderrLaunchFilename
+}
+
+task cleanJalviewjsChromiumUserDir {
+  doFirst {
+    delete jalviewjsChromiumUserDir
+  }
+  outputs.dir jalviewjsChromiumUserDir
+  // always run when depended on
+  outputs.upToDateWhen { !file(jalviewjsChromiumUserDir).exists() }
+}
+
+task jalviewjsChromiumProfile {
+  dependsOn cleanJalviewjsChromiumUserDir
+  mustRunAfter cleanJalviewjsChromiumUserDir
+
+  def firstRun = file("${jalviewjsChromiumUserDir}/First Run")
+
+  doFirst {
+    mkdir jalviewjsChromiumProfileDir
+    firstRun.text = ""
+  }
+  outputs.file firstRun
+}
+
+task jalviewjsLaunchTest {
+  group "Test"
+  description "Check JalviewJS opens in a browser"
+  dependsOn jalviewjsBuildSite
+  dependsOn jalviewjsCopyStderrLaunchFile
+  dependsOn jalviewjsChromiumProfile
+
+  def macOS = OperatingSystem.current().isMacOsX()
+  def chromiumBinary = macOS ? jalviewjs_macos_chromium_binary : jalviewjs_chromium_binary
+  if (chromiumBinary.startsWith("~/")) {
+    chromiumBinary = System.getProperty("user.home") + chromiumBinary.substring(1)
+  }
+  
+  def stdout
+  def stderr
+  doFirst {
+    def timeoutms = Integer.valueOf(jalviewjs_chromium_overall_timeout) * 1000
+    
+    def binary = file(chromiumBinary)
+    if (!binary.exists()) {
+      throw new StopExecutionException("Could not find chromium binary '${chromiumBinary}'. Cannot run task ${name}.")
+    }
+    stdout = new ByteArrayOutputStream()
+    stderr = new ByteArrayOutputStream()
+    def execStdout
+    def execStderr
+    if (jalviewjs_j2s_to_console.equals("true")) {
+      execStdout = new org.apache.tools.ant.util.TeeOutputStream(
+        stdout,
+        System.out)
+      execStderr = new org.apache.tools.ant.util.TeeOutputStream(
+        stderr,
+        System.err)
+    } else {
+      execStdout = stdout
+      execStderr = stderr
+    }
+    def execArgs = [
+      "--no-sandbox", // --no-sandbox IS USED BY THE THORIUM APPIMAGE ON THE BUILDSERVER
+      "--headless=new",
+      "--disable-gpu",
+      "--timeout=${timeoutms}",
+      "--virtual-time-budget=${timeoutms}",
+      "--user-data-dir=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_chromium_user_dir}",
+      "--profile-directory=${jalviewjs_chromium_profile_name}",
+      "--allow-file-access-from-files",
+      "--enable-logging=stderr",
+      "file://${jalviewDirAbsolutePath}/${jalviewjsStderrLaunchFilename}"
+    ]
+    
+    if (true || macOS) {
+      ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
+      Future f1 = executor.submit(
+        () -> {
+          exec {
+            standardOutput = execStdout
+            errorOutput = execStderr
+            executable(chromiumBinary)
+            args(execArgs)
+            println "COMMAND: '"+commandLine.join(" ")+"'"
+          }
+          executor.shutdownNow()
+        }
+      )
+
+      def noChangeBytes = 0
+      def noChangeIterations = 0
+      executor.scheduleAtFixedRate(
+        () -> {
+          String stderrString = stderr.toString()
+          // shutdown the task if we have a success string
+          if (stderrString.contains(jalviewjs_desktop_init_string)) {
+            f1.cancel()
+            Thread.sleep(1000)
+            executor.shutdownNow()
+          }
+          // if no change in stderr for 10s then also end
+          if (noChangeIterations >= jalviewjs_chromium_idle_timeout) {
+            executor.shutdownNow()
+          }
+          if (stderrString.length() == noChangeBytes) {
+            noChangeIterations++
+          } else {
+            noChangeBytes = stderrString.length()
+            noChangeIterations = 0
+          }
+        },
+        1, 1, TimeUnit.SECONDS)
+
+      executor.schedule(new Runnable(){
+        public void run(){
+          f1.cancel()
+          executor.shutdownNow()
+        }
+      }, timeoutms, TimeUnit.MILLISECONDS)
+
+      executor.awaitTermination(timeoutms+10000, TimeUnit.MILLISECONDS)
+      executor.shutdownNow()
+    }
+
+  }
+  
+  doLast {
+    def found = false
+    stderr.toString().eachLine { line ->
+      if (line.contains(jalviewjs_desktop_init_string)) {
+        println("Found line '"+line+"'")
+        found = true
+        return
+      }
+    }
+    if (!found) {
+      throw new GradleException("Could not find evidence of Desktop launch in JalviewJS.")
+    }
+  }
+}
+  
+
 task jalviewjs {
   group "JalviewJS"
-  description "Build the site"
+  description "Build the JalviewJS site and run the launch test"
   dependsOn jalviewjsBuildSite
+  dependsOn jalviewjsLaunchTest
 }
-
diff --git a/examples/testdata/7WKP-rna1.xml b/examples/testdata/7WKP-rna1.xml
new file mode 100644 (file)
index 0000000..2703212
--- /dev/null
@@ -0,0 +1,491 @@
+<?xml version="1.0"?>
+<!DOCTYPE rnaml SYSTEM "rnaml.dtd">
+
+<rnaml version="1.0">
+
+   <molecule id="1">
+      <sequence>
+         <numbering-system id="1" used-in-file="false">
+            <numbering-range>
+               <start>1</start>
+               <end>20</end>
+            </numbering-range>
+         </numbering-system>
+         <numbering-table length="20" comment="sequence number in pdb file">
+            1    2    3    4    5    6    7    8    9   10 
+           11   12   13   14   15   16   17   18   19   20 
+         
+         </numbering-table>
+         <seq-data>
+            GUUAGCAGCC GCAUAGGCUG 
+         </seq-data>
+         <seq-annotation comment="?">
+            <segment>
+               <seg-name>LOOP1</seg-name>
+               <base-id-5p><base-id><position>12</position></base-id></base-id-5p>
+               <base-id-3p><base-id><position>14</position></base-id></base-id-3p>
+            </segment>
+         </seq-annotation>
+      </sequence>
+      <structure>
+         <model id="?">
+            <model-info>
+               <method>Crystallography ?</method>
+               <resolution>? Angstroms</resolution>
+            </model-info>
+            <base>
+               <position>1</position>
+               <base-type>G</base-type>
+               <atom serial="1">
+                  <atom-type> P  </atom-type>
+                     <coordinates>7.232 28.529 -12.305</coordinates>
+               </atom>
+               <atom serial="9">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>4.752 23.963 -11.820</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>2</position>
+               <base-type>U</base-type>
+               <atom serial="24">
+                  <atom-type> P  </atom-type>
+                     <coordinates>3.868 23.252 -12.962</coordinates>
+               </atom>
+               <atom serial="32">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>0.983 18.432 -13.409</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>3</position>
+               <base-type>U</base-type>
+               <atom serial="44">
+                  <atom-type> P  </atom-type>
+                     <coordinates>1.438 17.540 -14.670</coordinates>
+               </atom>
+               <atom serial="52">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-1.417 16.962 -19.633</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>4</position>
+               <base-type>A</base-type>
+               <atom serial="64">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-2.710 16.020 -19.430</coordinates>
+               </atom>
+               <atom serial="72">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-7.138 19.555 -19.112</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>5</position>
+               <base-type>G</base-type>
+               <atom serial="86">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-7.829 20.393 -20.298</coordinates>
+               </atom>
+               <atom serial="94">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-11.316 24.166 -18.659</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>6</position>
+               <base-type>C</base-type>
+               <atom serial="109">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-12.021 24.790 -19.958</coordinates>
+               </atom>
+               <atom serial="117">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-14.686 21.023 -22.223</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>7</position>
+               <base-type>A</base-type>
+               <atom serial="129">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-16.102 21.690 -22.577</coordinates>
+               </atom>
+               <atom serial="137">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-20.284 18.901 -22.582</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>8</position>
+               <base-type>G</base-type>
+               <atom serial="151">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-21.555 19.844 -22.854</coordinates>
+               </atom>
+               <atom serial="159">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-26.285 20.028 -19.599</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>9</position>
+               <base-type>C</base-type>
+               <atom serial="174">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-27.355 21.190 -19.880</coordinates>
+               </atom>
+               <atom serial="182">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-30.900 21.877 -16.104</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>10</position>
+               <base-type>C</base-type>
+               <atom serial="194">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-31.737 23.147 -16.621</coordinates>
+               </atom>
+               <atom serial="202">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-33.685 26.755 -13.690</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>11</position>
+               <base-type>G</base-type>
+               <atom serial="214">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-34.360 27.602 -14.877</coordinates>
+               </atom>
+               <atom serial="222">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-34.025 32.801 -15.050</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>12</position>
+               <base-type>C</base-type>
+               <atom serial="237">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-35.037 32.875 -16.296</coordinates>
+               </atom>
+               <atom serial="245">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-31.850 33.486 -20.341</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>13</position>
+               <base-type>A</base-type>
+               <atom serial="257">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-30.767 32.411 -19.837</coordinates>
+               </atom>
+               <atom serial="265">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-25.756 33.919 -20.714</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>14</position>
+               <base-type>U</base-type>
+               <atom serial="279">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-24.465 33.135 -20.171</coordinates>
+               </atom>
+               <atom serial="287">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-21.219 37.107 -19.013</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>15</position>
+               <base-type>A</base-type>
+               <atom serial="299">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-22.129 38.268 -18.376</coordinates>
+               </atom>
+               <atom serial="307">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-20.542 37.656 -12.798</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>16</position>
+               <base-type>G</base-type>
+               <atom serial="321">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-19.666 36.373 -12.391</coordinates>
+               </atom>
+               <atom serial="329">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-21.101 34.091 -7.803</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>17</position>
+               <base-type>G</base-type>
+               <atom serial="344">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-20.251 32.728 -7.687</coordinates>
+               </atom>
+               <atom serial="352">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-22.951 28.722 -5.266</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>18</position>
+               <base-type>C</base-type>
+               <atom serial="367">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-21.797 27.634 -5.014</coordinates>
+               </atom>
+               <atom serial="375">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-23.692 22.611 -5.084</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>19</position>
+               <base-type>U</base-type>
+               <atom serial="387">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-22.583 21.449 -4.989</coordinates>
+               </atom>
+               <atom serial="395">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-22.919 16.822 -7.040</coordinates>
+               </atom>
+            </base>
+            <base>
+               <position>20</position>
+               <base-type>G</base-type>
+               <atom serial="407">
+                  <atom-type> P  </atom-type>
+                     <coordinates>-21.585 16.199 -6.397</coordinates>
+               </atom>
+               <atom serial="415">
+                  <atom-type> O3'</atom-type>
+                     <coordinates>-18.522 13.024 -10.277</coordinates>
+               </atom>
+            </base>
+            <str-annotation>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>1</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>2</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>S</edge-5p>
+                  <edge-3p>H</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>6</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>20</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>+</edge-5p>
+                  <edge-3p>+</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>7</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>19</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>-</edge-5p>
+                  <edge-3p>-</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>8</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>18</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>+</edge-5p>
+                  <edge-3p>+</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>9</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>17</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>+</edge-5p>
+                  <edge-3p>+</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>10</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>16</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>+</edge-5p>
+                  <edge-3p>+</edge-3p>
+                  <bond-orientation>c</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>11</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>15</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>S</edge-5p>
+                  <edge-3p>H</edge-3p>
+                  <bond-orientation>t</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>3</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>4</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>!</edge-5p>
+                  <edge-3p>!</edge-3p>
+                  <bond-orientation>!</bond-orientation>
+               </base-pair>
+               <base-pair comment="?">
+                  <base-id-5p>
+                     <base-id><position>14</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>15</position></base-id>
+                  </base-id-3p>
+                  <edge-5p>!</edge-5p>
+                  <edge-3p>!</edge-3p>
+                  <bond-orientation>!</bond-orientation>
+               </base-pair>
+               <helix id="H1">
+                  <base-id-5p>
+                     <base-id><position>6</position></base-id>
+                  </base-id-5p>
+                  <base-id-3p>
+                     <base-id><position>20</position></base-id>
+                  </base-id-3p>
+                  <length>6</length>
+               </helix>
+               <single-strand>
+                  <segment>
+                     <seg-name>SG1</seg-name>
+                     <base-id-5p><base-id><position>1</position></base-id></base-id-5p>
+                     <base-id-3p><base-id><position>5</position></base-id></base-id-3p>
+                  </segment>
+               </single-strand>
+               <single-strand>
+                  <segment>
+                     <seg-name>SG2</seg-name>
+                     <base-id-5p><base-id><position>12</position></base-id></base-id-5p>
+                     <base-id-3p><base-id><position>14</position></base-id></base-id-3p>
+                  </segment>
+               </single-strand>
+            </str-annotation>
+            <secondary-structure-display comment="x,y coodinates">
+               <ss-base-coord>
+                  <base-id><position>1</position></base-id>
+                  <coordinates>133.930 0.000</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>2</position></base-id>
+                  <coordinates>133.930 33.482</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>3</position></base-id>
+                  <coordinates>133.930 66.965</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>4</position></base-id>
+                  <coordinates>133.930 100.447</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>5</position></base-id>
+                  <coordinates>133.930 133.930</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>6</position></base-id>
+                  <coordinates>133.930 167.412</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>7</position></base-id>
+                  <coordinates>133.930 223.216</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>8</position></base-id>
+                  <coordinates>133.930 279.021</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>9</position></base-id>
+                  <coordinates>133.930 334.825</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>10</position></base-id>
+                  <coordinates>133.930 390.629</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>11</position></base-id>
+                  <coordinates>133.930 446.433</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>12</position></base-id>
+                  <coordinates>128.630 516.439</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>13</position></base-id>
+                  <coordinates>66.965 550.000</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>14</position></base-id>
+                  <coordinates>5.300 516.439</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>15</position></base-id>
+                  <coordinates>0.000 446.433</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>16</position></base-id>
+                  <coordinates>0.000 390.629</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>17</position></base-id>
+                  <coordinates>0.000 334.825</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>18</position></base-id>
+                  <coordinates>0.000 279.021</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>19</position></base-id>
+                  <coordinates>0.000 223.216</coordinates>
+               </ss-base-coord>
+               <ss-base-coord>
+                  <base-id><position>20</position></base-id>
+                  <coordinates>0.000 167.412</coordinates>
+               </ss-base-coord>
+            </secondary-structure-display>
+         </model>
+      </structure>
+   </molecule>
+
+
+   <interactions>
+            <str-annotation>
+            </str-annotation>
+   </interactions>
+</rnaml>
index 7b3a031..547304f 100644 (file)
@@ -201,7 +201,7 @@ jalviewjs_j2s_alt_file_property_config = j2s.config.altfileproperty
 # for developing in Eclipse as IDE, set this to automatically copy current swingjs/net.sf.j2s.core.jar to your dropins dir
 jalviewjs_eclipseIDE_auto_copy_j2s_plugin = false
 # Override this in a local.properties file
-jalviewjs_eclipse_root = ~/buildtools/eclipse/jee-2019-09
+jalviewjs_eclipse_root = ~/buildtools/eclipse/latest
 
 jalviewjs_eclipse_dropins_dir = utils/jalviewjs/eclipse/dropins
 jalviewjs_swingjs_zip = swingjs/SwingJS-site.zip
@@ -260,5 +260,14 @@ jalviewjs_j2s_to_console = true
 jalviewjs_closure_compiler = tools/closure_compiler.jar
 jalviewjs_j2s_closure_stdout = j2s-closure.out
 
+# for checking jalviewjs launches okay
+jalviewjs_chromium_binary = ~/buildtools/chromium/chrome
+jalviewjs_macos_chromium_binary = /Applications/Chromium.app/Contents/MacOS/Chromium
+jalviewjs_chromium_user_dir = chromium
+jalviewjs_chromium_idle_timeout = 10
+jalviewjs_chromium_overall_timeout = 40
+jalviewjs_chromium_profile_name = BUILD
+jalviewjs_stderr_launch = utils/jalviewjs/chromium_test/jalview_bin_Jalview-stderr.html
+jalviewjs_desktop_init_string = JALVIEWJS: CREATED DESKTOP
 
 testp=gradle.properties
index fc4f0a0..dd0bd52 100644 (file)
     </tr>
 
     <tr valign="top">
+    <td><code>&#8209;&#8209;nostartupfile</code></td>
+    <td>Don't show the default startup file.</td>
+    </tr>
+
+    <tr valign="top">
     <td><code>&#8209;&#8209;webservicediscovery / &#8209;&#8209;nowebservicediscovery</code></td>
     <td>Attempt (/ or don't attempt) to connect to JABAWS web services.</td>
     </tr>
 
 <!--
     <tr valign="top">
+    <td><code>&#8209;&#8209;P<em>PREFERENCE=VALUE</em></code></td>
+    <td>Set a Jalview preference for this session only.  Experimental.</td>
+    </tr>
+-->
+
+<!--
+    <tr valign="top">
     <td><code>&#8209;&#8209;initsubstitutions / &#8209;&#8209;noinitsubstitutions</code></td>
     <td>Set <code>&#8209;&#8209;substitutions</code> to be initially enabled (or initially disabled).</td>
     </tr>
     </tr>
 
     <tr valign="top">
-    <td><code>&#8209;&#8209;title&nbsp;<em>"string""</em></code></td>
+    <td><code>&#8209;&#8209;title&nbsp;<em>"string"</em></code></td>
     <td>Specifies the title for the open alignment window as <em>string</em>.</td>
     <td></td>
     <td align="center">&#x2713;</td>
 
     <tr valign="top">
     <td><code>&#8209;&#8209;colour&nbsp;<em>name</em></code></td>
-    <td>Applies the colour scheme <em>name</em> to the open alignment window.  Valid values for <em>name</em> are:
+    <td>Applies the colour scheme <em>name</em> to the open alignment window.  Valid values for <em>name</em> include:
     <br/>
     <code>clustal</code>,
     <br/>
     <code>t-coffee-scores</code>,
     <br/>
     <code>sequence-id</code>.
+    <br/>
+    <br/>
+    Names of user defined schemes will also work, and jalview colour scheme specifications like:
+    <br/>
+       <code>&#8209;&#8209;colour "D,E=red; K,R,H=0022FF; C,c=yellow"</code>
     <td></td>
     <td align="center">&#x2713;</td>
     </tr>
index 9ad61b5..8704ce8 100644 (file)
   <p>Usage data is collected from the logs of various web services
     that the Jalview Desktop contacts through its normal operation.
     These are described below:</p>
-  <ul>
-    <li><em>HTTP logs on the Jalview website</em><br> We
-      record IP addresses of machines which access the web site, either
-      via the browser when downloading the application, or when the
-      Jalview Desktop user interface is launched.<br> <br>
-      <ul>
-        <li><i>The Jalview Getdown Launcher</i> (Since 2.11.0) examines release
-          channels every time Jalview launches to determine if a new
-          release is available.</li>
-        <li><i>The questionnaire web service at
-            www.jalview.org/cgi-bin/questionnaire.pl is checked and a
-            unique cookie for the current questionnaire is stored in the
-            Jalview properties file.</i></li>
-        <li><i>The Jalview web services stack is contacted to
-            retrieve the currently available web services. All
-            interactions with the public Jalview web services are
-            logged, but we delete all job data (input data and results)
-            after about two weeks.</i></li>
-      </ul> <br></li>
-    <li><em>Google Analytics</em><br> Since Jalview 2.4.0b2,
-      the Jalview Desktop records usage data with Google Analytics via
-      the <a href="http://code.google.com/p/jgoogleanalytics/">JGoogleAnalytics</a>
-      class.<br> The Google Analytics logs for Jalview version 2.4
-      only record the fact that the application was started, but in the
-      future, we will use this mechanism to improve the Desktop user
-      interface, by tracking which parts of the user interface are being
-      used most often.</li>
-  </ul>
-  </p>
+       <ul>
+               <li><em>HTTP logs on the Jalview website</em><br> We record
+                       IP addresses of machines which access the web site, either via the
+                       browser when downloading the application, or when the Jalview Desktop
+                       user interface is launched.<br> <br>
+                       <ul>
+                               <li><i>The Jalview Getdown Launcher</i> (Since 2.11.0) examines
+                                       release channels every time Jalview launches to determine if a new
+                                       release is available.</li>
+                               <li><i>The questionnaire web service at
+                                               www.jalview.org/cgi-bin/questionnaire.pl is checked and a unique
+                                               cookie for the current questionnaire is stored in the Jalview
+                                               properties file.</i></li>
+                               <li><i>The Jalview web services stack is contacted to
+                                               retrieve the currently available web services. All interactions
+                                               with the public Jalview web services are logged, but we delete all
+                                               job data (input data and results) after about two weeks.</i></li>
+                       </ul> <br></li>
+               <li><em>Usage Analytics</em><br> Since Jalview 2.11.2.7, the
+                       Jalview Desktop records usage data with a self-hosted instance of the
+                       analytics stack <a href="https://plausible.io">Plausible.io</a> via a
+                       custom GPLv3 client developed by Ben Soares. Prior to this, Jalview
+                       versions as far back as 2.4 recorded application launches via <a
+                       href="http://code.google.com/p/jgoogleanalytics/">JGoogleAnalytics</a>
+                       .<br> Usage logs for Jalview record the fact that the
+                       application was started, and details about the OS, installed Jalview
+                       launcher (if any) and java version used. In the future, we will use
+                       this mechanism to improve the Desktop user interface, by tracking
+                       which parts of the user interface are being used most often.</li>
+       </ul>
   <p>
     <strong>Stopping Jalview from calling home</strong><br> If you
     run Jalview in 'headless mode' via the command line, then the
diff --git a/help/markdown/releases/release-2_11_2_7.md b/help/markdown/releases/release-2_11_2_7.md
new file mode 100644 (file)
index 0000000..361d39a
--- /dev/null
@@ -0,0 +1,12 @@
+---
+version: 2.11.2.7
+date: 2023-06-30
+channel: "release"
+---
+
+## New Features
+- <!-- JAL-4001 --> Jalview now reports usage statistics via Plausible.io
+
+## Issues Resolved
+- <!-- JAL-4116 --> PDB structures slow to view when Jalview Java console is open
+- <!-- JAL-4216 --> chains in PDB or mmCIF files with negative RESNUMs not correctly parsed
index 31cabd7..a433bc6 100644 (file)
@@ -1,6 +1,6 @@
 ---
 version: 2.11.3.0
-date: 2023-03-07
+date: 2023-07-19
 channel: "release"
 ---
 
@@ -13,36 +13,77 @@ channel: "release"
 - <!-- JAL-4019 --> Ambiguous Base Colourscheme
 - <!-- JAL-4061 --> Find can search sequence features' type and description
 - <!-- JAL-4062 --> Hold down Shift + CMD/CTRL C to copy highlighted regions as new sequences
+- <!-- JAL-1556 --> Quickly enable select and/or colour by for displayed annotation row via its popup menu
+- <!-- JAL-4094 --> Shift+Click+Drag to adjust height of all annotation tracks of same type
+- <!-- JAL-4190 --> Pressing escape in tree panel clears any current selection
+
 - <!-- JAL-4089 --> Use selected columns for superposition
 - <!-- JAL-4086 --> Highlight aligned positions on all associated structures when mousing over a column
 
+- <!-- JAL-4221 --> sequence descriptions are updated from database reference sources if not already defined
+
+
+### Improved support for working with computationally determined models
+
 - <!-- JAL-3895 --> Alphafold red/orange/yellow/green colourscheme for structures
 - <!-- JAL-4095 --> Interactive picking of low pAE score regions
 - <!-- JAL-4027 --> contact matrix datatype in Jalview
 - <!-- JAL-4033 --> Selections with visual feedback via contact matrix annotation
 
-- <!-- JAL-4075 --> Don't add string label version of DSSP secondary structure codes in secondary structure annotation rows
 - <!-- JAL-3855 --> Discover and import alphafold2 models and metadata from https://alphafold.ebi.ac.uk/
 
-- <!-- JAL-2961 --> Jmol view not always centred on structures when multiple structures are viewed
+- <!-- JAL-4091 --> Visual indication of relationship with associated sequence to distinguish different sequence associated annotation rows
+- <!-- JAL-4123 --> GUI and command line allows configuration of how temperature factor in imported 3D structure data should be interpreted
+- <!-- JAL-3914 --> Import model reliability scores encoded as temperature factor annotation with their correct name and semantics
+- <!-- JAL-3858 --> Import and display alphafold alignment uncertainty matrices from JSON
+- <!-- JAL-4134,JAL-4158 --> Column-wise alignment groups and selections and interactive tree viewer for PAE matrices
+- <!-- JAL-4124 --> Store/Restore PAE data and visualisation settings from Jalview Project
+- <!-- JAL-4083 --> Multiple residue sidechain highlighting in structure viewers from PAE mouseovers
 
-- <!-- JAL-2528, JAL-1713 --> Overview window is saved in project file, and state of 'show hidden regions' is preserved.
 
+### Jalview on the command line
+
+- <!-- JAL-4160,JAL-629 --> New command line argument framework allowing flexible batch processing, figure generation, and import of structures, pae matrices and other sequence associated data
+- <!-- JAL-4121 --> Assume --headless when jalview is run with a command line argument that generates output
+
+### Other improvements
+
+
+- <!-- JAL-4250 --> Secondary structure annotation glyphs are rendered anti-aliasing when enabled
+- <!-- JAL-325 --> Helix and Sheet glyphs vertically centered with respect to grey coil secondary structure annotation track
+- <!-- JAL-4253 --> Lower line of the sequence group border does not align with vertical and background residue box
+- <!-- JAL-4250 --> Updated JFreeSVG (https://www.jfree.org/jfreesvg) from 2.1 to 3.4.3
 - <!-- JAL-3119 --> Name of alignment and view included in overview window's title
+- <!-- JAL-4213 --> "add reference annotation" add all positions in reference annotation tracks, not just positions in the currently highlighted columns/selection range
+- <!-- JAL-4119 --> EMBL-EBI SIFTS file downloads now use split directories
 
-- <!-- JAL-4091 --> Visual indication of relationship with associated sequence to distinguish different sequence associated annotation rows
-- <!-- JAL-4094 --> Shift+Click+Drag to adjust height of all annotation tracks of same type
+- <!-- JAL-4195,JAL-4194,JAL-4193 --> sensible responses from the CLI when things go wrong during image export
+Add a command line option to set Jalview properties for this session only
+Add a command line option to suppress opening the startup file for this session
+
+
+JAL-4187        Powershell launcher script fails when given no arguments with the old ArgsParser
+
+known issue ? <!-- JAL-4127    --> 'Reload' for a jalview project results in all windows being duplicated
 
 
+- <!-- JAL-3830 --> Command-line wrapper script for macOS bundle, linux and Windows installations (bash, powershell and .bat wrappers)
+- <!-- JAL-3820 --> In Linux desktops' task-managers, the grouped Jalview windows get a generic name
+
 ## Still in progress (delete on release)
 
 - <!-- JAL-2382 --> Import and display sequence-associated contact predictions in CASP-RR format
 - <!-- JAL-2349 --> Contact prediction visualisation
 - <!-- JAL-2348 --> modularise annotation renderer
 
+### Development and Deployment
 
+- <!-- JAL-4167 --> Create separate gradle test task for some tests
+- <!-- JAL-4111 --> Allow gradle build to create suffixed DEVELOP-... builds with channel appbase
+- <!-- JAL-4243 --> Jalview bio.tools description maintained under jalview's git repo and bundled with source release
 
 ## Issues Resolved
+- <!-- JAL-2961 --> Jmol view not always centred on structures when multiple structures are viewed
 - <!-- JAL-3776 --> Cancelling interactive calculation leaves empty progress bar.
 - <!-- JAL-3772 --> Unsaved Alignment windows close without prompting to save, individually or at application quit.
 - <!-- JAL-1988 --> Can quit Jalview while 'save project' is in progress
@@ -52,3 +93,20 @@ channel: "release"
 - <!-- JAL-3785 --> Overview windows opened automatically (due to preferences settings) lack title
 - <!-- JAL-2353 --> Show Crossrefs fails to retrieve CDS from ENA or Ensembl for sequences retrieved from Uniprot due to version numbers in cross-reference accession
 - <!-- JAL-4184 --> Stockholm export does not include sequence descriptions
+- <!-- JAL-4075 --> Don't add string label version of DSSP secondary structure codes in secondary structure annotation rows
+- <!-- JAL-4182 --> reference annotation not correctly transferred to alignment containing a sub-sequence when a selection is active
+- <!-- JAL-4177 --> Can press 'Add' or 'New View' multiple times when manually adding and viewing a 3D structure via structure chooser
+- <!-- JAL-4133 --> Jalview project does not preserve font aspect ratio when Viewport is zoomed with mouse
+- <!-- JAL-4128 --> Resizing overview quickly with solid-drags enabled causes exception
+- <!-- JAL-4150 --> Sequences copied to clipboard from within Jalview cannot be pasted via the desktop's popup menu to a new alignment window
+- <!-- JAL-2528, JAL-1713 --> Overview window is saved in project file, and state of 'show hidden regions' is preserved.
+- <!-- JAL-4153 --> JvCacheableInputBoxTest flaky on build server
+
+## New Known defects
+- <!-- JAL-4178 --> Cannot cancel structure view open action once it has been started via the structure chooser dialog
+- <!-- JAL-4142 --> Example project's multiple views do not open in distinct locations when eXpand views is used to show them all separately
+- <!-- JAL-4165 --> Missing last letter when copying consensus sequence from alignment if first column is hidden
+
+
+
+
diff --git a/help/markdown/whatsnew/whatsnew-2_11_2_7.md b/help/markdown/whatsnew/whatsnew-2_11_2_7.md
new file mode 100644 (file)
index 0000000..f884134
--- /dev/null
@@ -0,0 +1,5 @@
+Jalview 2.11.2.7 is a minor patch release - it includes patches affecting efficiency when importing structures and a small revision to the import processing of structures with negative residue numbering. 
+
+With this release, Jalview usage statistics are now collected by a jalview.org hosted instance of the open source privacy-preserving analytics stack, Plausible.io. 
+
+
index 59495d5..ec82f83 100644 (file)
@@ -1,5 +1,7 @@
 The 2.11.3 series includes support for in-depth exploration of predicted alignment error matrices from AlphaFold in the context of multiple alignments, along with support for standard colourschemes for shading models according to their pLDDT.
 
+We're launching this release at ISMB 2023 - come find us !
+
 It also introduces new support for native ARM-based OSX architectures, and a few other goodies!
 
 
diff --git a/j11lib/JGoogleAnalytics_0.3.jar b/j11lib/JGoogleAnalytics_0.3.jar
deleted file mode 100644 (file)
index 0dbc98c..0000000
Binary files a/j11lib/JGoogleAnalytics_0.3.jar and /dev/null differ
diff --git a/j11lib/jfreesvg-2.1.jar b/j11lib/jfreesvg-2.1.jar
deleted file mode 100644 (file)
index 91d453c..0000000
Binary files a/j11lib/jfreesvg-2.1.jar and /dev/null differ
diff --git a/j11lib/jfreesvg-3.4.3.jar b/j11lib/jfreesvg-3.4.3.jar
new file mode 100644 (file)
index 0000000..cfd1463
Binary files /dev/null and b/j11lib/jfreesvg-3.4.3.jar differ
diff --git a/j8lib/JGoogleAnalytics_0.3.jar b/j8lib/JGoogleAnalytics_0.3.jar
deleted file mode 100644 (file)
index 0dbc98c..0000000
Binary files a/j8lib/JGoogleAnalytics_0.3.jar and /dev/null differ
diff --git a/j8lib/jfreesvg-2.1.jar b/j8lib/jfreesvg-2.1.jar
deleted file mode 100644 (file)
index 91d453c..0000000
Binary files a/j8lib/jfreesvg-2.1.jar and /dev/null differ
diff --git a/j8lib/jfreesvg-3.4.3.jar b/j8lib/jfreesvg-3.4.3.jar
new file mode 100644 (file)
index 0000000..cfd1463
Binary files /dev/null and b/j8lib/jfreesvg-3.4.3.jar differ
index 4200a46..924b9cb 100644 (file)
@@ -1452,6 +1452,8 @@ label.choose_tempfac_type = Choose Temperature Factor type
 label.interpret_tempfac_as = Interpret Temperature Factor as
 label.add_pae_matrix_file = Add PAE matrix file
 label.nothing_selected = Nothing selected
+prompt.analytics_title = Jalview Usage Statistics
+prompt.analytics = Do you want to help make Jalview better by enabling the collection of usage statistics with Plausible analytics?\nYou can enable or disable usage tracking in the preferences.
 label.working_ellipsis = Working ... 
 action.show_groups_on_matrix = Show groups on matrix
 action.show_groups_on_matrix_tooltip = When enabled, clusters defined on the matrix's associated tree or below the assigned threshold are shown as different colours on the matrix annotation row
index b3c6988..150a407 100644 (file)
@@ -1434,3 +1434,5 @@ label.tftype_default = Default
 label.tftype_plddt = pLDDT
 label.add_pae_matrix_file = Añadir un fichero de matriz PAE
 label.nothing_selected = Nada seleccionado
+prompt.analytics_title = Jalview Estadísticas de Uso
+prompt.analytics = ¿Quiere ayudar a mejorar Jalview habilitando la recopilación de estadísticas de uso con análisis Plausible?\nPuede habilitar o deshabilitar el seguimiento de uso en las preferencias.
index 65fd110..02b3f41 100755 (executable)
@@ -890,7 +890,8 @@ public class AlignSeq
         pdbpos++;
       }
 
-      if (allowmismatch || c1 == c2)
+      // ignore case differences
+      if (allowmismatch || (c1 == c2) || (Math.abs(c2-c1)==('a'-'A')))
       {
         // extend mapping interval
         if (lp1 + 1 != alignpos || lp2 + 1 != pdbpos)
index 1158c53..6ab49b2 100644 (file)
@@ -1512,7 +1512,7 @@ public class AlignmentUtils
    * @param alignment
    *          the alignment to add them to
    * @param selectionGroup
-   *          current selection group (or null if none)
+   *          current selection group - may be null, if provided then any added annotation will be trimmed to just those columns in the selection group
    */
   public static void addReferenceAnnotations(
           Map<SequenceI, List<AlignmentAnnotation>> annotations,
@@ -1536,7 +1536,7 @@ public class AlignmentUtils
    * @param seq
    * @param ann
    * @param selectionGroup
-   *          - may be null
+   *          current selection group - may be null, if provided then any added annotation will be trimmed to just those columns in the selection group
    * @return annotation added to {@code seq and {@code alignment}
    */
   public static AlignmentAnnotation addReferenceAnnotationTo(
index f4d69d5..d81dd44 100644 (file)
@@ -44,29 +44,31 @@ public class AverageDistanceEngine extends TreeEngine
 
   AlignmentAnnotation aa;
 
+  // 0 - normalised dot product
+  // 1 - L1 - ie (abs(v_1-v_2)/dim(v))
+  // L1 is more rational - since can reason about value of difference,
+  // normalised dot product might give cleaner clusters, but more difficult to
+  // understand.
+
+  int mode = 1;
+
   /**
    * compute cosine distance matrix for a given contact matrix and create a
    * UPGMA tree
-   * 
    * @param cm
+   * @param cosineOrDifference false - dot product : true - L1
    */
   public AverageDistanceEngine(AlignmentViewport av, AlignmentAnnotation aa,
-          ContactMatrixI cm)
+          ContactMatrixI cm, boolean cosineOrDifference)
   {
     this.av = av;
     this.aa = aa;
     this.cm = cm;
+    mode = (cosineOrDifference) ? 1 :0; 
     calculate(cm);
 
   }
 
-  // 0 - normalised dot product
-  // 1 - L1 - ie (abs(v_1-v_2)/dim(v))
-  // L1 is more rational - since can reason about value of difference,
-  // normalised dot product might give cleaner clusters, but more difficult to
-  // understand.
-
-  int mode = 1;
 
   public void calculate(ContactMatrixI cm)
   {
diff --git a/src/jalview/analytics/Plausible.java b/src/jalview/analytics/Plausible.java
new file mode 100644 (file)
index 0000000..ab2de77
--- /dev/null
@@ -0,0 +1,602 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analytics;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import jalview.bin.Cache;
+import jalview.bin.Console;
+import jalview.util.ChannelProperties;
+import jalview.util.HttpUtils;
+
+public class Plausible
+{
+  private static final String USER_AGENT;
+
+  private static final String JALVIEW_ID = "Jalview Desktop";
+
+  private static final String DOMAIN = "jalview.org";
+
+  private static final String CONFIG_API_BASE_URL = "https://www.jalview.org/services/config/analytics/url";
+
+  private static final String DEFAULT_API_BASE_URL = "https://analytics.jalview.org/api/event";
+
+  private static final String API_BASE_URL;
+
+  private static final String clientId;
+
+  public static final String APPLICATION_BASE_URL = "desktop://localhost";
+
+  private List<Map.Entry<String, String>> queryStringValues;
+
+  private List<Map.Entry<String, Object>> jsonObject;
+
+  private List<Map.Entry<String, String>> cookieValues;
+
+  private static boolean ENABLED = false;
+
+  private static boolean DEBUG = true;
+
+  private static Plausible instance = null;
+
+  private static final Map<String, String> defaultProps;
+
+  static
+  {
+    defaultProps = new HashMap<>();
+    defaultProps.put("app_name",
+            ChannelProperties.getProperty("app_name") + " Desktop");
+    defaultProps.put("version", Cache.getProperty("VERSION"));
+    defaultProps.put("build_date",
+            Cache.getDefault("BUILD_DATE", "unknown"));
+    defaultProps.put("java_version", System.getProperty("java.version"));
+    String val = System.getProperty("sys.install4jVersion");
+    if (val != null)
+    {
+      defaultProps.put("install4j_version", val);
+    }
+    val = System.getProperty("installer_template_version");
+    if (val != null)
+    {
+      defaultProps.put("install4j_template_version", val);
+    }
+    val = System.getProperty("launcher_version");
+    if (val != null)
+    {
+      defaultProps.put("launcher_version", val);
+    }
+    defaultProps.put("java_arch",
+            System.getProperty("os.arch") + " "
+                    + System.getProperty("os.name") + " "
+                    + System.getProperty("os.version"));
+    defaultProps.put("os", System.getProperty("os.name"));
+    defaultProps.put("os_version", System.getProperty("os.version"));
+    defaultProps.put("os_arch", System.getProperty("os.arch"));
+    String installation = Cache.applicationProperties
+            .getProperty("INSTALLATION");
+    if (installation != null)
+    {
+      defaultProps.put("installation", installation);
+    }
+
+    // ascertain the API_BASE_URL
+    API_BASE_URL = getAPIBaseURL();
+
+    // random clientId to make User-Agent unique (to register analytic)
+    clientId = String.format("%08x", new Random().nextInt());
+
+    USER_AGENT = HttpUtils.getUserAgent(
+            MethodHandles.lookup().lookupClass().getCanonicalName() + " "
+                    + clientId);
+  }
+
+  private Plausible()
+  {
+    this.resetLists();
+  }
+
+  public static void setEnabled(boolean b)
+  {
+    ENABLED = b;
+  }
+
+  public void sendEvent(String eventName, String urlString,
+          String... propsStrings)
+  {
+    sendEvent(eventName, urlString, false, propsStrings);
+  }
+
+  /**
+   * The simplest way to send an analytic event.
+   * 
+   * @param eventName
+   *          The event name. To emulate a webpage view use "pageview" and set a
+   *          "url" key/value. See https://plausible.io/docs/events-api
+   * @param sendDefaultProps
+   *          Flag whether to add the default props about the application.
+   * @param propsStrings
+   *          Optional multiple Strings in key, value pairs (there should be an
+   *          even number of propsStrings) to be set as property of the event.
+   *          To emulate a webpage view set "url" as the URL in a "pageview"
+   *          event.
+   */
+  public void sendEvent(String eventName, String urlString,
+          boolean sendDefaultProps, String... propsStrings)
+  {
+    // clear out old lists
+    this.resetLists();
+
+    if (!ENABLED)
+    {
+      Console.debug("Plausible not enabled.");
+      return;
+    }
+    Map<String, String> props = new HashMap<>();
+
+    // add these to all events from this application instance
+    if (sendDefaultProps)
+    {
+      props.putAll(defaultProps);
+    }
+
+    // add (and overwrite with) the passed in props
+    if (propsStrings != null && propsStrings.length > 0)
+    {
+      if (propsStrings.length % 2 != 0)
+      {
+        Console.warn(
+                "Cannot addEvent with odd number of propsStrings.  Ignoring the last one.");
+      }
+      for (int i = 0; i < propsStrings.length - 1; i += 2)
+      {
+        String key = propsStrings[i];
+        String value = propsStrings[i + 1];
+        props.put(key, value);
+      }
+    }
+
+    addJsonValue("domain", DOMAIN);
+    addJsonValue("name", eventName);
+    StringBuilder eventUrlSb = new StringBuilder(APPLICATION_BASE_URL);
+    if (!APPLICATION_BASE_URL.endsWith("/") && !urlString.startsWith("/"))
+    {
+      eventUrlSb.append("/");
+    }
+    eventUrlSb.append(urlString);
+    addJsonValue("url", eventUrlSb.toString());
+    addJsonObject("props", props);
+    StringBuilder urlSb = new StringBuilder();
+    urlSb.append(API_BASE_URL);
+    String qs = buildQueryString();
+    if (qs != null && qs.length() > 0)
+    {
+      urlSb.append('?');
+      urlSb.append(qs);
+    }
+    try
+    {
+      URL url = new URL(urlSb.toString());
+      URLConnection urlConnection = url.openConnection();
+      HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
+      httpURLConnection.setRequestMethod("POST");
+      httpURLConnection.setDoOutput(true);
+
+      String jsonString = buildJson();
+
+      Console.debug(
+              "Plausible: HTTP Request is: '" + urlSb.toString() + "'");
+      if (DEBUG)
+      {
+        Console.debug("Plausible: User-Agent is: '" + USER_AGENT + "'");
+      }
+      Console.debug("Plausible: POSTed JSON is:\n" + jsonString);
+
+      byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);
+      int jsonLength = jsonBytes.length;
+
+      httpURLConnection.setFixedLengthStreamingMode(jsonLength);
+      httpURLConnection.setRequestProperty("Content-Type",
+              "application/json");
+      httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
+      httpURLConnection.connect();
+      try (OutputStream os = httpURLConnection.getOutputStream())
+      {
+        os.write(jsonBytes);
+      }
+      int responseCode = httpURLConnection.getResponseCode();
+      String responseMessage = httpURLConnection.getResponseMessage();
+
+      if (responseCode < 200 || responseCode > 299)
+      {
+        Console.warn("Plausible connection failed: '" + responseCode + " "
+                + responseMessage + "'");
+      }
+      else
+      {
+        Console.debug("Plausible connection succeeded: '" + responseCode
+                + " " + responseMessage + "'");
+      }
+
+      if (DEBUG)
+      {
+        BufferedReader br = new BufferedReader(new InputStreamReader(
+                (httpURLConnection.getInputStream())));
+        StringBuilder sb = new StringBuilder();
+        String response;
+        while ((response = br.readLine()) != null)
+        {
+          sb.append(response);
+        }
+        String body = sb.toString();
+        Console.debug("Plausible response content:\n" + body);
+      }
+    } catch (MalformedURLException e)
+    {
+      Console.debug(
+              "Somehow the Plausible BASE_URL and queryString is malformed: '"
+                      + urlSb.toString() + "'",
+              e);
+      return;
+    } catch (IOException e)
+    {
+      Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
+              + "' failed.", e);
+    } catch (ClassCastException e)
+    {
+      Console.debug(
+              "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
+              e);
+    }
+  }
+
+  private void addJsonObject(String key, Map<String, String> map)
+  {
+    List<Map.Entry<String, ? extends Object>> list = new ArrayList<>();
+    for (String k : map.keySet())
+    {
+      list.add(stringEntry(k, map.get(k)));
+    }
+    addJsonObject(key, list);
+
+  }
+
+  private void addJsonObject(String key,
+          List<Map.Entry<String, ? extends Object>> object)
+  {
+    jsonObject.add(objectEntry(key, object));
+  }
+
+  private void addJsonValues(String key, List<Object> values)
+  {
+    jsonObject.add(objectEntry(key, values));
+  }
+
+  private void addJsonValue(String key, String value)
+  {
+    jsonObject.add(objectEntry(key, value));
+  }
+
+  private void addJsonValue(String key, int value)
+  {
+    jsonObject.add(objectEntry(key, Integer.valueOf(value)));
+  }
+
+  private void addJsonValue(String key, boolean value)
+  {
+    jsonObject.add(objectEntry(key, Boolean.valueOf(value)));
+  }
+
+  private void addQueryStringValue(String key, String value)
+  {
+    queryStringValues.add(stringEntry(key, value));
+  }
+
+  private void addCookieValue(String key, String value)
+  {
+    cookieValues.add(stringEntry(key, value));
+  }
+
+  private void resetLists()
+  {
+    jsonObject = new ArrayList<>();
+    queryStringValues = new ArrayList<>();
+    cookieValues = new ArrayList<>();
+  }
+
+  public static Plausible getInstance()
+  {
+    if (instance == null)
+    {
+      instance = new Plausible();
+    }
+    return instance;
+  }
+
+  public static void reset()
+  {
+    getInstance().resetLists();
+  }
+
+  private String buildQueryString()
+  {
+    StringBuilder sb = new StringBuilder();
+    for (Map.Entry<String, String> entry : queryStringValues)
+    {
+      if (sb.length() > 0)
+      {
+        sb.append('&');
+      }
+      try
+      {
+        sb.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
+      } catch (UnsupportedEncodingException e)
+      {
+        sb.append(entry.getKey());
+      }
+      sb.append('=');
+      try
+      {
+        sb.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
+      } catch (UnsupportedEncodingException e)
+      {
+        sb.append(entry.getValue());
+      }
+    }
+    return sb.toString();
+  }
+
+  private void buildCookieHeaders()
+  {
+    // TODO not needed yet
+  }
+
+  private String buildJson()
+  {
+    StringBuilder sb = new StringBuilder();
+    addJsonObject(sb, 0, jsonObject);
+    return sb.toString();
+  }
+
+  private void addJsonObject(StringBuilder sb, int indent,
+          List<Map.Entry<String, Object>> entries)
+  {
+    indent(sb, indent);
+    sb.append('{');
+    newline(sb);
+    Iterator<Map.Entry<String, Object>> entriesI = entries.iterator();
+    while (entriesI.hasNext())
+    {
+      Map.Entry<String, Object> entry = entriesI.next();
+      String key = entry.getKey();
+      // TODO sensibly escape " characters in key
+      Object value = entry.getValue();
+      indent(sb, indent + 1);
+      sb.append('"').append(quoteEscape(key)).append('"').append(':');
+      space(sb);
+      if (value != null && value instanceof List)
+      {
+        newline(sb);
+      }
+      addJsonValue(sb, indent + 2, value);
+      if (entriesI.hasNext())
+      {
+        sb.append(',');
+      }
+      newline(sb);
+    }
+    indent(sb, indent);
+    sb.append('}');
+  }
+
+  private void addJsonValue(StringBuilder sb, int indent, Object value)
+  {
+    if (value == null)
+    {
+      return;
+    }
+    try
+    {
+      if (value instanceof Map.Entry)
+      {
+        Map.Entry<String, Object> entry = (Map.Entry<String, Object>) value;
+        List<Map.Entry<String, Object>> object = new ArrayList<>();
+        object.add(entry);
+        addJsonObject(sb, indent, object);
+      }
+      else if (value instanceof List)
+      {
+        // list of Map.Entries or list of values?
+        List<Object> valueList = (List<Object>) value;
+        if (valueList.size() > 0 && valueList.get(0) instanceof Map.Entry)
+        {
+          // entries
+          // indent(sb, indent);
+          List<Map.Entry<String, Object>> entryList = (List<Map.Entry<String, Object>>) value;
+          addJsonObject(sb, indent, entryList);
+        }
+        else
+        {
+          // values
+          indent(sb, indent);
+          sb.append('[');
+          newline(sb);
+          Iterator<Object> valueListI = valueList.iterator();
+          while (valueListI.hasNext())
+          {
+            Object v = valueListI.next();
+            addJsonValue(sb, indent + 1, v);
+            if (valueListI.hasNext())
+            {
+              sb.append(',');
+            }
+            newline(sb);
+          }
+          indent(sb, indent);
+          sb.append("]");
+        }
+      }
+      else if (value instanceof String)
+      {
+        sb.append('"').append(quoteEscape((String) value)).append('"');
+      }
+      else if (value instanceof Integer)
+      {
+        sb.append(((Integer) value).toString());
+      }
+      else if (value instanceof Boolean)
+      {
+        sb.append('"').append(((Boolean) value).toString()).append('"');
+      }
+    } catch (ClassCastException e)
+    {
+      Console.debug(
+              "Could not deal with type of json Object " + value.toString(),
+              e);
+    }
+  }
+
+  private static String quoteEscape(String s)
+  {
+    if (s == null)
+    {
+      return null;
+    }
+    // this escapes quotation marks (") that aren't already escaped (in the
+    // string) ready to go into a quoted JSON string value
+    return s.replaceAll("((?<!\\\\)(?:\\\\{2})*)\"", "$1\\\\\"");
+  }
+
+  private static void prettyWhitespace(StringBuilder sb, String whitespace,
+          int repeat)
+  {
+    // only add whitespace if we're in DEBUG mode
+    if (!Console.getLogger().isDebugEnabled())
+    {
+      return;
+    }
+    if (repeat >= 0 && whitespace != null)
+    {
+      // sb.append(whitespace.repeat(repeat));
+      sb.append(String.join("", Collections.nCopies(repeat, whitespace)));
+
+    }
+    else
+    {
+      sb.append(whitespace);
+    }
+  }
+
+  private static void indent(StringBuilder sb, int indent)
+  {
+    prettyWhitespace(sb, "  ", indent);
+  }
+
+  private static void newline(StringBuilder sb)
+  {
+    prettyWhitespace(sb, "\n", -1);
+  }
+
+  private static void space(StringBuilder sb)
+  {
+    prettyWhitespace(sb, " ", -1);
+  }
+
+  protected static Map.Entry<String, Object> objectEntry(String s, Object o)
+  {
+    return new AbstractMap.SimpleEntry<String, Object>(s, o);
+  }
+
+  protected static Map.Entry<String, String> stringEntry(String s, String v)
+  {
+    return new AbstractMap.SimpleEntry<String, String>(s, v);
+  }
+
+  private static String getAPIBaseURL()
+  {
+    try
+    {
+      URL url = new URL(CONFIG_API_BASE_URL);
+      URLConnection urlConnection = url.openConnection();
+      HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
+      httpURLConnection.setRequestMethod("GET");
+      httpURLConnection.setRequestProperty("User-Agent", USER_AGENT);
+      httpURLConnection.setConnectTimeout(5000);
+      httpURLConnection.setReadTimeout(3000);
+      httpURLConnection.connect();
+      int responseCode = httpURLConnection.getResponseCode();
+      String responseMessage = httpURLConnection.getResponseMessage();
+
+      if (responseCode < 200 || responseCode > 299)
+      {
+        Console.warn("Config URL connection to '" + CONFIG_API_BASE_URL
+                + "' failed: '" + responseCode + " " + responseMessage
+                + "'");
+      }
+
+      BufferedReader br = new BufferedReader(
+              new InputStreamReader((httpURLConnection.getInputStream())));
+      StringBuilder sb = new StringBuilder();
+      String response;
+      while ((response = br.readLine()) != null)
+      {
+        sb.append(response);
+      }
+      if (sb.length() > 7 && sb.substring(0, 5).equals("https"))
+      {
+        return sb.toString();
+      }
+
+    } catch (MalformedURLException e)
+    {
+      Console.debug("Somehow the config URL is malformed: '"
+              + CONFIG_API_BASE_URL + "'", e);
+    } catch (IOException e)
+    {
+      Console.debug("Connection to Plausible BASE_URL '" + API_BASE_URL
+              + "' failed.", e);
+    } catch (ClassCastException e)
+    {
+      Console.debug(
+              "Couldn't cast URLConnection to HttpURLConnection in Plausible.",
+              e);
+    }
+    return DEFAULT_API_BASE_URL;
+  }
+}
index c6eb6de..1646d89 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2b1)
- * Copyright (C) 2014 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
index 6b33fea..5741908 100755 (executable)
@@ -42,7 +42,10 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Properties;
 import java.util.StringTokenizer;
 import java.util.TreeSet;
@@ -50,6 +53,7 @@ import java.util.TreeSet;
 import javax.swing.LookAndFeel;
 import javax.swing.UIManager;
 
+import jalview.analytics.Plausible;
 import jalview.datamodel.PDBEntry;
 import jalview.gui.Preferences;
 import jalview.gui.UserDefinedColours;
@@ -125,7 +129,7 @@ import jalview.ws.sifts.SiftsSettings;
  * service</li>
  * <li>QUESTIONNAIRE last questionnaire:responder id string from questionnaire
  * service</li>
- * <li>USAGESTATS (false - user prompted) Enable google analytics tracker for
+ * <li>USAGESTATS (false - user prompted) Enable analytics tracker for
  * collecting usage statistics</li>
  * <li>SHOW_OVERVIEW boolean for overview window display</li>
  * <li>ANTI_ALIAS boolean for smooth fonts</li>
@@ -310,6 +314,24 @@ public class Cache
   // in-memory only storage of proxy password, safer to use char array
   public static char[] proxyAuthPassword = null;
 
+  /**
+   * Session properties, set by command line, try not to affect stored
+   * properties!
+   */
+  private static Map<String, String> sessionProperties = new HashMap<>();
+
+  private static boolean bypassSessionProperties = false;
+
+  public static void enableSessionProperties()
+  {
+    bypassSessionProperties = false;
+  }
+
+  public static void disableSessionProperties()
+  {
+    bypassSessionProperties = true;
+  }
+
   /** Jalview Properties */
   public static Properties applicationProperties = new Properties()
   {
@@ -712,7 +734,21 @@ public class Cache
    */
   public static String getProperty(String key)
   {
-    String prop = applicationProperties.getProperty(key);
+    return getProperty(key, false);
+  }
+
+  public static String getProperty(String key,
+          boolean skipSessionProperties)
+  {
+    String prop = null;
+    if (!(skipSessionProperties || bypassSessionProperties))
+    {
+      prop = getSessionProperty(key);
+    }
+    if (prop == null)
+    {
+      prop = applicationProperties.getProperty(key);
+    }
     if (prop == null && Platform.isJS())
     {
       prop = applicationProperties.getProperty(Platform.getUniqueAppletID()
@@ -782,8 +818,16 @@ public class Cache
     try
     {
       oldValue = applicationProperties.setProperty(key, obj);
-      if (propertiesFile != null && !propsAreReadOnly)
+      if (propertiesFile != null && !propsAreReadOnly
+      // don't rewrite if new value is same as old value
+              && !((obj == null && oldValue == null)
+                      || (obj != null && obj.equals(oldValue))))
       {
+        // reset the session property too
+        if (sessionProperties.containsKey(key))
+        {
+          sessionProperties.remove(key);
+        }
         FileOutputStream out = new FileOutputStream(propertiesFile);
         applicationProperties.store(out, "---JalviewX Properties File---");
         out.close();
@@ -911,102 +955,42 @@ public class Cache
   }
 
   /**
-   * GA tracker object - actually JGoogleAnalyticsTracker null if tracking not
-   * enabled.
-   */
-  protected static Object tracker = null;
-
-  protected static Class trackerfocus = null;
-
-  protected static Class jgoogleanalyticstracker = null;
-
-  /**
-   * Initialise the google tracker if it is not done already.
+   * Initialise the tracker if it is not done already.
    */
-  public static void initGoogleTracker()
+  public static void initAnalytics()
   {
-    if (tracker == null)
+    Plausible.setEnabled(true);
+
+    String appName = ChannelProperties.getProperty("app_name") + " Desktop";
+    String version = Cache.getProperty("VERSION") + "_"
+            + Cache.getDefault("BUILD_DATE", "unknown");
+    String path;
+    /* we don't want to encode ':' as "%3A" for backward compatibility with the UA setup
+    try
     {
-      if (jgoogleanalyticstracker == null)
-      {
-        // try to get the tracker class
-        try
-        {
-          jgoogleanalyticstracker = Cache.class.getClassLoader().loadClass(
-                  "com.boxysystems.jgoogleanalytics.JGoogleAnalyticsTracker");
-          trackerfocus = Cache.class.getClassLoader()
-                  .loadClass("com.boxysystems.jgoogleanalytics.FocusPoint");
-        } catch (Exception e)
-        {
-          Console.debug(
-                  "com.boxysystems.jgoogleanalytics package is not present - tracking not enabled.");
-          tracker = null;
-          jgoogleanalyticstracker = null;
-          trackerfocus = null;
-          return;
-        }
-      }
-      // now initialise tracker
-      Exception re = null, ex = null;
-      Error err = null;
-      String vrs = "No Version Accessible";
-      try
-      {
-        // Google analytics tracking code for Library Finder
-        tracker = jgoogleanalyticstracker
-                .getConstructor(new Class[]
-                { String.class, String.class, String.class })
-                .newInstance(new Object[]
-                { ChannelProperties.getProperty("app_name") + " Desktop",
-                    (vrs = Cache.getProperty("VERSION") + "_"
-                            + Cache.getDefault("BUILD_DATE", "unknown")),
-                    "UA-9060947-1" });
-        jgoogleanalyticstracker
-                .getMethod("trackAsynchronously", new Class[]
-                { trackerfocus })
-                .invoke(tracker, new Object[]
-                { trackerfocus.getConstructor(new Class[] { String.class })
-                        .newInstance(new Object[]
-                        { "Application Started." }) });
-      } catch (RuntimeException e)
-      {
-        re = e;
-      } catch (Exception e)
-      {
-        ex = e;
-      } catch (Error e)
-      {
-        err = e;
-      }
-      if (re != null || ex != null || err != null)
-      {
-        if (re != null)
-        {
-          Console.debug("Caught runtime exception in googletracker init:",
-                  re);
-        }
-        if (ex != null)
-        {
-          Console.warn(
-                  "Failed to initialise GoogleTracker for Jalview Desktop with version "
-                          + vrs,
-                  ex);
-        }
-        if (err != null)
-        {
-          Console.error(
-                  "Whilst initing GoogleTracker for Jalview Desktop version "
-                          + vrs,
-                  err);
-        }
-      }
-      else
-      {
-        Console.debug("Successfully initialised tracker.");
-      }
+      path = "/" + String.join("/", URLEncoder.encode(appName, "UTF-8"),
+              URLEncoder.encode(version, "UTF-8"),
+              URLEncoder.encode(APPLICATION_STARTED, "UTF-8"));
+    } catch (UnsupportedEncodingException e)
+    {
+    */
+    List<String> pathParts = new ArrayList<>();
+    pathParts.add(appName);
+    pathParts.add(version);
+    pathParts.add(APPLICATION_STARTED);
+    path = ("/" + String.join("/", pathParts)).replace(' ', '+');
+    /*
     }
+    */
+    Plausible plausible = Plausible.getInstance();
+
+    // This will send a new "application_launch" event with parameters
+    // including the old-style "path", the channel name and version
+    plausible.sendEvent("application_launch", path, true);
   }
 
+  private static final String APPLICATION_STARTED = "Application Started";
+
   /**
    * get the user's default colour if available
    * 
@@ -1436,10 +1420,11 @@ public class Cache
                 if (customProxySet &&
                 // we have a username but no password for the scheme being
                 // requested
-                (protocol.equalsIgnoreCase("http")
-                        && (httpUser != null && httpUser.length() > 0
-                                && (httpPassword == null
-                                        || httpPassword.length == 0)))
+                        (protocol.equalsIgnoreCase("http")
+                                && (httpUser != null
+                                        && httpUser.length() > 0
+                                        && (httpPassword == null
+                                                || httpPassword.length == 0)))
                         || (protocol.equalsIgnoreCase("https")
                                 && (httpsUser != null
                                         && httpsUser.length() > 0
@@ -1694,4 +1679,17 @@ public class Cache
     }
     return bootstrapProps;
   }
+
+  public static void setSessionProperty(String key, String val)
+  {
+    if (key != null)
+    {
+      sessionProperties.put(key, val);
+    }
+  }
+
+  public static String getSessionProperty(String key)
+  {
+    return key == null ? null : sessionProperties.get(key);
+  }
 }
index 30fdc30..dbc4953 100644 (file)
@@ -44,6 +44,9 @@ import jalview.io.FileLoader;
 import jalview.io.HtmlSvgOutput;
 import jalview.io.IdentifyFile;
 import jalview.io.NewickFile;
+import jalview.io.exceptions.ImageOutputException;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemeProperty;
 import jalview.structure.StructureImportSettings.TFType;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.FileUtils;
@@ -100,6 +103,20 @@ public class Commands
         theseArgsWereParsed &= processLinked(id);
         processGroovyScript(id);
         boolean processLinkedOkay = theseArgsWereParsed;
+
+        // wait around until alignFrame isn't busy
+        AlignFrame af = afMap.get(id);
+        while (af != null && af.getViewport().isCalcInProgress())
+        {
+          try
+          {
+            Thread.sleep(25);
+          } catch (Exception q)
+          {
+          }
+          ;
+        }
+
         theseArgsWereParsed &= processImages(id);
         if (processLinkedOkay)
           theseArgsWereParsed &= processOutput(id);
@@ -107,7 +124,7 @@ public class Commands
         // close ap
         if (avm.getBoolean(Arg.CLOSE))
         {
-          AlignFrame af = afMap.get(id);
+          af = afMap.get(id);
           if (af != null)
           {
             af.closeMenuItem_actionPerformed(true);
@@ -249,7 +266,19 @@ public class Commands
                   Arg.COLOUR, sv, null, "DEFAULT_COLOUR_PROT", "");
           if ("" != colour)
           {
-            af.changeColour_actionPerformed(colour);
+            ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
+                    af.getViewport(), af.getViewport().getAlignment(),
+                    colour);
+
+            if (cs == null && !"None".equals(colour))
+            {
+              Console.warn(
+                      "Couldn't parse '" + colour + "' as a colourscheme.");
+            }
+            else
+            {
+              af.changeColour(cs);
+            }
             Jalview.testoutput(argParser, Arg.COLOUR, "zappo", colour);
           }
 
@@ -548,11 +577,36 @@ public class Commands
                           structureFilepath, tft, paeFilepath, false,
                           ssFromStructure, false, viewerType);
 
-          if (headless)
+          if (sv == null)
           {
-            sv.setAsync(false);
+            Console.error("Failed to import and open structure view.");
+            continue;
           }
-
+          try
+          {
+            long tries = 1000;
+            while (sv.isBusy() && tries > 0)
+            {
+              Thread.sleep(25);
+              if (sv.isBusy())
+              {
+                tries--;
+                Console.debug(
+                        "Waiting for viewer for " + structureFilepath);
+              }
+            }
+            if (tries == 0 && sv.isBusy())
+            {
+              Console.warn(
+                      "Gave up waiting for structure viewer to load. Something may have gone wrong.");
+            }
+          } catch (Exception x)
+          {
+            Console.warn("Exception whilst waiting for structure viewer "
+                    + structureFilepath, x);
+          }
+          Console.debug(
+                  "Successfully opened viewer for " + structureFilepath);
           String structureImageFilename = ArgParser.getValueFromSubValOrArg(
                   avm, av, Arg.STRUCTUREIMAGE, subVals);
           if (sv != null && structureImageFilename != null)
@@ -592,12 +646,13 @@ public class Commands
             }
             BitmapImageSizing userBis = ImageMaker
                     .parseScaleWidthHeightStrings(scale, width, height);
+            // TODO MAKE THIS VIEWER INDEPENDENT!!
             switch (StructureViewer.getViewerType())
             {
             case JMOL:
               try
               {
-                Thread.sleep(1000);
+                Thread.sleep(1000); // WHY ???
               } catch (InterruptedException e)
               {
                 // TODO Auto-generated catch block
@@ -608,8 +663,20 @@ public class Commands
               if (sview instanceof AppJmol)
               {
                 AppJmol jmol = (AppJmol) sview;
-                jmol.makePDBImage(structureImageFile, imageType, renderer,
-                        userBis);
+                try
+                {
+                  Console.debug("Rendering image to " + structureImageFile);
+                  jmol.makePDBImage(structureImageFile, imageType, renderer,
+                          userBis);
+                  Console.debug("Finished Rendering image to "
+                          + structureImageFile);
+
+                } catch (ImageOutputException ioexc)
+                {
+                  Console.warn("Unexpected error whilst exporting image to "
+                          + structureImageFile, ioexc);
+                }
+
               }
               break;
             default:
@@ -717,54 +784,60 @@ public class Commands
         Cache.setProperty("EXPORT_EMBBED_BIOJSON", "false");
 
         Console.info("Writing " + file);
-
-        switch (type)
+        try
         {
-
-        case "svg":
-          Console.debug("Outputting type '" + type + "' to " + fileName);
-          af.createSVG(file, renderer);
-          break;
-
-        case "png":
-          Console.debug("Outputting type '" + type + "' to " + fileName);
-          af.createPNG(file, null, userBis);
-          break;
-
-        case "html":
-          Console.debug("Outputting type '" + type + "' to " + fileName);
-          HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
-          htmlSVG.exportHTML(fileName, renderer);
-          break;
-
-        case "biojs":
-          try
+          switch (type)
           {
-            BioJsHTMLOutput.refreshVersionInfo(
-                    BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
-          } catch (URISyntaxException e)
-          {
-            e.printStackTrace();
+
+          case "svg":
+            Console.debug("Outputting type '" + type + "' to " + fileName);
+            af.createSVG(file, renderer);
+            break;
+
+          case "png":
+            Console.debug("Outputting type '" + type + "' to " + fileName);
+            af.createPNG(file, null, userBis);
+            break;
+
+          case "html":
+            Console.debug("Outputting type '" + type + "' to " + fileName);
+            HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
+            htmlSVG.exportHTML(fileName, renderer);
+            break;
+
+          case "biojs":
+            Console.debug(
+                    "Outputting BioJS MSA Viwer HTML file: " + fileName);
+            try
+            {
+              BioJsHTMLOutput.refreshVersionInfo(
+                      BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
+            } catch (URISyntaxException e)
+            {
+              e.printStackTrace();
+            }
+            BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
+            bjs.exportHTML(fileName);
+            break;
+
+          case "eps":
+            Console.debug("Outputting EPS file: " + fileName);
+            af.createEPS(file, renderer);
+            break;
+
+          case "imagemap":
+            Console.debug("Outputting ImageMap file: " + fileName);
+            af.createImageMap(file, name);
+            break;
+
+          default:
+            Console.warn(Arg.IMAGE.argString() + " type '" + type
+                    + "' not known. Ignoring");
+            break;
           }
-          BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
-          bjs.exportHTML(fileName);
-          Console.debug("Creating BioJS MSA Viwer HTML file: " + fileName);
-          break;
-
-        case "eps":
-          af.createEPS(file, name);
-          Console.debug("Creating EPS file: " + fileName);
-          break;
-
-        case "imagemap":
-          af.createImageMap(file, name);
-          Console.debug("Creating ImageMap file: " + fileName);
-          break;
-
-        default:
-          Console.warn(Arg.IMAGE.argString() + " type '" + type
-                  + "' not known. Ignoring");
-          break;
+        } catch (Exception ioex)
+        {
+          Console.warn("Unexpected error during export", ioex);
         }
       }
     }
index 4b18484..30fd530 100644 (file)
@@ -220,6 +220,11 @@ public class Console
     return JLogger.toLevel(level);
   }
 
+  public static JLogger getLogger()
+  {
+    return log;
+  }
+
   public static boolean initLogger()
   {
     return initLogger(null);
@@ -236,9 +241,13 @@ public class Console
       JLogger.LogLevel logLevel = JLogger.LogLevel.INFO;
 
       if (JLogger.isLevel(providedLogLevel))
+      {
         logLevel = Console.getLogLevel(providedLogLevel);
+      }
       else
+      {
         logLevel = getCachedLogLevel();
+      }
 
       if (!Platform.isJS())
       {
index dc97549..57f2575 100755 (executable)
@@ -91,6 +91,7 @@ import jalview.io.FileLoader;
 import jalview.io.HtmlSvgOutput;
 import jalview.io.IdentifyFile;
 import jalview.io.NewickFile;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -343,6 +344,25 @@ public class Jalview
       }
     }
 
+    // set individual session preferences
+    if (bootstrapArgs.contains(Arg.P))
+    {
+      for (String kev : bootstrapArgs.getValueList(Arg.P))
+      {
+        if (kev == null)
+        {
+          continue;
+        }
+        int equalsIndex = kev.indexOf(ArgParser.EQUALS);
+        if (equalsIndex > -1)
+        {
+          String key = kev.substring(0, equalsIndex);
+          String val = kev.substring(equalsIndex + 1);
+          Cache.setSessionProperty(key, val);
+        }
+      }
+    }
+
     // Move any new getdown-launcher-new.jar into place over old
     // getdown-launcher.jar
     String appdirString = System.getProperty("getdownappdir");
@@ -613,8 +633,7 @@ public class Jalview
     {
       headless = true;
     }
-    System.setProperty("http.agent",
-            "Jalview Desktop/" + Cache.getDefault("VERSION", "Unknown"));
+    System.setProperty("http.agent", HttpUtils.getUserAgent());
 
     try
     {
@@ -720,18 +739,18 @@ public class Jalview
           testoutput(argparser, Arg.WEBSERVICEDISCOVERY);
         }
 
-        boolean usagestats = bootstrapArgs.getBoolean(Arg.USAGESTATS);
+        boolean usagestats = !bootstrapArgs.getBoolean(Arg.NOUSAGESTATS);
         if (aparser.contains("nousagestats"))
           usagestats = false;
         if (usagestats)
         {
           startUsageStats(desktop);
-          testoutput(argparser, Arg.USAGESTATS);
+          testoutput(argparser, Arg.NOUSAGESTATS);
         }
         else
         {
           System.out.println("CMD [-nousagestats] executed successfully!");
-          testoutput(argparser, Arg.USAGESTATS);
+          testoutput(argparser, Arg.NOUSAGESTATS);
         }
 
         boolean questionnaire = bootstrapArgs.getBoolean(Arg.QUESTIONNAIRE);
@@ -790,6 +809,7 @@ public class Jalview
     // Run Commands from cli
     cmds = new Commands(argparser, headlessArg);
     boolean commandsSuccess = cmds.argsWereParsed();
+
     if (commandsSuccess)
     {
       if (headlessArg)
@@ -986,9 +1006,7 @@ public class Jalview
             ex.printStackTrace(System.err);
           }
         }
-        // TODO - load PDB structure(s) to alignment JAL-629
-        // (associate with identical sequence in alignment, or a specified
-        // sequence)
+
         if (groovyscript != null)
         {
           // Execute the groovy script after we've done all the rendering stuff
@@ -1002,100 +1020,109 @@ public class Jalview
         String imageName = "unnamed.png";
         while (aparser.getSize() > 1)
         {
-          String outputFormat = aparser.nextValue();
-          file = aparser.nextValue();
-
-          if (outputFormat.equalsIgnoreCase("png"))
-          {
-            af.createPNG(new File(file));
-            imageName = (new File(file)).getName();
-            System.out.println("Creating PNG image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("svg"))
-          {
-            File imageFile = new File(file);
-            imageName = imageFile.getName();
-            af.createSVG(imageFile);
-            System.out.println("Creating SVG image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("html"))
+          try
           {
-            File imageFile = new File(file);
-            imageName = imageFile.getName();
-            HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
-            htmlSVG.exportHTML(file);
+            String outputFormat = aparser.nextValue();
+            file = aparser.nextValue();
 
-            System.out.println("Creating HTML image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("biojsmsa"))
-          {
-            if (file == null)
+            if (outputFormat.equalsIgnoreCase("png"))
             {
-              System.err.println("The output html file must not be null");
-              return;
+              System.out.println("Creating PNG image: " + file);
+              af.createPNG(new File(file));
+              imageName = (new File(file)).getName();
+              continue;
             }
-            try
+            else if (outputFormat.equalsIgnoreCase("svg"))
             {
-              BioJsHTMLOutput.refreshVersionInfo(
-                      BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
-            } catch (URISyntaxException e)
+              System.out.println("Creating SVG image: " + file);
+              File imageFile = new File(file);
+              imageName = imageFile.getName();
+              af.createSVG(imageFile);
+              continue;
+            }
+            else if (outputFormat.equalsIgnoreCase("html"))
             {
-              e.printStackTrace();
+              File imageFile = new File(file);
+              imageName = imageFile.getName();
+              HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
+
+              System.out.println("Creating HTML image: " + file);
+              htmlSVG.exportHTML(file);
+              continue;
             }
-            BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
-            bjs.exportHTML(file);
-            System.out
-                    .println("Creating BioJS MSA Viwer HTML file: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("imgMap"))
-          {
-            af.createImageMap(new File(file), imageName);
-            System.out.println("Creating image map: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("eps"))
-          {
-            File outputFile = new File(file);
-            System.out.println(
-                    "Creating EPS file: " + outputFile.getAbsolutePath());
-            af.createEPS(outputFile);
-            continue;
-          }
-          FileFormatI outFormat = null;
-          try
-          {
-            outFormat = FileFormats.getInstance().forName(outputFormat);
-          } catch (Exception formatP)
-          {
-            System.out.println("Couldn't parse " + outFormat
-                    + " as a valid Jalview format string.");
-          }
-          if (outFormat != null)
-          {
-            if (!outFormat.isWritable())
+            else if (outputFormat.equalsIgnoreCase("biojsmsa"))
             {
+              if (file == null)
+              {
+                System.err.println("The output html file must not be null");
+                return;
+              }
+              try
+              {
+                BioJsHTMLOutput.refreshVersionInfo(
+                        BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
+              } catch (URISyntaxException e)
+              {
+                e.printStackTrace();
+              }
+              BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
               System.out.println(
-                      "This version of Jalview does not support alignment export as "
-                              + outputFormat);
+                      "Creating BioJS MSA Viwer HTML file: " + file);
+              bjs.exportHTML(file);
+              continue;
+            }
+            else if (outputFormat.equalsIgnoreCase("imgMap"))
+            {
+              System.out.println("Creating image map: " + file);
+              af.createImageMap(new File(file), imageName);
+              continue;
             }
-            else
+            else if (outputFormat.equalsIgnoreCase("eps"))
             {
-              af.saveAlignment(file, outFormat);
-              if (af.isSaveAlignmentSuccessful())
+              File outputFile = new File(file);
+              System.out.println(
+                      "Creating EPS file: " + outputFile.getAbsolutePath());
+              af.createEPS(outputFile);
+              continue;
+            }
+
+            FileFormatI outFormat = null;
+            try
+            {
+              outFormat = FileFormats.getInstance().forName(outputFormat);
+            } catch (Exception formatP)
+            {
+              System.out.println("Couldn't parse " + outFormat
+                      + " as a valid Jalview format string.");
+            }
+            if (outFormat != null)
+            {
+              if (!outFormat.isWritable())
               {
-                System.out.println("Written alignment in "
-                        + outFormat.getName() + " format to " + file);
+                System.out.println(
+                        "This version of Jalview does not support alignment export as "
+                                + outputFormat);
               }
               else
               {
-                System.out.println("Error writing file " + file + " in "
-                        + outFormat.getName() + " format!!");
+                af.saveAlignment(file, outFormat);
+                if (af.isSaveAlignmentSuccessful())
+                {
+                  System.out.println("Written alignment in "
+                          + outFormat.getName() + " format to " + file);
+                }
+                else
+                {
+                  System.out.println("Error writing file " + file + " in "
+                          + outFormat.getName() + " format!!");
+                }
               }
             }
+          } catch (ImageOutputException ioexc)
+          {
+            System.out.println(
+                    "Unexpected error whilst exporting image to " + file);
+            ioexc.printStackTrace();
           }
 
         }
@@ -1114,7 +1141,8 @@ public class Jalview
 
     if (!Platform.isJS() && !headless && file == null
             && Cache.getDefault("SHOW_STARTUP_FILE", true)
-            && !cmds.commandArgsProvided())
+            && !cmds.commandArgsProvided()
+            && !bootstrapArgs.getBoolean(Arg.NOSTARTUPFILE))
     // don't open the startup file if command line args have been processed
     // (&& !Commands.commandArgsProvided())
     /**
@@ -1192,93 +1220,102 @@ public class Jalview
 
   private static void setLookAndFeel()
   {
-    // property laf = "crossplatform", "system", "gtk", "metal", "nimbus",
-    // "mac" or "flat"
-    // If not set (or chosen laf fails), use the normal SystemLaF and if on Mac,
-    // try Quaqua/Vaqua.
-    String lafProp = System.getProperty("laf");
-    String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
-    String laf = "none";
-    if (lafProp != null)
-    {
-      laf = lafProp;
-    }
-    else if (lafSetting != null)
-    {
-      laf = lafSetting;
-    }
-    boolean lafSet = false;
-    switch (laf)
+    if (!Platform.isJS())
+    /**
+     * Java only
+     * 
+     * @j2sIgnore
+     */
     {
-    case "crossplatform":
-      lafSet = setCrossPlatformLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
+      // property laf = "crossplatform", "system", "gtk", "metal", "nimbus",
+      // "mac" or "flat"
+      // If not set (or chosen laf fails), use the normal SystemLaF and if on
+      // Mac,
+      // try Quaqua/Vaqua.
+      String lafProp = System.getProperty("laf");
+      String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
+      String laf = "none";
+      if (lafProp != null)
+      {
+        laf = lafProp;
       }
-      break;
-    case "system":
-      lafSet = setSystemLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "gtk":
-      lafSet = setGtkLookAndFeel();
-      if (!lafSet)
+      else if (lafSetting != null)
       {
-        Console.error("Could not set requested laf=" + laf);
+        laf = lafSetting;
       }
-      break;
-    case "metal":
-      lafSet = setMetalLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "nimbus":
-      lafSet = setNimbusLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "flat":
-      lafSet = setFlatLookAndFeel();
-      if (!lafSet)
+      boolean lafSet = false;
+      switch (laf)
       {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "mac":
-      lafSet = setMacLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
+      case "crossplatform":
+        lafSet = setCrossPlatformLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "system":
+        lafSet = setSystemLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "gtk":
+        lafSet = setGtkLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "metal":
+        lafSet = setMetalLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "nimbus":
+        lafSet = setNimbusLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "flat":
+        lafSet = setFlatLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "mac":
+        lafSet = setMacLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "none":
+        break;
+      default:
+        Console.error("Requested laf=" + laf + " not implemented");
       }
-      break;
-    case "none":
-      break;
-    default:
-      Console.error("Requested laf=" + laf + " not implemented");
-    }
-    if (!lafSet)
-    {
-      // Flatlaf default for everyone!
-      lafSet = setFlatLookAndFeel();
       if (!lafSet)
       {
-        setSystemLookAndFeel();
-      }
-      if (Platform.isLinux())
-      {
-        setLinuxLookAndFeel();
-      }
-      if (Platform.isMac())
-      {
-        setMacLookAndFeel();
+        // Flatlaf default for everyone!
+        lafSet = setFlatLookAndFeel();
+        if (!lafSet)
+        {
+          setSystemLookAndFeel();
+        }
+        if (Platform.isLinux())
+        {
+          setLinuxLookAndFeel();
+        }
+        if (Platform.isMac())
+        {
+          setMacLookAndFeel();
+        }
       }
     }
   }
@@ -1536,7 +1573,7 @@ public class Jalview
                     + "-questionnaire URL\tQueries the given URL for information about any Jalview user questionnaires.\n"
                     + "-noquestionnaire\tTurn off questionnaire check.\n"
                     + "-nonews\tTurn off check for Jalview news.\n"
-                    + "-nousagestats\tTurn off google analytics tracking for this session.\n"
+                    + "-nousagestats\tTurn off analytics tracking for this session.\n"
                     + "-sortbytree OR -nosortbytree\tEnable or disable sorting of the given alignment by the given tree\n"
                     // +
                     // "-setprop PROPERTY=VALUE\tSet the given Jalview property,
@@ -1558,18 +1595,16 @@ public class Jalview
      * start a User Config prompt asking if we can log usage statistics.
      */
     PromptUserConfig prompter = new PromptUserConfig(Desktop.desktop,
-            "USAGESTATS", "Jalview Usage Statistics",
-            "Do you want to help make Jalview better by enabling "
-                    + "the collection of usage statistics with Google Analytics ?"
-                    + "\n\n(you can enable or disable usage tracking in the preferences)",
+            "USAGESTATS",
+            MessageManager.getString("prompt.plausible_analytics_title"),
+            MessageManager.getString("prompt.plausible_analytics"),
             new Runnable()
             {
               @Override
               public void run()
               {
-                Console.debug(
-                        "Initialising googletracker for usage stats.");
-                Cache.initGoogleTracker();
+                Console.debug("Initialising analytics for usage stats.");
+                Cache.initAnalytics();
                 Console.debug("Tracking enabled.");
               }
             }, new Runnable()
@@ -1577,7 +1612,7 @@ public class Jalview
               @Override
               public void run()
               {
-                Console.debug("Not enabling Google Tracking.");
+                Console.debug("Not enabling analytics.");
               }
             }, null, true);
     desktop.addDialogThread(prompter);
@@ -1780,8 +1815,14 @@ public class Jalview
     }
   }
 
-  /*
-   * testoutput for string values
+  /******************************
+   * 
+   * TEST OUTPUT METHODS
+   * 
+   ******************************/
+  /**
+   * method for reporting string values parsed/processed during tests
+   * 
    */
   protected static void testoutput(ArgParser ap, Arg a, String s1,
           String s2)
@@ -1805,6 +1846,10 @@ public class Jalview
     testoutput(true, a, s1, s2);
   }
 
+  /**
+   * method for reporting string values parsed/processed during tests
+   */
+
   protected static void testoutput(BootstrapArgs bsa, Arg a, String s1,
           String s2)
   {
@@ -1829,6 +1874,9 @@ public class Jalview
     testoutput(true, a, s1, s2);
   }
 
+  /**
+   * report value set for string values parsed/processed during tests
+   */
   private static void testoutput(boolean yes, Arg a, String s1, String s2)
   {
     if (yes && ((s1 == null && s2 == null)
@@ -1840,7 +1888,7 @@ public class Jalview
   }
 
   /*
-   * testoutput for boolean values
+   * testoutput for boolean and unary values
    */
   protected static void testoutput(ArgParser ap, Arg a)
   {
@@ -1883,7 +1931,15 @@ public class Jalview
 
   private static void testoutput(boolean yes, Arg a)
   {
-    System.out.println("[TESTOUTPUT] arg "
-            + (yes ? a.argString() : a.negateArgString()) + " was set");
+    String message = null;
+    if (a.hasOption(Opt.BOOLEAN))
+    {
+      message = (yes ? a.argString() : a.negateArgString()) + " was set";
+    }
+    else if (a.hasOption(Opt.UNARY))
+    {
+      message = a.argString() + (yes ? " was set" : " was not set");
+    }
+    System.out.println("[TESTOUTPUT] arg " + message);
   }
 }
index 0d6e3b1..2f25978 100644 (file)
@@ -44,9 +44,10 @@ public enum Arg
   QUESTIONNAIRE(Type.CONFIG,
           "Show (or don't show) the questionnaire if one is available.",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
-  USAGESTATS(Type.CONFIG,
-          "Send (or don't send) initial launch usage stats.", true,
-          Opt.BOOLEAN, Opt.BOOTSTRAP),
+  NOUSAGESTATS(Type.CONFIG, "Don't send initial launch usage stats.",
+          Opt.UNARY, Opt.BOOTSTRAP),
+  NOSTARTUPFILE(Type.CONFIG, "Don't show the default startup file.",
+          Opt.UNARY, Opt.BOOTSTRAP),
   WEBSERVICEDISCOVERY(Type.CONFIG,
           "Attempt (or don't attempt) to connect to JABAWS web services.",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
@@ -65,6 +66,9 @@ public enum Arg
   INITSUBSTITUTIONS(Type.CONFIG,
           "Set ‑‑substitutions to be initially enabled (or initially disabled).",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP, Opt.NOACTION, Opt.SECRET),
+  P(Type.CONFIG, "Set a Jalview preference value for this session.",
+          Opt.PREFIXKEV, Opt.PRESERVECASE, Opt.STRING, Opt.BOOTSTRAP,
+          Opt.MULTI, Opt.NOACTION, Opt.SECRET), // keep this secret for now.
 
   // Opening an alignment
   OPEN(Type.OPENING,
@@ -80,7 +84,7 @@ public enum Arg
           "Specifies the title for the open alignment window as string.",
           Opt.STRING, Opt.LINKED),
   COLOUR(Type.OPENING, "color", // being a bit soft on the Americans!
-          "Applies the colour scheme to the open alignment window. Valid values are:\n"
+          "Applies the colour scheme to the open alignment window. Valid values include:\n"
                   + "clustal,\n" + "blosum62,\n" + "pc-identity,\n"
                   + "zappo,\n" + "taylor,\n" + "gecos-flower,\n"
                   + "gecos-blossom,\n" + "gecos-sunset,\n"
@@ -89,7 +93,11 @@ public enum Arg
                   + "turn-propensity,\n" + "buried-index,\n"
                   + "nucleotide,\n" + "nucleotide-ambiguity,\n"
                   + "purine-pyrimidine,\n" + "rna-helices,\n"
-                  + "t-coffee-scores,\n" + "sequence-id.",
+                  + "t-coffee-scores,\n" + "sequence-id.\n"
+                  +"\n"
+                  + "Names of user defined colourschemes will also work,\n"
+                 +"and jalview colourscheme specifications like\n"
+                  +"--colour=\"D,E=red; K,R,H=0022FF; C,c=yellow\"",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   FEATURES(Type.OPENING, "Add a feature file or URL to the open alignment.",
           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
@@ -399,6 +407,14 @@ public enum Arg
      * A LAST arg gets moved to appear last in the usage statement (within type)
      */
     LAST(null),
+    /*
+     * After other args are checked, the following args can prefix a KEY=VALUE argument
+     */
+    PREFIXKEV("prefixes key=value"),
+    /*
+     * do not lowercase the name when getting the arg name or arg string
+     */
+    PRESERVECASE(null),
     //
     ;
 
@@ -486,15 +502,15 @@ public enum Arg
   private Arg(Type type, String alternativeName, String description,
           boolean defaultBoolean, Opt... options)
   {
+    this.type = type;
+    this.description = description;
+    this.defaultBoolValue = defaultBoolean;
+    this.setOptions(options);
     this.argNames = alternativeName != null
             ? new String[]
             { this.getName(), alternativeName }
             : new String[]
             { this.getName() };
-    this.type = type;
-    this.description = description;
-    this.defaultBoolValue = defaultBoolean;
-    this.setOptions(options);
   }
 
   public String argString()
@@ -544,7 +560,9 @@ public enum Arg
 
   public String getName()
   {
-    return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
+    String name = hasOption(Opt.PRESERVECASE) ? this.name()
+            : this.name().toLowerCase(Locale.ROOT);
+    return name.replace('_', '-');
   }
 
   @Override
@@ -794,7 +812,16 @@ public enum Arg
     argSb.append(
             a.hasOption(Opt.BOOLEAN) ? booleanArgString(a) : a.argString());
     if (a.hasOption(Opt.STRING))
-      argSb.append("=value");
+    {
+      if (a.hasOption(Opt.PREFIXKEV))
+      {
+        argSb.append("key=value");
+      }
+      else
+      {
+        argSb.append("=value");
+      }
+    }
     return argSb.toString();
   }
 
index f08e678..907b1fa 100644 (file)
@@ -48,81 +48,115 @@ public class ArgParser
 
   protected static final String DOUBLEDASH = "--";
 
-  protected static final char EQUALS = '=';
+  public static final char EQUALS = '=';
 
   protected static final String NEGATESTRING = "no";
 
-  // the default linked id prefix used for no id (not even square braces)
+  /**
+   * the default linked id prefix used for no id (ie when not even square braces
+   * are provided)
+   */
   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
 
-  // the linkedId string used to match all linkedIds seen so far
+  /**
+   * the linkedId string used to match all linkedIds seen so far
+   */
   protected static final String MATCHALLLINKEDIDS = "*";
 
-  // the linkedId string used to match all of the last --open'ed linkedIds
+  /**
+   * the linkedId string used to match all of the last --open'ed linkedIds
+   */
   protected static final String MATCHOPENEDLINKEDIDS = "open*";
 
-  // the counter added to the default linked id prefix
+  /**
+   * the counter added to the default linked id prefix
+   */
   private int defaultLinkedIdCounter = 0;
 
-  // the substitution string used to use the defaultLinkedIdCounter
+  /**
+   * the substitution string used to use the defaultLinkedIdCounter
+   */
   private static final String DEFAULTLINKEDIDCOUNTER = "{}";
 
-  // the counter added to the default linked id prefix. NOW using
-  // linkedIdAutoCounter
-  // private int openLinkedIdCounter = 0;
-
-  // the linked id prefix used for --open files. NOW the same as DEFAULT
+  /**
+   * the linked id prefix used for --open files. NOW the same as DEFAULT
+   */
   protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
 
-  // the counter used for {n} substitutions
+  /**
+   * the counter used for {n} substitutions
+   */
   private int linkedIdAutoCounter = 0;
 
-  // the linked id substitution string used to increment the idCounter (and use
-  // the incremented value)
+  /**
+   * the linked id substitution string used to increment the idCounter (and use
+   * the incremented value)
+   */
   private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
 
-  // the linked id substitution string used to use the idCounter
+  /**
+   * the linked id substitution string used to use the idCounter
+   */
   private static final String LINKEDIDAUTOCOUNTER = "{n}";
 
-  // the linked id substitution string used to use the filename extension of
-  // --append
-  // or --open
+  /**
+   * the linked id substitution string used to use the filename extension of
+   * --append or --open
+   */
   private static final String LINKEDIDEXTENSION = "{extension}";
 
-  // the linked id substitution string used to use the base filename of --append
-  // or --open
+  /**
+   * the linked id substitution string used to use the base filename of --append
+   */
+  /** or --open */
   private static final String LINKEDIDBASENAME = "{basename}";
 
-  // the linked id substitution string used to use the dir path of --append
-  // or --open
+  /**
+   * the linked id substitution string used to use the dir path of --append or
+   * --open
+   */
   private static final String LINKEDIDDIRNAME = "{dirname}";
 
-  // the current argfile
+  /**
+   * the current argfile
+   */
   private String argFile = null;
 
-  // the linked id substitution string used to use the dir path of the latest
-  // --argfile name
+  /**
+   * the linked id substitution string used to use the dir path of the latest
+   */
+  /** --argfile name */
   private static final String ARGFILEBASENAME = "{argfilebasename}";
 
-  // the linked id substitution string used to use the dir path of the latest
-  // --argfile name
+  /**
+   * the linked id substitution string used to use the dir path of the latest
+   * --argfile name
+   */
   private static final String ARGFILEDIRNAME = "{argfiledirname}";
 
-  // flag to say whether {n} subtitutions in output filenames should be made.
-  // Turn on and off with --substitutions and --nosubstitutions
-  // Start with it on
+  /**
+   * flag to say whether {n} subtitutions in output filenames should be made.
+   * Turn on and off with --substitutions and --nosubstitutions Start with it on
+   */
   private boolean substitutions = true;
 
-  // flag to say whether the default linkedId is the current default linked id
-  // or ALL linkedIds
+  /**
+   * flag to say whether the default linkedId is the current default linked id
+   *
+   * or ALL linkedIds
+   */
   private boolean allLinkedIds = false;
 
-  // flag to say whether the default linkedId is the current default linked id
-  // or OPENED linkedIds
+  /**
+   * flag to say whether the default linkedId is the current default linked id
+   * or OPENED linkedIds
+   */
   private boolean openedLinkedIds = false;
 
-  // flag to say whether the structure arguments should be applied to all
-  // structures with this linked id
+  /**
+   * flag to say whether the structure arguments should be applied to all
+   * structures with this linked id
+   */
   private boolean allStructures = false;
 
   protected static final Map<String, Arg> argMap;
@@ -302,12 +336,34 @@ public class ArgParser
         Arg a = argMap.get(argName);
         // check for boolean prepended by "no" e.g. --nowrap
         boolean negated = false;
-        if (a == null && argName.startsWith(NEGATESTRING) && argMap
-                .containsKey(argName.substring(NEGATESTRING.length())))
+        if (a == null)
         {
-          argName = argName.substring(NEGATESTRING.length());
-          a = argMap.get(argName);
-          negated = true;
+          if (argName.startsWith(NEGATESTRING) && argMap
+                  .containsKey(argName.substring(NEGATESTRING.length())))
+          {
+            argName = argName.substring(NEGATESTRING.length());
+            a = argMap.get(argName);
+            negated = true;
+          }
+          else
+          {
+            // after all other args, look for Opt.PREFIXKEV args if still not
+            // found
+            for (Arg potentialArg : EnumSet.allOf(Arg.class))
+            {
+              if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
+                      && argName.startsWith(potentialArg.getName())
+                      && equalPos > -1)
+              {
+                val = argName.substring(potentialArg.getName().length())
+                        + EQUALS + val;
+                argName = argName.substring(0,
+                        potentialArg.getName().length());
+                a = potentialArg;
+                break;
+              }
+            }
+          }
         }
 
         // check for config errors
@@ -464,7 +520,8 @@ public class ArgParser
               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
                       + val.substring(MATCHALLLINKEDIDS.length());
             }
-            else if (a.hasOption(Opt.OUTPUTFILE) && a.hasOption(Opt.ALLOWALL)
+            else if (a.hasOption(Opt.OUTPUTFILE)
+                    && a.hasOption(Opt.ALLOWALL)
                     && val.startsWith(MATCHOPENEDLINKEDIDS))
             {
               // --output=open*.ext is shorthand for --opened --output
@@ -902,15 +959,19 @@ public class ArgParser
     FIRST, BEFORE, AFTER
   }
 
-  // get from following Arg of type a or subval of same name (lowercase)
+  /**
+   * get from following Arg of type a or subval of same name (lowercase)
+   */
   public static String getValueFromSubValOrArg(ArgValuesMap avm,
           ArgValue av, Arg a, SubVals sv)
   {
     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
   }
 
-  // get from following Arg of type a or subval key or preference pref or
-  // default def
+  /**
+   * get from following Arg of type a or subval key or preference pref or
+   * default def
+   */
   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
           Arg a, SubVals sv, String key, String pref, String def)
   {
@@ -918,8 +979,10 @@ public class ArgParser
             def);
   }
 
-  // get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
-  // Arg of type a or subval key or preference pref or default def
+  /**
+   * get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
+   * Arg of type a or subval key or preference pref or default def
+   */
   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
           Position pos, ArgValue av, SubVals sv, String key, String pref,
           String def)
@@ -1048,7 +1111,9 @@ public class ArgParser
 
   // the following methods look for the "*" linkedId and add the argvalue to all
   // linkedId ArgValues if it does.
-  // This version inserts the subvals sv into all created values
+  /**
+   * This version inserts the subvals sv into all created values
+   */
   private void addValue(String linkedId, Type type, ArgValues avs,
           SubVals sv, String v, int argIndex, boolean doSubs)
   {
@@ -1096,21 +1161,32 @@ public class ArgParser
             doSubs);
   }
 
-  /*
+  /**
    * The following operations look for the "*" and "open*" linkedIds and add the
-   * argvalue to all appropriate linkedId ArgValues if it does.
-   * If subvals are supplied, they are inserted into all new set values.
+   * argvalue to all appropriate linkedId ArgValues if it does. If subvals are
+   * supplied, they are inserted into all new set values.
    * 
-   * @param op The ArgParser.Op operation
-   * @param linkedId The String linkedId from the ArgValuesMap
-   * @param type The Arg.Type to attach to this ArgValue
-   * @param avs The ArgValues for this linkedId
-   * @param sv Use these SubVals on the ArgValue
-   * @param merge Merge the SubVals with any existing on the value.  False will replace unless sv is null
-   * @param v The value of the ArgValue (may contain subvals).
-   * @param b The boolean value of the ArgValue.
-   * @param argIndex The argIndex for the ArgValue.
-   * @param doSubs Whether to perform substitutions on the subvals and value.
+   * @param op
+   *          The ArgParser.Op operation
+   * @param linkedId
+   *          The String linkedId from the ArgValuesMap
+   * @param type
+   *          The Arg.Type to attach to this ArgValue
+   * @param avs
+   *          The ArgValues for this linkedId
+   * @param sv
+   *          Use these SubVals on the ArgValue
+   * @param merge
+   *          Merge the SubVals with any existing on the value. False will
+   *          replace unless sv is null
+   * @param v
+   *          The value of the ArgValue (may contain subvals).
+   * @param b
+   *          The boolean value of the ArgValue.
+   * @param argIndex
+   *          The argIndex for the ArgValue.
+   * @param doSubs
+   *          Whether to perform substitutions on the subvals and value.
    */
   private void argValueOperation(Op op, String linkedId, Type type,
           ArgValues avs, SubVals sv, boolean merge, String v, boolean b,
index e1ad1d7..4b7b180 100644 (file)
@@ -4,6 +4,7 @@ import java.io.File;
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -108,6 +109,24 @@ public class BootstrapArgs
           }
         }
 
+        // after all other args, look for Opt.PREFIX args if still not found
+        if (!ArgParser.argMap.containsKey(argName))
+        {
+          for (Arg potentialArg : EnumSet.allOf(Arg.class))
+          {
+            if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
+                    && argName.startsWith(potentialArg.getName())
+                    && val != null)
+            {
+              val = argName.substring(potentialArg.getName().length())
+                      + ArgParser.EQUALS + val;
+              argName = argName.substring(0,
+                      potentialArg.getName().length());
+              break;
+            }
+          }
+        }
+
         if (ArgParser.argMap.containsKey(argName) && val == null)
         {
           val = "true";
index 69dcf71..bb31c5d 100644 (file)
@@ -55,9 +55,9 @@ public class ContactListImpl implements ContactListI
     {
       from_column = 0;
     }
-    if (to_column > getContactHeight())
+    if (to_column >= getContactHeight())
     {
-      to_column = getContactHeight();
+      to_column = getContactHeight()-1;
     }
     ContactRange cr = new ContactRange();
     cr.setFrom_column(from_column);
index 1d20987..925025f 100644 (file)
@@ -5,6 +5,9 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.util.List;
 
+import jalview.util.ColorUtils;
+import jalview.ws.datamodel.MappableContactMatrixI;
+
 public interface ContactMatrixI
 {
 
@@ -122,4 +125,79 @@ public interface ContactMatrixI
   }
 
   void setGroupSet(GroupSet makeGroups);
+
+  default void randomlyReColourGroups() {
+    if (hasGroupSet())
+    {
+      GroupSetI groups = getGroupSet();
+      for (BitSet group:groups.getGroups())
+      {
+        groups.setColorForGroup(group, ColorUtils.getARandomColor());
+      }
+    }
+  }
+
+  default void transferGroupColorsTo(AlignmentAnnotation aa)
+  {
+    if (hasGroupSet())
+    {
+      GroupSetI groups = getGroupSet();
+      // stash colors in linked annotation row.
+      // doesn't work yet. TESTS!
+      int sstart = aa.sequenceRef != null ? aa.sequenceRef.getStart() - 1
+              : 0;
+      Annotation ae;
+      Color gpcol = null;
+      int[] seqpos = null;
+      for (BitSet gp : groups.getGroups())
+      {
+        gpcol = groups.getColourForGroup(gp);
+        for (int p = gp.nextSetBit(0); p >= 0
+                && p < Integer.MAX_VALUE; p = gp.nextSetBit(p + 1))
+        {
+          if (this instanceof MappableContactMatrixI)
+          {
+            MappableContactMatrixI mcm = (MappableContactMatrixI) this;
+            seqpos = mcm.getMappedPositionsFor(aa.sequenceRef, p);
+            if (seqpos == null)
+            {
+              // no mapping for this column.
+              continue;
+            }
+            // TODO: handle ranges...
+            ae = aa.getAnnotationForPosition(seqpos[0]);
+          }
+          else
+          {
+            ae = aa.getAnnotationForPosition(p + sstart);
+          }
+          if (ae != null)
+          {
+            ae.colour = gpcol.brighter().darker();
+          }
+        }
+      }
+    }
+  }
+  
+  /**
+   * look up the colour for a column in the associated contact matrix 
+   * @return Color.white or assigned colour
+   */
+  default Color getGroupColorForPosition(int column)
+  {
+    if (hasGroupSet())
+    {
+      GroupSetI groups = getGroupSet();
+      for (BitSet gp:groups.getGroups())
+      {
+        if (gp.get(column))
+        {
+          return groups.getColourForGroup(gp);
+        }
+      }
+    }
+    return Color.white;
+  }
+  
 }
index f384b1e..3b1757b 100755 (executable)
@@ -154,6 +154,11 @@ public class DBRefSource
 
   public static boolean isPrimaryCandidate(String ucversion)
   {
+    if (ucversion==null)
+    {
+      // Null/empty version is not a real reference ?
+      return false;
+    }
     // tricky - this test really needs to search the sequence's set of dbrefs to
     // see if there is a primary reference that derived this reference.
     for (int i = allSources.length; --i >= 0;)
index c7a73b7..db38e7b 100644 (file)
@@ -145,30 +145,59 @@ public class GroupSet implements GroupSetI
     return treeType;
   }
 
-  public static GroupSet makeGroups(ContactMatrixI matrix, float thresh,
+  public static GroupSet makeGroups(ContactMatrixI matrix, boolean autoCut)
+  {
+    return makeGroups(matrix, autoCut, 0, autoCut);
+  }
+  public static GroupSet makeGroups(ContactMatrixI matrix, boolean auto, float thresh,
           boolean abs)
   {
     AverageDistanceEngine clusterer = new AverageDistanceEngine(null, null,
-            matrix);
+            matrix, true);
     double height = clusterer.findHeight(clusterer.getTopNode());
+    Console.debug("Column tree height: " + height);
     String newick = new jalview.io.NewickFile(clusterer.getTopNode(), false,
             true).print();
     String treeType = "UPGMA";
     Console.trace("Newick string\n" + newick);
 
     List<BinaryNode> nodegroups;
-    if (abs ? height > thresh : 0 < thresh && thresh < 1)
+    float cut = -1f;
+    if (auto)
     {
-      float cut = abs ? (float) (thresh / height) : thresh;
-      Console.debug("Threshold " + cut + " for height=" + height);
-
-      nodegroups = clusterer.groupNodes(cut);
+      double rootw = 0;
+      int p = 2;
+      BinaryNode bn = clusterer.getTopNode();
+      while (p-- > 0 & bn.left() != null)
+      {
+        if (bn.left() != null)
+        {
+          bn = bn.left();
+        }
+        if (bn.left() != null)
+        {
+          rootw = bn.height;
+        }
+      }
+      thresh = Math.max((float) (rootw / height) - 0.01f, 0);
+      cut = thresh;
+      nodegroups = clusterer.groupNodes(thresh);
     }
     else
     {
-      nodegroups = new ArrayList<BinaryNode>();
-      nodegroups.add(clusterer.getTopNode());
+      if (abs ? (height > thresh) : (0 < thresh && thresh < 1))
+      {
+        cut = abs ? thresh : (float) (thresh * height);
+        Console.debug("Threshold " + cut + " for height=" + height);
+        nodegroups = clusterer.groupNodes(cut);
+      }
+      else
+      {
+        nodegroups = new ArrayList<BinaryNode>();
+        nodegroups.add(clusterer.getTopNode());
+      }
     }
+    
     List<BitSet> groups = new ArrayList<>();
     for (BinaryNode root : nodegroups)
     {
@@ -179,7 +208,8 @@ public class GroupSet implements GroupSetI
       }
       groups.add(gpset);
     }
-    GroupSet grps = new GroupSet(abs, thresh, groups, treeType, newick);
+    GroupSet grps = new GroupSet(abs, (cut == -1f) ? thresh : cut, groups,
+            treeType, newick);
     return grps;
   }
 
index 32d295a..5bb55e5 100755 (executable)
@@ -1754,6 +1754,13 @@ public class Sequence extends ASequence implements SequenceI
       transferAnnotation(entry.getDatasetSequence(), mp);
       return;
     }
+    // transfer from entry to sequence
+    // if entry has a description and sequence doesn't, then transfer
+    if (entry.getDescription()!=null && (description==null || description.trim().length()==0))
+    {
+      description = entry.getDescription();
+    }
+    
     // transfer any new features from entry onto sequence
     if (entry.getSequenceFeatures() != null)
     {
index c64dac1..57b406e 100644 (file)
@@ -488,7 +488,7 @@ public class JmolParser extends StructureFile implements JmolStatusListener
   {
     int length = sq.getLength();
     boolean ssFound = false;
-    Annotation asecstr[] = new Annotation[length + firstResNum - 1];
+    Annotation asecstr[] = new Annotation[length + (firstResNum-sq.getStart())];
     for (int p = 0; p < length; p++)
     {
       if (secstr[p] >= 'A' && secstr[p] <= 'z')
index 77c0238..49bd4e9 100644 (file)
@@ -140,6 +140,7 @@ import jalview.io.JnetAnnotationMaker;
 import jalview.io.NewickFile;
 import jalview.io.ScoreMatrixFile;
 import jalview.io.TCoffeeScoreFile;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.io.vcf.VCFLoader;
 import jalview.jbgui.GAlignFrame;
 import jalview.project.Jalview2XML;
@@ -1455,34 +1456,74 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void htmlMenuItem_actionPerformed(ActionEvent e)
   {
     HtmlSvgOutput htmlSVG = new HtmlSvgOutput(alignPanel);
-    htmlSVG.exportHTML(null);
+    try {
+      htmlSVG.exportHTML(null);
+    } catch (ImageOutputException x) {
+      // report problem to console and raise dialog
+    }
   }
 
   @Override
   public void bioJSMenuItem_actionPerformed(ActionEvent e)
   {
     BioJsHTMLOutput bjs = new BioJsHTMLOutput(alignPanel);
+    try {
     bjs.exportHTML(null);
+  } catch (ImageOutputException x) {
+    // report problem to console and raise dialog
+  }
   }
 
   public void createImageMap(File file, String image)
   {
+    try {
     alignPanel.makePNGImageMap(file, image);
+    } catch (ImageOutputException x) {
+      // report problem to console and raise dialog
+    }
   }
 
+  @Override
+  public void createPNG_actionPerformed(ActionEvent e) {
+    try{
+      createPNG(null);
+    } catch (ImageOutputException ioex)
+    {
+      // raise dialog, and report via console
+    }
+  }
+  @Override
+  public void createEPS_actionPerformed(ActionEvent e) {
+    try{
+      createEPS(null);
+    } catch (ImageOutputException ioex)
+    {
+      // raise dialog, and report via console
+    }
+    
+  }
+  @Override
+  public void createSVG_actionPerformed(ActionEvent e) {
+    try{
+      createSVG(null);
+    } catch (ImageOutputException ioex)
+    {
+      // raise dialog, and report via console
+    }
+    
+  }
   /**
    * Creates a PNG image of the alignment and writes it to the given file. If
    * the file is null, the user is prompted to choose a file.
    * 
    * @param f
    */
-  @Override
-  public void createPNG(File f)
+  public void createPNG(File f) throws ImageOutputException
   {
     createPNG(f, null, BitmapImageSizing.nullBitmapImageSizing());
   }
 
-  public void createPNG(File f, String renderer, BitmapImageSizing userBis)
+  public void createPNG(File f, String renderer, BitmapImageSizing userBis) throws ImageOutputException
   {
     alignPanel.makeAlignmentImage(TYPE.PNG, f, renderer, userBis);
   }
@@ -1493,13 +1534,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * 
    * @param f
    */
-  @Override
-  public void createEPS(File f)
+  public void createEPS(File f)  throws ImageOutputException
   {
     createEPS(f, null);
   }
 
-  public void createEPS(File f, String renderer)
+  public void createEPS(File f, String renderer) throws ImageOutputException
   {
     alignPanel.makeAlignmentImage(TYPE.EPS, f, renderer);
   }
@@ -1510,13 +1550,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * 
    * @param f
    */
-  @Override
-  public void createSVG(File f)
+  public void createSVG(File f)  throws ImageOutputException
   {
     createSVG(f, null);
   }
 
-  public void createSVG(File f, String renderer)
+  public void createSVG(File f, String renderer) throws ImageOutputException
   {
     alignPanel.makeAlignmentImage(TYPE.SVG, f, renderer);
   }
@@ -4210,10 +4249,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       NewickFile fin = new NewickFile(
               new FileParse(cm.getNewick(), DataSourceType.PASTE));
-      String title = cm.getAnnotLabel() + " " + cm.getTreeMethod() + " tree"
-              + aa.sequenceRef != null
+      String title = aa.label + " "
+              + cm.getTreeMethod() + " tree" + (aa.sequenceRef != null
                       ? (" for " + aa.sequenceRef.getDisplayId(false))
-                      : "";
+                      : "");
 
       showColumnWiseTree(fin, aa, title, w, h, x, y);
     } catch (Throwable xx)
@@ -4233,7 +4272,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       {
         return null;
       }
-      TreePanel tp = new TreePanel(alignPanel, nf, aa, title);
+      TreePanel tp = new TreePanel(alignPanel, nf, aa, treeTitle);
 
       tp.setSize(w, h);
 
@@ -4242,7 +4281,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         tp.setLocation(x, y);
       }
 
-      Desktop.addInternalFrame(tp, title, w, h);
+      Desktop.addInternalFrame(tp, treeTitle, w, h);
       return tp;
     } catch (Throwable xx)
     {
index 7befa20..aa28a8c 100644 (file)
@@ -58,6 +58,7 @@ import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.gui.ImageExporter.ImageWriterI;
 import jalview.io.HTMLOutput;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.jbgui.GAlignmentPanel;
 import jalview.math.AlignmentDimension;
 import jalview.schemes.ResidueProperties;
@@ -1175,6 +1176,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   void makeAlignmentImage(ImageMaker.TYPE type, File file, String renderer)
+          throws ImageOutputException
   {
     makeAlignmentImage(type, file, renderer,
             BitmapImageSizing.nullBitmapImageSizing());
@@ -1190,7 +1192,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
    * @param bitmapscale
    */
   void makeAlignmentImage(ImageMaker.TYPE type, File file, String renderer,
-          BitmapImageSizing userBis)
+          BitmapImageSizing userBis) throws ImageOutputException
   {
     final int borderBottomOffset = 5;
 
@@ -1266,6 +1268,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
   }
 
   public void makePNGImageMap(File imgMapFile, String imageName)
+          throws ImageOutputException
   {
     // /////ONLY WORKS WITH NON WRAPPED ALIGNMENTS
     // ////////////////////////////////////////////
@@ -1390,7 +1393,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
       } catch (Exception ex)
       {
-        ex.printStackTrace();
+        throw new ImageOutputException(
+                "couldn't write ImageMap due to unexpected error", ex);
       }
     } // /////////END OF IMAGE MAP
 
index 6b82a37..28065c3 100755 (executable)
@@ -497,8 +497,10 @@ public class AnnotationLabels extends JPanel
           public void actionPerformed(ActionEvent e)
           {
             sel_row.setShowGroupsForContactMatrix(chitem.getState());
-            ap.getAnnotationPanel()
-            .paint(ap.getAnnotationPanel().getGraphics());
+            // so any annotation colour changes are propagated - though they
+            // probably won't be unless the annotation row colours are removed
+            // too!
+            ap.alignmentChanged();
           }
         });
         pop.add(chitem);
@@ -536,7 +538,10 @@ public class AnnotationLabels extends JPanel
               {
                 final long progBar;
                 ap.alignFrame.setProgressBar(MessageManager.formatMessage("action.clustering_matrix_for",cm.getAnnotDescr(),5f), progBar = System.currentTimeMillis());
-                cm.setGroupSet(GroupSet.makeGroups(cm, 5f, true));
+                cm.setGroupSet(GroupSet.makeGroups(cm, true));
+                cm.randomlyReColourGroups();
+                cm.transferGroupColorsTo(alignmentAnnotation);
+                ap.alignmentChanged();
                 ap.alignFrame.showContactMapTree(alignmentAnnotation, cm);
                 ap.alignFrame.setProgressBar(null, progBar);
               }
index 8e04048..8321741 100755 (executable)
@@ -737,7 +737,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
         if (evt.isControlDown()
                 && PAEContactMatrix.PAEMATRIX.equals(clicked.getCalcId()))
         {
-          int c = fr - 1;
+          int c = fr;
           ContactRange cr = forCurrentX.getRangeFor(fr, to);
           double cval;
           // TODO: could use GraphLine instead of arbitrary picking
@@ -747,7 +747,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
           // controls feathering - what other elements in row/column
           // should we select
           double thresh = cr.getMean() + (cr.getMax() - cr.getMean()) * .15;
-          while (c > 0)
+          while (c >= 0)
           {
             cval = forCurrentX.getContactAt(c);
             if (// cr.getMin() <= cval &&
@@ -1078,12 +1078,12 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       ContactGeometry lastXcgeom = new ContactGeometry(forFromX,
               cma.graphHeight);
       ContactGeometry.contactInterval lastXci = lastXcgeom
-              .mapFor(rowIndex[1], rowIndex[1] - deltaY);
+              .mapFor(rowIndex[1], rowIndex[1] + deltaY);
 
       ContactGeometry cXcgeom = new ContactGeometry(forToX,
               cma.graphHeight);
       ContactGeometry.contactInterval cXci = cXcgeom.mapFor(rowIndex[1],
-              rowIndex[1] - deltaY);
+              rowIndex[1] + deltaY);
 
       // mark rectangular region formed by drag
       jalview.bin.Console.trace("Matrix Selection from last(" + fromXc
@@ -1247,7 +1247,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       {
         row = i;
         res[0] = row;
-        res[1] = height - yPos;
+        res[1] = yPos-lheight;
         break;
       }
     }
@@ -1301,6 +1301,10 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     // TODO abstract tooltip generator so different implementations can be built
     if (ann.graph == AlignmentAnnotation.CONTACT_MAP)
     {
+      if (rowAndOffset>=ann.graphHeight)
+      {
+        return null;
+      }
       ContactListI clist = av.getContactList(ann, column);
       if (clist != null)
       {
index b7bac37..49eae98 100644 (file)
@@ -28,9 +28,16 @@ import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
 import java.io.File;
+import java.lang.reflect.InvocationTargetException;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
 
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
@@ -46,6 +53,7 @@ import jalview.datamodel.StructureViewerModel;
 import jalview.datamodel.StructureViewerModel.StructureData;
 import jalview.gui.ImageExporter.ImageWriterI;
 import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.structure.StructureCommand;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.BrowserLauncher;
@@ -426,12 +434,16 @@ public class AppJmol extends StructureViewerBase
   @Override
   public void makePDBImage(ImageMaker.TYPE type)
   {
+    try {
     makePDBImage(null, type, null,
             BitmapImageSizing.nullBitmapImageSizing());
+    } catch (ImageOutputException ioex) {
+      Console.error("Unexpected error whilst writing "+type.toString(),ioex);
+    }
   }
 
   public void makePDBImage(File file, ImageMaker.TYPE type, String renderer,
-          BitmapImageSizing userBis)
+          BitmapImageSizing userBis) throws ImageOutputException
   {
     int width = getWidth();
     int height = getHeight();
@@ -467,9 +479,48 @@ public class AppJmol extends StructureViewerBase
     };
     String view = MessageManager.getString("action.view")
             .toLowerCase(Locale.ROOT);
-    ImageExporter exporter = new ImageExporter(writer,
+    final ImageExporter exporter = new ImageExporter(writer,
             getProgressIndicator(), type, getTitle());
-    exporter.doExport(file, this, width, height, view, renderer, userBis);
+    
+    final Throwable[] exceptions = new Throwable[1];
+    exceptions[0] = null;
+    final AppJmol us = this;
+    try
+    {
+      Thread runner = Executors.defaultThreadFactory().newThread(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          try
+          {
+            exporter.doExport(file, us, width, height, view, renderer,
+                    userBis);
+          } catch (Throwable t)
+          {
+            exceptions[0] = t;
+          }
+        }
+      });
+      runner.start();
+      do { Thread.sleep(25); } while (runner.isAlive());
+    } catch (Throwable e)
+    {
+      throw new ImageOutputException(
+              "Unexpected error when generating image", e);
+    }
+    if (exceptions[0] != null)
+    {
+      if (exceptions[0] instanceof ImageOutputException)
+      {
+        throw ((ImageOutputException) exceptions[0]);
+      }
+      else
+      {
+        throw new ImageOutputException(
+                "Unexpected error when generating image", exceptions[0]);
+      }
+    }
   }
 
   @Override
index 6f2faae..12ff20b 100644 (file)
@@ -121,6 +121,7 @@ import jalview.io.FormatAdapter;
 import jalview.io.IdentifyFile;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.jbgui.GSplitFrame;
 import jalview.jbgui.GStructureViewer;
 import jalview.project.Jalview2XML;
@@ -535,6 +536,9 @@ public class Desktop extends jalview.jbgui.GDesktop
       setBounds(xPos, yPos, 900, 650);
     }
 
+    // start dialogue queue for single dialogues
+    startDialogQueue();
+
     if (!Platform.isJS())
     /**
      * Java only
@@ -622,6 +626,12 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
     desktop.addMouseListener(ma);
+
+    if (Platform.isJS())
+    {
+      // used for jalviewjsTest
+      jalview.bin.Console.info("JALVIEWJS: CREATED DESKTOP");
+    }
   }
 
   /**
@@ -1068,7 +1078,36 @@ public class Desktop extends jalview.jbgui.GDesktop
 
     setKeyBindings(frame);
 
-    desktop.add(frame);
+    // Since the latest FlatLaf patch, we occasionally have problems showing structureViewer frames...
+    int tries=3;
+    boolean shown=false;
+    Exception last=null;
+    do
+    {
+      try
+      {
+        desktop.add(frame);
+        shown=true;
+      } catch (IllegalArgumentException iaex)
+      {
+        last=iaex;
+        tries--;
+        jalview.bin.Console.info(
+                "Squashed IllegalArgument Exception (" + tries + " left) for "+frame.getTitle(),
+                iaex);
+        try
+        {
+          Thread.sleep(5);
+        } catch (InterruptedException iex)
+        {
+        }
+        ;
+      }
+    } while (!shown && tries > 0);
+    if (!shown)
+    {
+      jalview.bin.Console.error("Serious Problem whilst showing window "+frame.getTitle(),last);
+    }
 
     windowMenu.add(menuItem);
 
@@ -3050,7 +3089,7 @@ public class Desktop extends jalview.jbgui.GDesktop
   /**
    * pause the queue
    */
-  private java.util.concurrent.Semaphore block = new Semaphore(0);
+  private Semaphore block = new Semaphore(0);
 
   private static groovy.ui.Console groovyConsole;
 
@@ -3068,12 +3107,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       {
         if (dialogPause)
         {
-          try
-          {
-            block.acquire();
-          } catch (InterruptedException x)
-          {
-          }
+          acquireDialogQueue();
         }
         if (instance == null)
         {
@@ -3091,12 +3125,41 @@ public class Desktop extends jalview.jbgui.GDesktop
     });
   }
 
+  private boolean dialogQueueStarted = false;
+
   public void startDialogQueue()
   {
+    if (dialogQueueStarted)
+    {
+      return;
+    }
     // set the flag so we don't pause waiting for another permit and semaphore
     // the current task to begin
-    dialogPause = false;
+    releaseDialogQueue();
+    dialogQueueStarted = true;
+  }
+
+  public void acquireDialogQueue()
+  {
+    try
+    {
+      block.acquire();
+      dialogPause = true;
+    } catch (InterruptedException e)
+    {
+      jalview.bin.Console.debug("Interruption when acquiring DialogueQueue",
+              e);
+    }
+  }
+
+  public void releaseDialogQueue()
+  {
+    if (!dialogPause)
+    {
+      return;
+    }
     block.release();
+    dialogPause = false;
   }
 
   /**
@@ -3131,7 +3194,15 @@ public class Desktop extends jalview.jbgui.GDesktop
     String title = "View of desktop";
     ImageExporter exporter = new ImageExporter(writer, null, TYPE.EPS,
             title);
-    exporter.doExport(of, this, width, height, title);
+    try
+    {
+      exporter.doExport(of, this, width, height, title);
+    } catch (ImageOutputException ioex)
+    {
+      jalview.bin.Console.error(
+              "Unexpected error whilst writing Jalview desktop snapshot as EPS",
+              ioex);
+    }
   }
 
   /**
@@ -3571,4 +3642,52 @@ public class Desktop extends jalview.jbgui.GDesktop
       jalview.bin.Console.debug(Cache.getStackTraceString(e));
     }
   }
+
+  /**
+   * closes the current instance window, disposes and forgets about it.
+   */
+  public static void closeDesktop()
+  {
+    if (Desktop.instance != null)
+    {
+      Desktop.instance.closeAll_actionPerformed(null);
+      Desktop.instance.setVisible(false);
+      Desktop us = Desktop.instance;
+      Desktop.instance = null;
+      // call dispose in a separate thread - try to avoid indirect deadlocks
+      new Thread(new Runnable() {
+        @Override
+        public void run()
+        {
+          ExecutorService dex = us.dialogExecutor;
+          if (dex!=null) {
+            dex.shutdownNow();
+            us.dialogExecutor=null;
+            us.block.drainPermits();
+          }
+          us.dispose();
+        }
+      }).start();
+    }
+  }
+
+  /**
+   * checks if any progress bars are being displayed in any of the windows managed by the desktop
+   * @return
+   */
+  public boolean operationsAreInProgress()
+  {
+    JInternalFrame[] frames = getAllFrames();
+    for (JInternalFrame frame:frames)
+    {
+      if (frame instanceof IProgressIndicator)
+      {
+        if (((IProgressIndicator)frame).operationInProgress())
+        {
+          return true;
+        }
+      }
+    }
+    return operationInProgress();
+  }
 }
index 32704d6..8d28b1b 100644 (file)
@@ -29,10 +29,12 @@ import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.util.ImageMaker;
 import jalview.util.ImageMaker.TYPE;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
+import jalview.util.StringUtils;
 import jalview.util.imagemaker.BitmapImageSizing;
 
 /**
@@ -103,7 +105,7 @@ public class ImageExporter
    *          what the image is of e.g. Tree, Alignment
    */
   public void doExport(File file, Component parent, int width, int height,
-          String imageSource)
+          String imageSource) throws ImageOutputException
   {
     doExport(file, parent, width, height, imageSource, null,
             BitmapImageSizing.nullBitmapImageSizing());
@@ -111,6 +113,7 @@ public class ImageExporter
 
   public void doExport(File file, Component parent, int width, int height,
           String imageSource, String renderer, BitmapImageSizing userBis)
+          throws ImageOutputException
   {
     final long messageId = System.currentTimeMillis();
     setStatus(
@@ -123,6 +126,12 @@ public class ImageExporter
      */
     if (file == null && !Jalview.isHeadlessMode())
     {
+      if (Desktop.instance.isInBatchMode())
+      {
+        // defensive error report - we could wait for user input.. I guess ?
+        throw (new ImageOutputException(
+                "Need an output file to render to when exporting images in batch mode!"));
+      }
       JalviewFileChooser chooser = imageType.getFileChooser();
       chooser.setFileView(new JalviewFileView());
       MessageManager.formatMessage("label.create_image_of",
@@ -158,9 +167,9 @@ public class ImageExporter
       renderStyle = "Text";
     }
     AtomicBoolean textSelected = new AtomicBoolean(
-            !"Lineart".equals(renderStyle));
-    if ((imageType == TYPE.EPS || imageType == TYPE.SVG)
-            && LineartOptions.PROMPT_EACH_TIME.equals(renderStyle)
+            !StringUtils.equalsIgnoreCase("lineart", renderStyle));
+    if ((imageType == TYPE.EPS || imageType == TYPE.SVG) && StringUtils
+            .equalsIgnoreCase(LineartOptions.PROMPT_EACH_TIME, renderStyle)
             && !Jalview.isHeadlessMode())
     {
       final File chosenFile = file;
@@ -182,8 +191,8 @@ public class ImageExporter
     else
     {
       /*
-       * character rendering not required, or preference already set 
-       * - just do the export
+       * character rendering not required, or preference already set
+       * or we're in headless mode - just do the export
        */
       exportImage(file, !textSelected.get(), width, height, messageId,
               userBis);
@@ -220,8 +229,8 @@ public class ImageExporter
               messageId);
     } catch (Exception e)
     {
-      System.out.println(String.format("Error creating %s file: %s", type,
-              e.toString()));
+      jalview.bin.Console.error(String.format("Error creating %s file: %s",
+              type, e.toString()), e);
       setStatus(MessageManager.formatMessage("info.error_creating_file",
               type), messageId);
     }
index 26db8c3..211c370 100644 (file)
@@ -49,6 +49,7 @@ import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.gui.ImageExporter.ImageWriterI;
 import jalview.gui.JalviewColourChooser.ColourChooserListener;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.jbgui.GPCAPanel;
 import jalview.math.RotatableMatrix.Axis;
 import jalview.util.ImageMaker;
@@ -438,7 +439,11 @@ public class PCAPanel extends GPCAPanel
     };
     String pca = MessageManager.getString("label.pca");
     ImageExporter exporter = new ImageExporter(writer, null, type, pca);
-    exporter.doExport(null, this, width, height, pca);
+    try {
+      exporter.doExport(null, this, width, height, pca);
+    } catch (ImageOutputException ioex) {
+      Console.error("Unexpected error whilst writing "+type.toString(),ioex);
+    }
   }
 
   @Override
index be9293f..88c1292 100644 (file)
@@ -1734,10 +1734,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   protected void addReferenceAnnotations_actionPerformed(
           Map<SequenceI, List<AlignmentAnnotation>> candidates)
   {
-    final SequenceGroup selectionGroup = this.ap.av.getSelectionGroup();
     final AlignmentI alignment = this.ap.getAlignment();
     AlignmentUtils.addReferenceAnnotations(candidates, alignment,
-            selectionGroup);
+            null);
     refresh();
   }
 
index 35fdf6a..c4f32c3 100755 (executable)
@@ -156,6 +156,9 @@ public class Preferences extends GPreferences
   public static List<String> groupURLLinks;
   static
   {
+    // don't populate with session properties
+    Cache.disableSessionProperties();
+
     // get links selected to be in the menu (SEQUENCE_LINKS)
     // and links entered by the user but not selected (STORED_LINKS)
     String inMenuString = Cache.getDefault("SEQUENCE_LINKS", "");
@@ -180,6 +183,9 @@ public class Preferences extends GPreferences
      */
 
     groupURLLinks = new ArrayList<>();
+
+    // reenable
+    Cache.enableSessionProperties();
   }
 
   JInternalFrame frame;
@@ -248,6 +254,9 @@ public class Preferences extends GPreferences
   private Preferences()
   {
     super();
+    // don't populate with session properties
+    Cache.disableSessionProperties();
+
     frame = new JInternalFrame();
     frame.setFrameIcon(null);
     frame.setContentPane(this);
@@ -689,6 +698,8 @@ public class Preferences extends GPreferences
      * Set Startup tab defaults
      */
 
+    // re-enable
+    Cache.enableSessionProperties();
   }
 
   /**
@@ -701,6 +712,8 @@ public class Preferences extends GPreferences
   protected void setupOutputCombo(JComboBox<Object> comboBox,
           String propertyKey)
   {
+    Cache.disableSessionProperties();
+
     comboBox.addItem(promptEachTimeOpt);
     comboBox.addItem(lineArtOpt);
     comboBox.addItem(textOpt);
@@ -722,6 +735,8 @@ public class Preferences extends GPreferences
     {
       comboBox.setSelectedItem(promptEachTimeOpt);
     }
+
+    Cache.enableSessionProperties();
   }
 
   /**
@@ -733,6 +748,8 @@ public class Preferences extends GPreferences
   @Override
   public void ok_actionPerformed(ActionEvent e)
   {
+    Cache.disableSessionProperties();
+
     if (!validateSettings())
     {
       return;
@@ -746,6 +763,7 @@ public class Preferences extends GPreferences
     /*
      * Save Visual settings
      */
+
     Cache.applicationProperties.setProperty("SHOW_JVSUFFIX",
             Boolean.toString(seqLimit.isSelected()));
     Cache.applicationProperties.setProperty("RIGHT_ALIGN_IDS",
@@ -1020,10 +1038,14 @@ public class Preferences extends GPreferences
     } catch (Exception ex)
     {
     }
+
+    Cache.enableSessionProperties();
   }
 
   public void saveProxySettings()
   {
+    Cache.disableSessionProperties();
+
     String newProxyType = customProxy.isSelected() ? Cache.PROXYTYPE_CUSTOM
             : noProxy.isSelected() ? Cache.PROXYTYPE_NONE
                     : Cache.PROXYTYPE_SYSTEM;
@@ -1044,6 +1066,8 @@ public class Preferences extends GPreferences
       wsPrefs.update++;
     }
     previousProxyType = newProxyType;
+
+    Cache.enableSessionProperties();
   }
 
   /**
@@ -1075,6 +1099,8 @@ public class Preferences extends GPreferences
   @Override
   public void startupFileTextfield_mouseClicked()
   {
+    Cache.disableSessionProperties();
+
     // TODO: JAL-3048 not needed for Jalview-JS
     String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
     JalviewFileChooser chooser = JalviewFileChooser
@@ -1096,6 +1122,8 @@ public class Preferences extends GPreferences
       startupFileTextfield
               .setText(chooser.getSelectedFile().getAbsolutePath());
     }
+
+    Cache.enableSessionProperties();
   }
 
   /**
@@ -1405,6 +1433,7 @@ public class Preferences extends GPreferences
      */
     String viewerPath = "";
     List<String> paths = null;
+    Cache.disableSessionProperties();
     try
     {
       ViewerType viewerType = ViewerType.valueOf(selectedItem);
@@ -1430,6 +1459,7 @@ public class Preferences extends GPreferences
     {
       // only valid entries should be in the drop-down
     }
+    Cache.enableSessionProperties();
     structureViewerPath.setText(viewerPath);
 
     paths.add(0, structureViewerPath.getText());
index d15cdcf..62e48d2 100755 (executable)
@@ -1375,8 +1375,8 @@ public class SeqCanvas extends JPanel implements ViewportListenerI
         }
         else if (inGroup)
         {
-          drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
-          drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
+          drawVerticals(g, sx, xwidth, visWidth, oldY, bottom);
+          drawHorizontals(g, sx, xwidth, visWidth, top, bottom+1);
 
           // reset top and bottom
           top = -1;
@@ -1387,8 +1387,8 @@ public class SeqCanvas extends JPanel implements ViewportListenerI
       if (inGroup)
       {
         sy = verticalOffset + ((i - startSeq) * charHeight);
-        drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
-        drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
+        drawVerticals(g, sx, xwidth, visWidth, oldY, bottom);
+        drawHorizontals(g, sx, xwidth, visWidth, top, bottom+1);
       }
     }
   }
index 61273c7..0b7af0d 100755 (executable)
@@ -113,6 +113,7 @@ public class SplashScreen extends JPanel
    */
   public SplashScreen(boolean isTransient)
   {
+    Desktop.instance.acquireDialogQueue();
     this.transientDialog = isTransient;
 
     if (Platform.isJS()) // BH 2019
@@ -323,7 +324,7 @@ public class SplashScreen extends JPanel
     }
 
     closeSplash();
-    Desktop.instance.startDialogQueue();
+    Desktop.instance.releaseDialogQueue();
   }
 
   /**
index 55ce44a..6fbd422 100755 (executable)
@@ -53,6 +53,7 @@ import javax.swing.ToolTipManager;
 import jalview.analysis.Conservation;
 import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
+import jalview.bin.Console;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.BinaryNode;
@@ -66,6 +67,7 @@ import jalview.datamodel.SequenceNode;
 import jalview.gui.JalviewColourChooser.ColourChooserListener;
 import jalview.schemes.ColourSchemeI;
 import jalview.structure.SelectionSource;
+import jalview.util.ColorUtils;
 import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.ws.datamodel.MappableContactMatrixI;
@@ -221,6 +223,13 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
     boolean has_placeholders = false;
     longestName = "";
 
+    AlignmentAnnotation aa = tp.getAssocAnnotation();
+    ContactMatrixI cm = (aa!=null) ? av.getContactMatrix(aa) : null;
+    if (cm!=null && cm.hasCutHeight())
+    {
+      threshold=(float) cm.getCutHeight();
+    }
+    
     for (int i = 0; i < leaves.size(); i++)
     {
       BinaryNode lf = leaves.elementAt(i);
@@ -236,6 +245,14 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
         longestName = TreeCanvas.PLACEHOLDER
                 + ((Sequence) lf.element()).getName();
       }
+      if (tp.isColumnWise() && cm!=null)
+      {
+        // get color from group colours, if they are set for the matrix
+        try {
+          Color col = cm.getGroupColorForPosition(parseColumnNode(lf));
+          setColor(lf,col.brighter());
+        } catch (NumberFormatException ex) {};
+      }
     }
 
     setMarkPlaceholders(has_placeholders);
@@ -259,7 +276,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
    * @param offy
    *          DOCUMENT ME!
    */
-  public void drawNode(Graphics g, BinaryNode node, float chunk,
+  public void drawNode(Graphics g, BinaryNode node, double chunk,
           double wscale, int width, int offx, int offy)
   {
     if (node == null)
@@ -778,7 +795,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
               + ((BinaryNode) top.right()).count;
     }
 
-    float chunk = (float) (height - (offy)) / top.count;
+    double chunk = (double) (height - (offy)) / (double)top.count;
 
     drawNode(g2, tree.getTopNode(), chunk, wscale, width, offx, offy);
 
@@ -1025,7 +1042,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
           threshold = 0f;
         }
       }
-
+      Console.log.debug("Tree cut threshold set at:" + threshold);
       PaintRefresher.Refresh(tp,
               getAssociatedPanel().av.getSequenceSetId());
       repaint();
@@ -1040,8 +1057,8 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
     Map<BitSet, Color> colors = new HashMap();
     for (int i = 0; i < groups.size(); i++)
     {
-      Color col = new Color((int) (Math.random() * 255),
-              (int) (Math.random() * 255), (int) (Math.random() * 255));
+      Color col = ColorUtils.getARandomColor();
+      
       setColor(groups.get(i), col.brighter());
 
       Vector<BinaryNode> l = tree.findLeaves(groups.get(i));
@@ -1071,41 +1088,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
             cm.setColorForGroup(gp, colors.get(gp));
           }
         }
-        // stash colors in linked annotation row.
-        // doesn't work yet. TESTS!
-        int sstart = aa.sequenceRef != null ? aa.sequenceRef.getStart() - 1
-                : 0;
-        Annotation ae;
-        Color gpcol = null;
-        int[] seqpos = null;
-        for (BitSet gp : colors.keySet())
-        {
-          gpcol = colors.get(gp);
-          for (int p = gp.nextSetBit(0); p >= 0
-                  && p < Integer.MAX_VALUE; p = gp.nextSetBit(p + 1))
-          {
-            if (cm instanceof MappableContactMatrixI)
-            {
-              MappableContactMatrixI mcm = (MappableContactMatrixI) cm;
-              seqpos = mcm.getMappedPositionsFor(aa.sequenceRef, p);
-              if (seqpos == null)
-              {
-                // no mapping for this column.
-                continue;
-              }
-              // TODO: handle ranges...
-              ae = aa.getAnnotationForPosition(seqpos[0]);
-            }
-            else
-            {
-              ae = aa.getAnnotationForPosition(p + sstart);
-            }
-            if (ae != null)
-            {
-              ae.colour = gpcol.brighter().darker();
-            }
-          }
-        }
+        cm.transferGroupColorsTo(aa);
       }
     }
 
@@ -1123,15 +1106,18 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
       }
     }
   }
-
+  private int parseColumnNode(BinaryNode bn) throws NumberFormatException
+  {
+    return Integer.parseInt(
+            bn.getName().substring(bn.getName().indexOf("c") + 1));
+  }
   private boolean isColumnForNodeSelected(BinaryNode bn)
   {
     SequenceI rseq = tp.assocAnnotation.sequenceRef;
     int colm = -1;
     try
     {
-      colm = Integer.parseInt(
-              bn.getName().substring(bn.getName().indexOf("c") + 1));
+      colm = parseColumnNode(bn);
     } catch (Exception e)
     {
       return false;
@@ -1198,8 +1184,7 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
         // parse out from nodename
         try
         {
-          colm = Integer.parseInt(
-                  bn.getName().substring(bn.getName().indexOf("c") + 1));
+          colm = parseColumnNode(bn);
         } catch (Exception e)
         {
           continue;
@@ -1244,14 +1229,14 @@ public class TreeCanvas extends JPanel implements MouseListener, Runnable,
       if (mcm!=null)
       {
         int[] seqpos = mcm.getMappedPositionsFor(
-                tp.assocAnnotation.sequenceRef, colm);
+                rseq, colm);
         if (seqpos == null)
         {
           // no mapping for this column.
           continue;
         }
         // TODO: handle ranges...
-        offp = seqpos[0]-1;
+        offp = rseq.findIndex(seqpos[0])-1;
       }
       else
       {
index 4737235..2eff542 100755 (executable)
@@ -69,6 +69,7 @@ import jalview.gui.ImageExporter.ImageWriterI;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
 import jalview.io.NewickFile;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.jbgui.GTreePanel;
 import jalview.util.ImageMaker.TYPE;
 import jalview.util.MessageManager;
@@ -178,17 +179,18 @@ public class TreePanel extends GTreePanel
     this.treeType = type;
     this.scoreModelName = modelName;
 
+    treeCanvas = new TreeCanvas(this, ap, scrollPane);
+    scrollPane.setViewportView(treeCanvas);
+    
     if (columnWise)
     {
       bootstrapMenu.setVisible(false);
-      placeholdersMenu.setSelected(false);
+      placeholdersMenu.setState(false);
       placeholdersMenu.setVisible(false);
-      fitToWindow.setSelected(false);
+      fitToWindow.setState(false);
       sortAssocViews.setVisible(false);
     }
 
-    treeCanvas = new TreeCanvas(this, ap, scrollPane);
-    scrollPane.setViewportView(treeCanvas);
 
     addKeyListener(new KeyAdapter()
     {
@@ -390,9 +392,9 @@ public class TreePanel extends GTreePanel
                 ? new NJTree(av, sm, similarityParams)
                 : new AverageDistanceTree(av, sm, similarityParams);
         tree = new TreeModel(njtree);
-        showDistances(true);
+        // don't display distances for columnwise trees        
       }
-
+      showDistances(!columnWise);
       tree.reCount(tree.getTopNode());
       tree.findHeight(tree.getTopNode());
       treeCanvas.setTree(tree);
@@ -757,8 +759,12 @@ public class TreePanel extends GTreePanel
     String tree = MessageManager.getString("label.tree");
     ImageExporter exporter = new ImageExporter(writer, null, imageFormat,
             tree);
+    try {
     exporter.doExport(null, this, width, height,
             tree.toLowerCase(Locale.ROOT));
+    } catch (ImageOutputException ioex) {
+      Console.error("Unexpected error whilst writing "+imageFormat.toString(),ioex);
+    }
   }
 
   /**
index ed80eb9..d659e2a 100644 (file)
@@ -34,6 +34,7 @@ import jalview.datamodel.AlignExportSettingsAdapter;
 import jalview.datamodel.AlignmentExportData;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.IProgressIndicator;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.util.MessageManager;
 
 public abstract class HTMLOutput implements Runnable
@@ -302,12 +303,12 @@ public abstract class HTMLOutput implements Runnable
     return generatedFile;
   }
 
-  public void exportHTML(String outputFile)
+  public void exportHTML(String outputFile) throws ImageOutputException
   {
     exportHTML(outputFile, null);
   }
 
-  public void exportHTML(String outputFile, String renderer)
+  public void exportHTML(String outputFile, String renderer) throws ImageOutputException
   {
     setProgressMessage(MessageManager.formatMessage(
             "status.exporting_alignment_as_x_file", getDescription()));
@@ -358,5 +359,5 @@ public abstract class HTMLOutput implements Runnable
   }
 
   // used to pass an option such as render to run
-  public abstract void run(String string);
+  public abstract void run(String string) throws ImageOutputException;
 }
\ No newline at end of file
index 4d3ddc1..3041a23 100644 (file)
@@ -160,8 +160,10 @@ public class RnamlFile extends AlignFile
         }
       }
       sqs[i] = new Sequence(id, seq, begin, end);
-
-      sqs[i].setEnd(sqs[i].findPosition(sqs[i].getLength()));
+      if (seq.length()!=(end-begin+1))
+      {
+        sqs[i].setEnd(sqs[i].findPosition(sqs[i].getLength()));
+      }
       String[] annot = new String[rna.length()];
       Annotation[] ann = new Annotation[rna.length()];
 
diff --git a/src/jalview/io/exceptions/ImageOutputException.java b/src/jalview/io/exceptions/ImageOutputException.java
new file mode 100644 (file)
index 0000000..bf06494
--- /dev/null
@@ -0,0 +1,36 @@
+package jalview.io.exceptions;
+
+/**
+ * wrapper for passing error messages and exceptions back to UI when image io goes wrong
+ * @author jprocter
+ *
+ */
+public class ImageOutputException extends Exception
+{
+
+  public ImageOutputException()
+  {
+  }
+
+  public ImageOutputException(String message)
+  {
+    super(message);
+  }
+
+  public ImageOutputException(Throwable cause)
+  {
+    super(cause);
+  }
+
+  public ImageOutputException(String message, Throwable cause)
+  {
+    super(message, cause);
+  }
+
+  public ImageOutputException(String message, Throwable cause,
+          boolean enableSuppression, boolean writableStackTrace)
+  {
+    super(message, cause, enableSuppression, writableStackTrace);
+  }
+
+}
index f6ab8c9..c0cdfee 100755 (executable)
@@ -57,6 +57,7 @@ import jalview.bin.Cache;
 import jalview.gui.JvSwingUtils;
 import jalview.gui.Preferences;
 import jalview.io.FileFormats;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.schemes.ResidueColourScheme;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
@@ -1081,7 +1082,7 @@ public class GAlignFrame extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        createPNG(null);
+        createPNG_actionPerformed(e);
       }
     });
     createPNG.setActionCommand(
@@ -1113,7 +1114,7 @@ public class GAlignFrame extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        createEPS(null);
+        createEPS_actionPerformed(e);
       }
     });
 
@@ -1123,7 +1124,7 @@ public class GAlignFrame extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        createSVG(null);
+        createSVG_actionPerformed(e);
       }
     });
 
@@ -1975,6 +1976,24 @@ public class GAlignFrame extends JInternalFrame
     // selectMenu.add(listenToViewSelections);
   }
 
+  protected void createPNG_actionPerformed(ActionEvent object)
+  {
+    // TODO Auto-generated method stub
+    
+  }
+
+  protected void createEPS_actionPerformed(ActionEvent object)
+  {
+    // TODO Auto-generated method stub
+    
+  }
+
+  protected void createSVG_actionPerformed(ActionEvent object)
+  {
+    // TODO Auto-generated method stub
+    
+  }
+
   protected void copyHighlightedColumns_actionPerformed(
           ActionEvent actionEvent)
   {
@@ -2468,9 +2487,6 @@ public class GAlignFrame extends JInternalFrame
   {
   }
 
-  public void createPNG(java.io.File f)
-  {
-  }
 
   protected void font_actionPerformed(ActionEvent e)
   {
@@ -2485,14 +2501,6 @@ public class GAlignFrame extends JInternalFrame
 
   }
 
-  public void createEPS(java.io.File f)
-  {
-  }
-
-  public void createSVG(java.io.File f)
-  {
-
-  }
 
   protected void loadTreeMenuItem_actionPerformed(ActionEvent e)
   {
index ad1fa4a..d943d39 100644 (file)
@@ -27,16 +27,23 @@ import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.Image;
+import java.awt.RenderingHints;
+import java.awt.Stroke;
 import java.awt.geom.AffineTransform;
 import java.awt.image.ImageObserver;
 import java.util.BitSet;
 import java.util.Hashtable;
 
+import org.jfree.graphics2d.svg.SVGGraphics2D;
+import org.jibble.epsgraphics.EpsGraphics2D;
+
 import jalview.analysis.AAFrequency;
 import jalview.analysis.CodingUtils;
 import jalview.analysis.Rna;
 import jalview.analysis.StructureFrequency;
 import jalview.api.AlignViewportI;
+import jalview.bin.Cache;
+import jalview.bin.Console;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.ColumnSelection;
@@ -88,6 +95,10 @@ public class AnnotationRenderer
 
   private boolean av_ignoreGapsConsensus;
 
+  private boolean vectorRendition = false;
+
+  private boolean glyphLineDrawn = false;
+
   /**
    * attributes set from AwtRenderPanelI
    */
@@ -189,7 +200,7 @@ public class AnnotationRenderer
          * if new annotation with a closing base pair half of the stem, 
          * display a backward arrow
          */
-        g.fillPolygon(new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
+        fillPolygon(g, new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
                 new int[]
                 { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
                 3);
@@ -209,7 +220,7 @@ public class AnnotationRenderer
          * if annotation ending with an opeing base pair half of the stem, 
          * display a forward arrow
          */
-        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 },
+        fillPolygon(g, new int[] { x2 - 5, x2 - 5, x2 },
                 new int[]
                 { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
                 3);
@@ -221,7 +232,7 @@ public class AnnotationRenderer
       }
     }
     // draw arrow body
-    g.fillRect(x1, y + 4 + iconOffset, x2 - x1, 7);
+    fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 7);
   }
 
   void drawNotCanonicalAnnot(Graphics g, Color nonCanColor,
@@ -229,9 +240,8 @@ public class AnnotationRenderer
           int iconOffset, int startRes, int column, boolean validRes,
           boolean validEnd)
   {
-    // System.out.println(nonCanColor);
+    // Console.info(nonCanColor);
 
-    g.setColor(nonCanColor);
     int sCol = (lastSSX / charWidth)
             + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
@@ -245,9 +255,16 @@ public class AnnotationRenderer
     boolean diffdownstream = !validRes || !validEnd
             || row_annotations[column] == null
             || !dc.equals(row_annotations[column].displayCharacter);
-    // System.out.println("Column "+column+" diff up: "+diffupstream+"
+    // Console.info("Column "+column+" diff up:
+    // "+diffupstream+"
     // down:"+diffdownstream);
     // If a closing base pair half of the stem, display a backward arrow
+    if (diffupstream || diffdownstream)
+    {
+      // draw glyphline under arrow
+      drawGlyphLine(g, lastSSX, x, y, iconOffset);
+    }
+    g.setColor(nonCanColor);
     if (column > 0 && Rna.isClosingParenthesis(dc))
     {
 
@@ -255,9 +272,10 @@ public class AnnotationRenderer
       // if (validRes && column>1 && row_annotations[column-2]!=null &&
       // dc.equals(row_annotations[column-2].displayCharacter))
       {
-        g.fillPolygon(new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
+        fillPolygon(g, new int[] { lastSSX + 5, lastSSX + 5, lastSSX },
                 new int[]
-                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
+                { y + iconOffset + 1, y + 13 + iconOffset,
+                    y + 7 + iconOffset },
                 3);
         x1 += 5;
       }
@@ -272,9 +290,10 @@ public class AnnotationRenderer
       // display a forward arrow
       if (diffdownstream)
       {
-        g.fillPolygon(new int[] { x2 - 5, x2 - 5, x2 },
+        fillPolygon(g, new int[] { x2 - 6, x2 - 6, x2 - 1 },
                 new int[]
-                { y + iconOffset, y + 14 + iconOffset, y + 8 + iconOffset },
+                { y + iconOffset + 1, y + 13 + iconOffset,
+                    y + 7 + iconOffset },
                 3);
         x2 -= 5;
       }
@@ -284,7 +303,8 @@ public class AnnotationRenderer
       }
     }
     // draw arrow body
-    g.fillRect(x1, y + 4 + iconOffset, x2 - x1, 7);
+    unsetAntialias(g);
+    fillRect(g, x1, y + 4 + iconOffset, x2 - x1, 6);
   }
 
   // public void updateFromAnnotationPanel(FontMetrics annotFM, AlignViewportI
@@ -447,6 +467,12 @@ public class AnnotationRenderer
           AlignViewportI av, Graphics g, int activeRow, int startRes,
           int endRes)
   {
+    if (g instanceof EpsGraphics2D || g instanceof SVGGraphics2D)
+    {
+      this.setVectorRendition(true);
+    }
+    Graphics2D g2d = (Graphics2D) g;
+
     long stime = System.currentTimeMillis();
     boolean usedFaded = false;
     // NOTES:
@@ -592,9 +618,13 @@ public class AnnotationRenderer
          * 
          * continue; }
          */
+
         // first pass sets up state for drawing continuation from left-hand
         // column
         // of startRes
+
+        // flag used for vector rendition
+        this.glyphLineDrawn = false;
         x = (startRes == 0) ? 0 : -1;
         while (x < endRes - startRes)
         {
@@ -626,6 +656,7 @@ public class AnnotationRenderer
                   : null;
           if (x > -1)
           {
+            unsetAntialias(g);
             if (activeRow == i)
             {
               g.setColor(Color.red);
@@ -634,24 +665,24 @@ public class AnnotationRenderer
               {
                 if (columnSelection.contains(column))
                 {
-                  g.fillRect(x * charWidth, y, charWidth, charHeight);
+                  fillRect(g, x * charWidth, y, charWidth, charHeight);
                 }
               }
             }
             if (row.getInvalidStrucPos() > x)
             {
               g.setColor(Color.orange);
-              g.fillRect(x * charWidth, y, charWidth, charHeight);
+              fillRect(g, x * charWidth, y, charWidth, charHeight);
             }
             else if (row.getInvalidStrucPos() == x)
             {
               g.setColor(Color.orange.darker());
-              g.fillRect(x * charWidth, y, charWidth, charHeight);
+              fillRect(g, x * charWidth, y, charWidth, charHeight);
             }
             if (validCharWidth && validRes && displayChar != null
                     && (displayChar.length() > 0))
             {
-              Graphics2D gg = ((Graphics2D) g);
+              // Graphics2D gg = (g);
               float fmWidth = fm.charsWidth(displayChar.toCharArray(), 0,
                       displayChar.length());
 
@@ -674,11 +705,11 @@ public class AnnotationRenderer
 
               if (row_annotations[column].colour == null)
               {
-                gg.setColor(Color.black);
+                g2d.setColor(Color.black);
               }
               else
               {
-                gg.setColor(row_annotations[column].colour);
+                g2d.setColor(row_annotations[column].colour);
               }
 
               /*
@@ -691,19 +722,20 @@ public class AnnotationRenderer
               /*
                * translate to drawing position _before_ applying any scaling
                */
-              gg.translate(xPos, yPos);
+              g2d.translate(xPos, yPos);
               if (scaledToFit)
               {
                 /*
                  * use a scaling transform to make the label narrower
                  * (JalviewJS doesn't have Font.deriveFont(AffineTransform))
                  */
-                gg.transform(
+                g2d.transform(
                         AffineTransform.getScaleInstance(fmScaling, 1.0));
               }
+              setAntialias(g);
               if (column == 0 || row.graph > 0)
               {
-                gg.drawString(displayChar, 0, 0);
+                g2d.drawString(displayChar, 0, 0);
               }
               else if (row_annotations[column - 1] == null || (labelAllCols
                       || !displayChar.equals(
@@ -711,7 +743,7 @@ public class AnnotationRenderer
                       || (displayChar.length() < 2
                               && row_annotations[column].secondaryStructure == ' ')))
               {
-                gg.drawString(displayChar, 0, 0);
+                g2d.drawString(displayChar, 0, 0);
               }
               if (scaledToFit)
               {
@@ -719,10 +751,10 @@ public class AnnotationRenderer
                  * undo scaling before translating back 
                  * (restoring saved transform does NOT work in JS PDFGraphics!)
                  */
-                gg.transform(AffineTransform
+                g2d.transform(AffineTransform
                         .getScaleInstance(1D / fmScaling, 1.0));
               }
-              gg.translate(-xPos, -yPos);
+              g2d.translate(-xPos, -yPos);
             }
           }
           if (row.hasIcons)
@@ -784,7 +816,8 @@ public class AnnotationRenderer
               {
 
                 // int nb_annot = x - temp;
-                // System.out.println("\t type :"+lastSS+"\t x :"+x+"\t nbre
+                // Console.info("\t type :"+lastSS+"\t x
+                // :"+x+"\t nbre
                 // annot :"+nb_annot);
                 switch (lastSS)
                 {
@@ -878,10 +911,17 @@ public class AnnotationRenderer
                   // temp = x;
                   break;
                 default:
-                  g.setColor(Color.gray);
-                  g.fillRect(lastSSX, y + 6 + iconOffset,
-                          (x * charWidth) - lastSSX, 2);
-                  // temp = x;
+                  if (isVectorRendition())
+                  {
+                    // draw single full width glyphline
+                    drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset);
+                    // disable more glyph lines
+                    this.glyphLineDrawn = true;
+                  }
+                  else
+                  {
+                    drawGlyphLine(g, lastSSX, x, y, iconOffset);
+                  }
                   break;
                 }
               }
@@ -1006,14 +1046,23 @@ public class AnnotationRenderer
           case 'y':
           case 'Z':
           case 'z':
-            // System.out.println(lastSS);
+            // Console.info(lastSS);
             Color nonCanColor = getNotCanonicalColor(lastSS);
             drawNotCanonicalAnnot(g, nonCanColor, row_annotations, lastSSX,
                     x, y, iconOffset, startRes, column, validRes, validEnd);
             break;
           default:
-            drawGlyphLine(g, row_annotations, lastSSX, x, y, iconOffset,
-                    startRes, column, validRes, validEnd);
+            if (isVectorRendition())
+            {
+              // draw single full width glyphline
+              drawGlyphLine(g, lastSSX, endRes - x, y, iconOffset);
+              // disable more glyph lines
+              this.glyphLineDrawn = true;
+            }
+            else
+            {
+              drawGlyphLine(g, lastSSX, x, y, iconOffset);
+            }
             break;
           }
         }
@@ -1091,7 +1140,7 @@ public class AnnotationRenderer
               }
               else
               {
-                System.err.println(
+                Console.warn(
                         "rendered with " + renderer.getClass().toString());
               }
             }
@@ -1122,17 +1171,15 @@ public class AnnotationRenderer
       {
         if (clipst)
         {
-          System.err.println(
-                  "Start clip at : " + yfrom + " (index " + f_i + ")");
+          Console.warn("Start clip at : " + yfrom + " (index " + f_i + ")");
         }
         if (clipend)
         {
-          System.err.println(
-                  "End clip at : " + yto + " (index " + f_to + ")");
+          Console.warn("End clip at : " + yto + " (index " + f_to + ")");
         }
       }
       ;
-      System.err.println("Annotation Rendering time:"
+      Console.warn("Annotation Rendering time:"
               + (System.currentTimeMillis() - stime));
     }
     ;
@@ -1150,10 +1197,15 @@ public class AnnotationRenderer
 
   // private Color sdNOTCANONICAL_COLOUR;
 
-  void drawGlyphLine(Graphics g, Annotation[] row, int lastSSX, int x,
-          int y, int iconOffset, int startRes, int column, boolean validRes,
-          boolean validEnd)
+  void drawGlyphLine(Graphics g, int lastSSX, int x, int y, int iconOffset)
   {
+    if (glyphLineDrawn)
+    {
+      // if we've drawn a single long glyphline for an export, don't draw the
+      // bits
+      return;
+    }
+    unsetAntialias(g);
     g.setColor(GLYPHLINE_COLOR);
     g.fillRect(lastSSX, y + 6 + iconOffset, (x * charWidth) - lastSSX, 2);
   }
@@ -1163,45 +1215,52 @@ public class AnnotationRenderer
           int lastSSX, int x, int y, int iconOffset, int startRes,
           int column, boolean validRes, boolean validEnd)
   {
-    g.setColor(SHEET_COLOUR);
-
     if (!validEnd || !validRes || row == null || row[column] == null
             || row[column].secondaryStructure != 'E')
     {
-      g.fillRect(lastSSX, y + 4 + iconOffset, (x * charWidth) - lastSSX - 4,
-              7);
-      g.fillPolygon(
+      // draw the glyphline underneath
+      drawGlyphLine(g, lastSSX, x, y, iconOffset);
+
+      g.setColor(SHEET_COLOUR);
+      fillRect(g, lastSSX, y + 4 + iconOffset,
+              (x * charWidth) - lastSSX - 4, 6);
+      fillPolygon(g,
               new int[]
-              { (x * charWidth) - 4, (x * charWidth) - 4, (x * charWidth) },
+              { (x * charWidth) - 6, (x * charWidth) - 6,
+                  (x * charWidth - 1) },
               new int[]
-              { y + iconOffset, y + 14 + iconOffset, y + 7 + iconOffset },
+              { y + iconOffset + 1, y + 13 + iconOffset,
+                  y + 7 + iconOffset },
               3);
     }
     else
     {
-      g.fillRect(lastSSX, y + 4 + iconOffset, (x + 1) * charWidth - lastSSX,
-              7);
+      g.setColor(SHEET_COLOUR);
+      fillRect(g, lastSSX, y + 4 + iconOffset, (x * charWidth) - lastSSX,
+              6);
     }
-
   }
 
   void drawHelixAnnot(Graphics g, Annotation[] row, int lastSSX, int x,
           int y, int iconOffset, int startRes, int column, boolean validRes,
           boolean validEnd)
   {
-    g.setColor(HELIX_COLOUR);
-
     int sCol = (lastSSX / charWidth)
             + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
     int x2 = (x * charWidth);
 
-    if (USE_FILL_ROUND_RECT)
+    if (USE_FILL_ROUND_RECT || isVectorRendition())
     {
+      // draw glyph line behind helix (visible in EPS or SVG output)
+      drawGlyphLine(g, lastSSX, x, y, iconOffset);
+
+      g.setColor(HELIX_COLOUR);
+      setAntialias(g);
       int ofs = charWidth / 2;
       // Off by 1 offset when drawing rects and ovals
       // to offscreen image on the MAC
-      g.fillRoundRect(lastSSX, y + 4 + iconOffset, x2 - x1, 8, 8, 8);
+      fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - 1, 8, 8, 8);
       if (sCol == 0 || row[sCol - 1] == null
               || row[sCol - 1].secondaryStructure != 'H')
       {
@@ -1209,8 +1268,8 @@ public class AnnotationRenderer
       else
       {
         // g.setColor(Color.orange);
-        g.fillRoundRect(lastSSX, y + 4 + iconOffset, x2 - x1 - ofs + 1, 8,
-                0, 0);
+        fillRoundRect(g, lastSSX, y + 3 + iconOffset, x2 - x1 - ofs, 8, 0,
+                0);
       }
       if (!validRes || row[column] == null
               || row[column].secondaryStructure != 'H')
@@ -1220,30 +1279,38 @@ public class AnnotationRenderer
       else
       {
         // g.setColor(Color.magenta);
-        g.fillRoundRect(lastSSX + ofs, y + 4 + iconOffset,
-                x2 - x1 - ofs + 1, 8, 0, 0);
-
+        fillRoundRect(g, lastSSX + ofs, y + 3 + iconOffset, x2 - x1 - ofs,
+                8, 0, 0);
       }
 
       return;
     }
 
-    if (sCol == 0 || row[sCol - 1] == null
-            || row[sCol - 1].secondaryStructure != 'H')
+    boolean leftEnd = sCol == 0 || row[sCol - 1] == null
+            || row[sCol - 1].secondaryStructure != 'H';
+    boolean rightEnd = !validRes || row[column] == null
+            || row[column].secondaryStructure != 'H';
+
+    if (leftEnd || rightEnd)
+    {
+      drawGlyphLine(g, lastSSX, x, y, iconOffset);
+    }
+    g.setColor(HELIX_COLOUR);
+
+    if (leftEnd)
     {
-      g.fillArc(lastSSX, y + 4 + iconOffset, charWidth, 8, 90, 180);
+      fillArc(g, lastSSX, y + 3 + iconOffset, charWidth, 8, 90, 180);
       x1 += charWidth / 2;
     }
 
-    if (!validRes || row[column] == null
-            || row[column].secondaryStructure != 'H')
+    if (rightEnd)
     {
-      g.fillArc((x * charWidth) - charWidth, y + 4 + iconOffset, charWidth,
-              8, 270, 180);
+      fillArc(g, ((x - 1) * charWidth), y + 3 + iconOffset, charWidth, 8,
+              270, 180);
       x2 -= charWidth / 2;
     }
 
-    g.fillRect(x1, y + 4 + iconOffset, x2 - x1, 8);
+    fillRect(g, x1, y + 3 + iconOffset, x2 - x1, 8);
   }
 
   void drawLineGraph(Graphics g, AlignmentAnnotation _aa,
@@ -1254,6 +1321,13 @@ public class AnnotationRenderer
     {
       return;
     }
+    Stroke roundStroke = new BasicStroke(1, BasicStroke.CAP_ROUND,
+            BasicStroke.JOIN_ROUND);
+    Stroke squareStroke = new BasicStroke(1, BasicStroke.CAP_SQUARE,
+            BasicStroke.JOIN_MITER);
+    Graphics2D g2d = (Graphics2D) g;
+    Stroke prevStroke = g2d.getStroke();
+    g2d.setStroke(roundStroke);
 
     int x = 0;
 
@@ -1280,7 +1354,8 @@ public class AnnotationRenderer
     }
 
     g.setColor(Color.gray);
-    g.drawLine(x - charWidth, y2, (eRes - sRes + 1) * charWidth, y2);
+    drawLine(g, squareStroke, x * charWidth - charWidth, y2,
+            (eRes - sRes) * charWidth, y2);
 
     eRes = Math.min(eRes, aa_annotations.length);
 
@@ -1322,7 +1397,7 @@ public class AnnotationRenderer
         // standalone value
         y1 = y - (int) (((aa_annotations[column].value - min) / range)
                 * graphHeight);
-        g.drawLine(x * charWidth + charWidth / 4, y1,
+        drawLine(g, x * charWidth + charWidth / 4, y1,
                 x * charWidth + 3 * charWidth / 4, y1);
         x++;
         continue;
@@ -1339,7 +1414,7 @@ public class AnnotationRenderer
       y2 = y - (int) (((aa_annotations[column].value - min) / range)
               * graphHeight);
 
-      g.drawLine(x * charWidth - charWidth / 2, y1,
+      drawLine(g, (x - 1) * charWidth + charWidth / 2, y1,
               x * charWidth + charWidth / 2, y2);
       x++;
     }
@@ -1348,14 +1423,14 @@ public class AnnotationRenderer
     {
       g.setColor(_aa.threshold.colour);
       Graphics2D g2 = (Graphics2D) g;
-      g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
+      Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE,
               BasicStroke.JOIN_ROUND, 3f, new float[]
-              { 5f, 3f }, 0f));
+              { 5f, 3f }, 0f);
 
       y2 = (int) (y - ((_aa.threshold.value - min) / range) * graphHeight);
-      g.drawLine(0, y2, (eRes - sRes) * charWidth, y2);
-      g2.setStroke(new BasicStroke());
+      drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2);
     }
+    g2d.setStroke(prevStroke);
   }
 
   @SuppressWarnings("unused")
@@ -1382,7 +1457,7 @@ public class AnnotationRenderer
 
     g.setColor(Color.gray);
 
-    g.drawLine(x, y2, (eRes - sRes) * charWidth, y2);
+    drawLine(g, x, y2, (eRes - sRes) * charWidth, y2);
 
     int column;
     int aaMax = aa_annotations.length - 1;
@@ -1420,11 +1495,11 @@ public class AnnotationRenderer
       {
         if (y1 - y2 > 0)
         {
-          g.fillRect(x * charWidth, y2, charWidth, y1 - y2);
+          fillRect(g, x * charWidth, y2, charWidth, y1 - y2);
         }
         else
         {
-          g.fillRect(x * charWidth, y1, charWidth, y2 - y1);
+          fillRect(g, x * charWidth, y1, charWidth, y2 - y1);
         }
       }
       // draw profile if available
@@ -1455,7 +1530,8 @@ public class AnnotationRenderer
           // lm is not necessary - we can just use fm - could be off by no more
           // than 0.5 px
           // LineMetrics lm = g.getFontMetrics(ofont).getLineMetrics("Q", g);
-          // System.out.println(asc + " " + dec + " " + (asc - lm.getAscent())
+          // Console.info(asc + " " + dec + " " + (asc -
+          // lm.getAscent())
           // + " " + (dec - lm.getDescent()));
 
           double asc = fm.getAscent();
@@ -1580,15 +1656,13 @@ public class AnnotationRenderer
     if (_aa.threshold != null)
     {
       g.setColor(_aa.threshold.colour);
-      Graphics2D g2 = (Graphics2D) g;
-      g2.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE,
+      Stroke s = new BasicStroke(1, BasicStroke.CAP_SQUARE,
               BasicStroke.JOIN_ROUND, 3f, new float[]
-              { 5f, 3f }, 0f));
+              { 5f, 3f }, 0f);
 
       y2 = (int) (y
               - ((_aa.threshold.value - min) / range) * _aa.graphHeight);
-      g.drawLine(0, y2, (eRes - sRes) * charWidth, y2);
-      g2.setStroke(new BasicStroke());
+      drawLine(g, s, 0, y2, (eRes - sRes) * charWidth, y2);
     }
   }
 
@@ -1598,7 +1672,7 @@ public class AnnotationRenderer
   {
     eRes = Math.min(eRes, aa_annotations.length);
     g.setColor(Color.white);
-    g.fillRect(0, 0, width, y);
+    fillRect(g, 0, 0, width, y);
     g.setColor(new Color(0, 0, 180));
 
     int x = 0, height;
@@ -1622,7 +1696,7 @@ public class AnnotationRenderer
           height = y;
         }
 
-        g.fillRect(x, y - height, charWidth, height);
+        fillRect(g, x, y - height, charWidth, height);
       }
       x += charWidth;
     }
@@ -1749,9 +1823,92 @@ public class AnnotationRenderer
       return new Color(0, 80, 255);
 
     default:
-      System.out.println("This is not a interaction : " + lastss);
+      Console.info("This is not a interaction : " + lastss);
       return null;
 
     }
   }
+
+  private void fillPolygon(Graphics g, int[] xpoints, int[] ypoints, int n)
+  {
+    setAntialias(g);
+    g.fillPolygon(xpoints, ypoints, n);
+  }
+
+  /*
+  private void fillRect(Graphics g, int a, int b, int c, int d)
+  {
+    fillRect(g, false, a, b, c, d);
+  }*/
+
+  private void fillRect(Graphics g, int a, int b, int c, int d)
+  {
+    unsetAntialias(g);
+    g.fillRect(a, b, c, d);
+  }
+
+  private void fillRoundRect(Graphics g, int a, int b, int c, int d, int e,
+          int f)
+  {
+    setAntialias(g);
+    g.fillRoundRect(a, b, c, d, e, f);
+  }
+
+  private void fillArc(Graphics g, int a, int b, int c, int d, int e, int f)
+  {
+    setAntialias(g);
+    g.fillArc(a, b, c, d, e, f);
+  }
+
+  private void drawLine(Graphics g, Stroke s, int a, int b, int c, int d)
+  {
+    Graphics2D g2d = (Graphics2D) g;
+    Stroke p = g2d.getStroke();
+    g2d.setStroke(s);
+    drawLine(g, a, b, c, d);
+    g2d.setStroke(p);
+  }
+
+  private void drawLine(Graphics g, int a, int b, int c, int d)
+  {
+    setAntialias(g);
+    g.drawLine(a, b, c, d);
+  }
+
+  private void setAntialias(Graphics g)
+  {
+    if (isVectorRendition())
+    {
+      // no need to antialias vector drawings
+      return;
+    }
+    if (Cache.getDefault("ANTI_ALIAS", true))
+    {
+      Graphics2D g2d = (Graphics2D) g;
+      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+              RenderingHints.VALUE_ANTIALIAS_ON);
+    }
+  }
+
+  private void unsetAntialias(Graphics g)
+  {
+    if (isVectorRendition())
+    {
+      // no need to antialias vector drawings
+      return;
+    }
+    Graphics2D g2d = (Graphics2D) g;
+    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+            RenderingHints.VALUE_ANTIALIAS_OFF);
+  }
+
+  public void setVectorRendition(boolean b)
+  {
+    vectorRendition = b;
+  }
+
+  public boolean isVectorRendition()
+  {
+    return vectorRendition;
+  }
 }
index 8f38d02..0471145 100644 (file)
@@ -109,11 +109,12 @@ public abstract class ContactMapRenderer implements AnnotationRowRendererI
     }
     eRes = Math.min(eRes, aa_annotations.length);
 
-    int x = 0, y2 = y;
+    int x = 0, topY = y;
 
-    g.setColor(shade.no_data);
-
-    g.drawLine(x, y2, (eRes - sRes) * charWidth, y2);
+    // uncomment below to render whole area of matrix as pink
+    // g.setColor(shade.no_data);
+    // g.fillRect(x, topY-_aa.height, (eRes - sRes) * charWidth, _aa.graphHeight);
+    
     boolean showGroups = _aa.isShowGroupsForContactMatrix();
     int column;
     int aaMax = aa_annotations.length - 1;
@@ -165,11 +166,11 @@ public abstract class ContactMapRenderer implements AnnotationRowRendererI
       final ContactGeometry cgeom = new ContactGeometry(contacts,
               _aa.graphHeight);
 
-      for (int ht = y2, eht = y2
-              - _aa.graphHeight; ht >= eht; ht -= cgeom.pixels_step)
+      for (int ht = 0, botY = topY
+              - _aa.height; ht < _aa.graphHeight; ht += cgeom.pixels_step)
       {
-        ContactGeometry.contactInterval ci = cgeom.mapFor(y2 - ht,
-                y2 - ht + cgeom.pixels_step);
+        ContactGeometry.contactInterval ci = cgeom.mapFor(ht,
+                ht + cgeom.pixels_step);
         // cstart = (int) Math.floor(((double) y2 - ht) * contacts_per_pixel);
         // cend = (int) Math.min(contact_height,
         // Math.ceil(cstart + contacts_per_pixel * pixels_step));
@@ -216,11 +217,11 @@ public abstract class ContactMapRenderer implements AnnotationRowRendererI
         g.setColor(col);
         if (cgeom.pixels_step > 1)
         {
-          g.fillRect(x * charWidth, ht, charWidth, 1 + cgeom.pixels_step);
+          g.fillRect(x * charWidth, botY+ht, charWidth, 1 + cgeom.pixels_step);
         }
         else
         {
-          g.drawLine(x * charWidth, ht, (x + 1) * charWidth, ht);
+          g.drawLine(x * charWidth, botY+ht, (x + 1) * charWidth, botY+ht);
         }
       }
       x++;
index 6734735..b728c9d 100644 (file)
@@ -68,7 +68,18 @@ public class ColorUtils
     return color;
 
   }
+  
+  /**
+   * 
+   * @return random color
+   */
+  public static final Color getARandomColor()
+  {
 
+    Color col = new Color((int) (Math.random() * 255),
+            (int) (Math.random() * 255), (int) (Math.random() * 255));
+    return col;
+  }
   /**
    * Convert to Tk colour code format
    * 
index 9fea705..e9ff931 100644 (file)
@@ -337,9 +337,9 @@ public class Comparison
         // a long sequence.
         // check for at least 55% nucleotide, and nucleotide and ambiguity codes
         // (including N) must make up 95%
-        return ntCount * 100 > NUCLEOTIDE_COUNT_PERCENT * allCount
+        return ntCount * 100 >= NUCLEOTIDE_COUNT_PERCENT * allCount
                 && 100 * (ntCount + nCount
-                        + ntaCount) > NUCLEOTIDE_COUNT_LONG_SEQUENCE_AMBIGUITY_PERCENT
+                        + ntaCount) >= NUCLEOTIDE_COUNT_LONG_SEQUENCE_AMBIGUITY_PERCENT
                                 * allCount;
       }
       else if (allCount > NUCLEOTIDE_COUNT_VERY_SHORT_SEQUENCE)
@@ -347,7 +347,7 @@ public class Comparison
         // a short sequence.
         // check if a short sequence is at least 55% nucleotide and the rest of
         // the symbols are all X or all N
-        if (ntCount * 100 > NUCLEOTIDE_COUNT_PERCENT * allCount
+        if (ntCount * 100 >= NUCLEOTIDE_COUNT_PERCENT * allCount
                 && (nCount == aaCount || xCount == aaCount))
         {
           return true;
index 0454cab..8379777 100644 (file)
@@ -25,10 +25,11 @@ import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.ProtocolException;
 import java.net.URL;
-import java.util.List;
 
 import javax.ws.rs.HttpMethod;
 
+import jalview.bin.Cache;
+
 public class HttpUtils
 {
 
@@ -101,4 +102,46 @@ public class HttpUtils
     return connection.getResponseCode() == 200;
   }
 
+  public static String getUserAgent()
+  {
+    return getUserAgent(null);
+  }
+
+  public static String getUserAgent(String className)
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Jalview");
+    sb.append('/');
+    sb.append(Cache.getDefault("VERSION", "Unknown"));
+    sb.append(" (");
+    sb.append(System.getProperty("os.name"));
+    sb.append("; ");
+    sb.append(System.getProperty("os.arch"));
+    sb.append(' ');
+    sb.append(System.getProperty("os.name"));
+    sb.append(' ');
+    sb.append(System.getProperty("os.version"));
+    sb.append("; ");
+    sb.append("java/");
+    sb.append(System.getProperty("java.version"));
+    sb.append("; ");
+    sb.append("jalview/");
+    sb.append(ChannelProperties.getProperty("channel"));
+    if (className != null)
+    {
+      sb.append("; ");
+      sb.append(className);
+    }
+    String installation = Cache.applicationProperties
+            .getProperty("INSTALLATION");
+    if (installation != null)
+    {
+      sb.append("; ");
+      sb.append(installation);
+    }
+    sb.append(')');
+    sb.append(" help@jalview.org");
+    return sb.toString();
+  }
+
 }
index 1c67c92..7cbbe1c 100644 (file)
@@ -586,6 +586,15 @@ public class StringUtils
     return min < text.length() + 1 ? min : -1;
   }
 
+  public static boolean equalsIgnoreCase(String s1, String s2)
+  {
+    if (s1 == null || s2 == null)
+    {
+      return s1 == s2;
+    }
+    return s1.toLowerCase(Locale.ROOT).equals(s2.toLowerCase(Locale.ROOT));
+  }
+
   public static int indexOfFirstWhitespace(String text)
   {
     int index = -1;
index a8c93cf..33cb9dc 100644 (file)
@@ -2132,7 +2132,7 @@ public abstract class AlignmentViewport
 
         if (aa.graph > 0)
         {
-          aa.height += aa.graphHeight;
+          aa.height += aa.graphHeight+20;
         }
 
         if (aa.height == 0)
index 08ef3a2..fc28e53 100644 (file)
@@ -25,6 +25,7 @@ import jalview.api.AlignCalcWorkerI;
 import jalview.datamodel.AlignmentAnnotation;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Hashtable;
@@ -200,11 +201,29 @@ public class AlignCalcManager implements AlignCalcManagerI
   @Override
   public boolean isWorking()
   {
+    boolean working=false;
     synchronized (inProgress)
     {
       // System.err.println("isWorking "+hashCode());
-      return inProgress.size() > 0;
+      working |= inProgress.size() > 0;
     }
+    synchronized (updating)
+    {
+      Collection<List<AlignCalcWorkerI>> workersLists = updating.values();
+      synchronized (workersLists)
+      {
+        for (List<AlignCalcWorkerI> workers : workersLists)
+        {
+          if (workers!=null)
+          {
+            synchronized (workers) {
+              working |= workers.size() > 0;
+            }
+          }
+        }
+      }
+    }
+    return working;
   }
 
   @Override
index c822ef4..22884f1 100644 (file)
@@ -21,6 +21,21 @@ import jalview.util.MapList;
 import jalview.util.MapUtils;
 import jalview.ws.dbsources.EBIAlfaFold;
 
+/**
+ * routines and class for holding predicted alignment error matrices as produced
+ * by alphafold et al.
+ * 
+ * getContactList(column) returns the vector of predicted alignment errors for
+ * reference position given by column getElementAt(column, i) returns the
+ * predicted superposition error for the ith position when column is used as
+ * reference
+ * 
+ * Many thanks to Ora Schueler Furman for noticing that earlier development
+ * versions did not show the PAE oriented correctly
+ *
+ * @author jprocter
+ *
+ */
 public class PAEContactMatrix extends
         MappableContactMatrix<PAEContactMatrix> implements ContactMatrixI
 {
@@ -129,17 +144,18 @@ public class PAEContactMatrix extends
         Object d = scores.next();
         if (d instanceof Double)
         {
-          elements[row][col++] = ((Double) d).longValue();
+          elements[col][row] = ((Double) d).longValue();
         }
         else
         {
-          elements[row][col++] = (float) ((Long) d).longValue();
+          elements[col][row] = (float) ((Long) d).longValue();
         }
 
-        if (maxscore < elements[row][col - 1])
+        if (maxscore < elements[col][row])
         {
-          maxscore = elements[row][col - 1];
+          maxscore = elements[col][row];
         }
+        col++;
       }
       row++;
       col = 0;
@@ -180,7 +196,7 @@ public class PAEContactMatrix extends
     cols = ((List<Long>) pae_obj.get("residue2")).iterator();
     Iterator<Double> scores = ((List<Double>) pae_obj.get("distance"))
             .iterator();
-    elements = new float[maxrow][maxcol];
+    elements = new float[maxcol][maxrow];
     while (scores.hasNext())
     {
       float escore = scores.next().floatValue();
@@ -194,13 +210,17 @@ public class PAEContactMatrix extends
       {
         maxcol = col;
       }
-      elements[row - 1][col - 1] = escore;
+      elements[col - 1][row-1] = escore;
     }
 
     maxscore = ((Double) MapUtils.getFirst(pae_obj,
             "max_predicted_aligned_error", "max_pae")).floatValue();
   }
 
+  /**
+   * getContactList(column) @returns the vector of predicted alignment errors
+   * for reference position given by column
+   */
   @Override
   public ContactListI getContactList(final int column)
   {
@@ -235,6 +255,10 @@ public class PAEContactMatrix extends
     });
   }
 
+  /**
+   * getElementAt(column, i) @returns the predicted superposition error for the
+   * ith position when column is used as reference
+   */
   @Override
   protected double getElementAt(int _column, int i)
   {
index 0c707e5..24526cf 100644 (file)
@@ -57,6 +57,7 @@ import jalview.analysis.scoremodels.ScoreMatrix;
 import jalview.analysis.scoremodels.ScoreModels;
 import jalview.api.DBRefEntryI;
 import jalview.api.SiftsClientI;
+import jalview.bin.Console;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.SequenceI;
@@ -116,7 +117,7 @@ public class SiftsClient implements SiftsClientI
 
   private static final String NOT_OBSERVED = "Not_Observed";
 
-  private static final String SIFTS_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
+  private static final String SIFTS_SPLIT_FTP_BASE_URL = "https://ftp.ebi.ac.uk/pub/databases/msd/sifts/split_xml/";
 
   private final static String NEWLINE = System.lineSeparator();
 
@@ -305,7 +306,7 @@ public class SiftsClient implements SiftsClientI
       pdbId = pdbId.replace(".cif", "");
     }
     String siftFile = pdbId + ".xml.gz";
-    String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
+    String siftsFileFTPURL = getDownloadUrlFor(siftFile);
 
     /*
      * Download the file from URL to either
@@ -348,6 +349,14 @@ public class SiftsClient implements SiftsClientI
     return downloadTo;
   }
 
+  public static String getDownloadUrlFor(String siftFile)
+  {
+    String durl = SIFTS_SPLIT_FTP_BASE_URL+siftFile.substring(1, 3)+"/"+siftFile;
+    Console.trace("SIFTS URL for "+siftFile+" is "+durl);
+    return durl;
+    
+  }
+
   /**
    * Delete the SIFTs file for the given PDB Id in the local SIFTs download
    * directory
@@ -535,7 +544,7 @@ public class SiftsClient implements SiftsClientI
 
     if (mapping.isEmpty())
     {
-      throw new SiftsException("SIFTS mapping failed");
+      throw new SiftsException("SIFTS mapping failed for "+entityId+" and "+seq.getName());
     }
     // also construct a mapping object between the seq-coord sys and the PDB
     // seq's coord sys
index 5a8361d..760e0ba 100644 (file)
@@ -67,7 +67,7 @@ public class AverageDistanceEngineTest
             + matrix.getMin());
     long start = System.currentTimeMillis();
     AverageDistanceEngine clusterer = new AverageDistanceEngine(
-            af.getViewport(), null, matrix);
+            af.getViewport(), null, matrix, false);
     System.out.println("built a tree in "
             + (System.currentTimeMillis() - start) * 0.001 + " seconds.");
     StringBuffer sb = new StringBuffer();
index 5134511..44bea81 100644 (file)
@@ -29,11 +29,15 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
 import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.util.Locale;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import com.google.common.base.Charsets;
+
 /**
  * Test the alignment -> Mapping routines
  * 
@@ -83,16 +87,44 @@ public class TestAlignSeq
     assertEquals(as.getAStr1(), as.getAStr2());
 
     Mapping s1tos2 = as.getMappingFromS1(false);
+    checkMapping(s1tos2,s1,s2);
+  }
+
+  public void checkMapping(Mapping s1tos2,SequenceI _s1,SequenceI _s2)
+  {
     System.out.println(s1tos2.getMap().toString());
-    for (int i = s2.getStart(); i < s2.getEnd(); i++)
+    for (int i = _s2.getStart(); i < _s2.getEnd(); i++)
     {
-      System.out.println("Position in s2: " + i
-              + " maps to position in s1: " + s1tos2.getPosition(i));
-      // TODO fails: getCharAt doesn't allow for the start position??
-      // assertEquals(String.valueOf(s2.getCharAt(i)),
-      // String.valueOf(s1.getCharAt(s1tos2.getPosition(i))));
+      int p=s1tos2.getPosition(i);
+      char s2c=_s2.getCharAt(i-_s2.getStart());
+      char s1c=_s1.getCharAt(p-_s1.getStart());
+      System.out.println("Position in s2: " + i +s2c 
+      + " maps to position in s1: " +p+s1c);
+      assertEquals(s1c,s2c);
     }
   }
+  @Test(groups = { "Functional" })
+  /**
+   * simple test that mapping from alignment corresponds identical positions.
+   */
+  public void testGetMappingForS1_withLowerCase()
+  {
+    // make one of the sequences lower case
+    SequenceI ns2 = new Sequence(s2);
+    ns2.replace('D', 'd');
+    AlignSeq as = AlignSeq.doGlobalNWAlignment(s1, ns2, AlignSeq.PEP);
+    System.out.println("s1: " + as.getAStr1());
+    System.out.println("s2: " + as.getAStr2());
+
+    // aligned results match
+    assertEquals("ASDFA", as.getAStr1());
+    assertEquals(as.getAStr1(), as.getAStr2().toUpperCase(Locale.ROOT));
+
+    Mapping s1tos2 = as.getMappingFromS1(false);
+    assertEquals("ASdFA",as.getAStr2());
+    // verify mapping is consistent between original all-caps sequences
+    checkMapping(s1tos2,s1,s2);
+  }
 
   @Test(groups = { "Functional" })
   public void testExtractGaps()
index 049e9a8..30a1b9f 100644 (file)
@@ -333,9 +333,8 @@ public class CommandLineOperationsNG
   public void testAllInputOperations(String expectedString,
           String failureMsg)
   {
-    if ("[TESTOUTPUT] arg --nousagestats was set".equals(expectedString))
-      Assert.assertTrue(successfulCMDs.contains(expectedString),
-              failureMsg);
+    Assert.assertTrue(successfulCMDs.contains(expectedString),
+            failureMsg + "; was expecting '" + expectedString + "'");
   }
 
   @Test(
@@ -352,7 +351,8 @@ public class CommandLineOperationsNG
     file.deleteOnExit();
     Worker worker = getJalviewDesktopRunner(withAWT, cmd, timeout);
     assertNotNull(worker, "worker is null");
-    String msg = "Didn't create an output" + type + " file.[" + cmd + "]";
+    String msg = "Didn't create an output" + type + " file '" + fileName
+            + "'. [" + cmd + "]";
     assertTrue(file.exists(), msg);
     FileAssert.assertFile(file, msg);
     FileAssert.assertMinLength(file, expectedMinFileSize);
index b4c927b..73a0241 100644 (file)
@@ -19,7 +19,6 @@ import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
 import jalview.util.ArrayUtils;
 
-@Test
 public class CommandsTest
 {
   private static final String testfiles = "test/jalview/bin/argparser/testfiles";
@@ -49,8 +48,15 @@ public class CommandsTest
   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
-    if (Desktop.instance != null)
-      Desktop.instance.closeAll_actionPerformed(null);
+    Desktop.closeDesktop();
+  }
+  
+  public static void callJalviewMain(String[] args) {
+    if (Jalview.getInstance()!=null) {
+      Jalview.getInstance().doMain(args);
+    } else {
+      Jalview.main(args);
+    }
   }
 
   /* --setprops is currently disabled so this test won't work
@@ -70,67 +76,86 @@ public class CommandsTest
   public void commandsOpenTest(String cmdLine, boolean cmdArgs,
           int numFrames, String[] sequences)
   {
-    String[] args = (cmdLine + " --gui").split("\\s+");
-    Jalview.main(args);
-    Commands cmds = Jalview.getInstance().getCommands();
-    Assert.assertNotNull(cmds);
-    Assert.assertEquals(cmds.commandArgsProvided(), cmdArgs,
-            "Commands were not provided in the args");
-    Assert.assertEquals(cmds.argsWereParsed(), cmdArgs,
-            "Overall command parse and operation is false");
+    try
+    {
+      String[] args = (cmdLine + " --gui").split("\\s+");
+      callJalviewMain(args);
+      Commands cmds = Jalview.getInstance().getCommands();
+      Assert.assertNotNull(cmds);
+      Assert.assertEquals(cmds.commandArgsProvided(), cmdArgs,
+              "Commands were not provided in the args");
+      Assert.assertEquals(cmds.argsWereParsed(), cmdArgs,
+              "Overall command parse and operation is false");
 
-    Assert.assertEquals(Desktop.getAlignFrames().length, numFrames,
-            "Wrong number of AlignFrames");
+      Assert.assertEquals(Desktop.getAlignFrames().length, numFrames,
+              "Wrong number of AlignFrames");
 
-    if (sequences != null)
-    {
-      Set<String> openedSequenceNames = new HashSet<>();
-      AlignFrame[] afs = Desktop.getAlignFrames();
-      for (AlignFrame af : afs)
-      {
-        openedSequenceNames
-                .addAll(af.getViewport().getAlignment().getSequenceNames());
-      }
-      for (String sequence : sequences)
+      if (sequences != null)
       {
-        Assert.assertTrue(openedSequenceNames.contains(sequence),
-                "Sequence '" + sequence
-                        + "' was not found in opened alignment files: "
-                        + cmdLine + ".\nOpened sequence names are:\n"
-                        + String.join("\n", openedSequenceNames));
+        Set<String> openedSequenceNames = new HashSet<>();
+        AlignFrame[] afs = Desktop.getAlignFrames();
+        for (AlignFrame af : afs)
+        {
+          openedSequenceNames.addAll(
+                  af.getViewport().getAlignment().getSequenceNames());
+        }
+        for (String sequence : sequences)
+        {
+          Assert.assertTrue(openedSequenceNames.contains(sequence),
+                  "Sequence '" + sequence
+                          + "' was not found in opened alignment files: "
+                          + cmdLine + ".\nOpened sequence names are:\n"
+                          + String.join("\n", openedSequenceNames));
+        }
       }
-    }
 
-    Assert.assertFalse(
-            lookForSequenceName("THIS_SEQUENCE_ID_DOESN'T_EXIST"));
+      Assert.assertFalse(
+              lookForSequenceName("THIS_SEQUENCE_ID_DOESN'T_EXIST"));
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during commandsOpenTest", x);
+    } finally
+    {
+      tearDown();
+
+    }
   }
 
-  @Test(groups = "Functional", dataProvider = "structureImageOutputFiles")
+  @Test(groups = {"Functional","testTask1"}, dataProvider = "structureImageOutputFiles")
   public void structureImageOutputTest(String cmdLine, String[] filenames)
           throws IOException
   {
     cleanupFiles(filenames);
     String[] args = (cmdLine + " --gui").split("\\s+");
-    Jalview.main(args);
-    Commands cmds = Jalview.getInstance().getCommands();
-    Assert.assertNotNull(cmds);
-    File lastFile = null;
-    for (String filename : filenames)
+    try
     {
-      File file = new File(filename);
-      Assert.assertTrue(file.exists(), "File '" + filename
-              + "' was not created by '" + cmdLine + "'");
-      Assert.assertTrue(file.isFile(), "File '" + filename
-              + "' is not a file from '" + cmdLine + "'");
-      Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
-              + "' has no content from '" + cmdLine + "'");
-      // make sure the successive output files get bigger!
-      if (lastFile != null)
-        Assert.assertTrue(
-                Files.size(file.toPath()) > Files.size(lastFile.toPath()));
+      callJalviewMain(args);
+      Commands cmds = Jalview.getInstance().getCommands();
+      Assert.assertNotNull(cmds);
+      File lastFile = null;
+      for (String filename : filenames)
+      {
+        File file = new File(filename);
+        Assert.assertTrue(file.exists(), "File '" + filename
+                + "' was not created by '" + cmdLine + "'");
+        Assert.assertTrue(file.isFile(), "File '" + filename
+                + "' is not a file from '" + cmdLine + "'");
+        Assert.assertTrue(Files.size(file.toPath()) > 0, "File '" + filename
+                + "' has no content from '" + cmdLine + "'");
+        // make sure the successive output files get bigger!
+        if (lastFile != null)
+          Assert.assertTrue(Files.size(file.toPath()) > Files
+                  .size(lastFile.toPath()));
+      }
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during structureImageOutputTest",
+              x);
+    } finally
+    {
+      cleanupFiles(filenames);
+      tearDown();
     }
-    cleanupFiles(filenames);
-    tearDown();
   }
 
   @Test(groups = "Functional", dataProvider = "argfileOutputFiles")
@@ -139,7 +164,8 @@ public class CommandsTest
   {
     cleanupFiles(filenames);
     String[] args = (cmdLine + " --gui").split("\\s+");
-    Jalview.main(args);
+    try {
+    callJalviewMain(args);
     Commands cmds = Jalview.getInstance().getCommands();
     Assert.assertNotNull(cmds);
     File lastFile = null;
@@ -157,8 +183,15 @@ public class CommandsTest
         Assert.assertTrue(
                 Files.size(file.toPath()) > Files.size(lastFile.toPath()));
     }
-    cleanupFiles(filenames);
-    tearDown();
+    } catch (Exception x)
+    {
+      Assert.fail("Unexpected exception during argFilesGlobAndSubstitutions",
+              x);
+    } finally
+    {
+      cleanupFiles(filenames);
+      tearDown();
+    }
   }
 
   @DataProvider(name = "structureImageOutputFiles")
@@ -297,7 +330,7 @@ public class CommandsTest
           String[] nonfilenames)
   {
     String[] args = (cmdLine + " --gui").split("\\s+");
-    Jalview.main(args);
+    callJalviewMain(args);
     Commands cmds = Jalview.getInstance().getCommands();
     Assert.assertNotNull(cmds);
     for (String filename : filenames)
index 0a47700..d6b6f3c 100644 (file)
@@ -51,8 +51,7 @@ public class CommandsTest2
   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
-    if (Desktop.instance != null)
-      Desktop.instance.closeAll_actionPerformed(null);
+    Desktop.closeDesktop();
   }
 
   @Test(
@@ -65,15 +64,19 @@ public class CommandsTest2
   {
     String[] args = cmdLine.split("\\s+");
 
-    Jalview.main(args);
-    try
-    {
-      // sleep for slow build server to open annotations and viewer windows
-      Thread.sleep(seqNum * 50 + annNum * 50 + viewerNum * 500);
-    } catch (InterruptedException e)
+    CommandsTest.callJalviewMain(args);
+    while (Desktop.instance!=null && Desktop.instance.operationsAreInProgress())
     {
-      e.printStackTrace();
+      try
+      {
+        // sleep for slow build server to open annotations and viewer windows
+        Thread.sleep(viewerNum * 50);
+      } catch (InterruptedException e)
+      {
+        e.printStackTrace();
+      }
     }
+    ;
 
     AlignFrame[] afs = Desktop.getAlignFrames();
     Assert.assertNotNull(afs);
index 67edb37..bdfa5a3 100644 (file)
@@ -73,7 +73,7 @@ public class PAEContactMatrixTest
     verifyPAEmatrix(seq, aa, 0, 0, 4);
 
     // test clustering
-    paematrix.setGroupSet(GroupSet.makeGroups(paematrix, 0.1f, false));
+    paematrix.setGroupSet(GroupSet.makeGroups(paematrix, false,0.1f, false));
 
     // remap - test the MappableContactMatrix.liftOver method
     SequenceI newseq = new Sequence("Seq", "ASDQEASDQEASDQE");
index 344d74d..7bbb9f3 100644 (file)
@@ -2315,8 +2315,16 @@ public class SequenceTest
   {
     Sequence origSeq = new Sequence("MYSEQ", "THISISASEQ");
     Sequence toSeq = new Sequence("MYSEQ", "THISISASEQ");
+    origSeq.setDescription("DESCRIPTION");
     origSeq.addDBRef(new DBRefEntry("UNIPROT", "0", "Q12345", null, true));
+
+    toSeq.transferAnnotation(origSeq, null);
+    assertEquals("DESCRIPTION",toSeq.getDescription());
+    toSeq = new Sequence("MYSEQ", "THISISASEQ");
+    toSeq.setDescription("unchanged");
     toSeq.transferAnnotation(origSeq, null);
+    assertEquals("unchanged",toSeq.getDescription());
+    
     assertTrue(toSeq.getDBRefs().size() == 1);
 
     assertTrue(toSeq.getDBRefs().get(0).isCanonical());
index 65cea6f..d2b6f65 100644 (file)
  */
 package jalview.io;
 
+import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
 import java.io.File;
+import java.io.IOException;
 
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import fr.orsay.lri.varna.utils.RNAMLParser;
+import groovy.lang.Sequence;
+
 public class RNAMLfileTest
 {
 
@@ -57,4 +65,14 @@ public class RNAMLfileTest
 
   }
 
+  @Test(groups= {"Functional"})
+  public void testRnamlSeqImport() throws IOException
+  {
+    RnamlFile parser = new RnamlFile("examples/testdata/7WKP-rna1.xml", DataSourceType.FILE);
+    SequenceI[] seqs  = parser.getSeqsAsArray();
+    assertNotNull(seqs);
+    assertEquals(seqs.length,1);
+    assertEquals(seqs[0].getEnd()-seqs[0].getStart()+1,seqs[0].getSequence().length);
+  }
+
 }
index 9946f4c..288444e 100644 (file)
  */
 package jalview.io.cache;
 
+import java.lang.reflect.InvocationTargetException;
 import java.util.LinkedHashSet;
 
+import javax.swing.SwingUtilities;
+
 import org.junit.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -54,11 +57,19 @@ public class JvCacheableInputBoxTest
 
     try
     {
-      // This delay is essential to prevent the
-      // assertion below from executing before
-      // swing thread finishes updating the combo-box
-      Thread.sleep(350);
-    } catch (InterruptedException e)
+      // fix for JAL-4153
+      // This delay is essential to prevent the assertion below from executing
+      // before swing thread finishes updating the combo-box
+      SwingUtilities.invokeAndWait(() -> {
+        try
+        {
+          Thread.sleep(1);
+        } catch (InterruptedException e)
+        {
+          e.printStackTrace();
+        }
+      });
+    } catch (InvocationTargetException | InterruptedException e)
     {
       e.printStackTrace();
     }
@@ -73,12 +84,21 @@ public class JvCacheableInputBoxTest
     cacheBox.addItem(testInput);
     cacheBox.setSelectedItem(testInput);
     cacheBox.updateCache();
+
     try
     {
-      // This delay is to let
-      // cacheBox.updateCache() finish updating the cache
-      Thread.sleep(350);
-    } catch (InterruptedException e)
+      // fix for JAL-4153
+      // This delay is to let cacheBox.updateCache() finish updating the cache
+      SwingUtilities.invokeAndWait(() -> {
+        try
+        {
+          Thread.sleep(1);
+        } catch (InterruptedException e)
+        {
+          e.printStackTrace();
+        }
+      });
+    } catch (InvocationTargetException | InterruptedException e)
     {
       e.printStackTrace();
     }
index 37f946b..c9532cc 100644 (file)
@@ -1563,7 +1563,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       {
         paevals[i][j] = ((i - j < 2)
                 || ((i > 1 && i < 5) && (j > 1 && i < 5))) ? 1 : 0f;
-        paevals[j][i] = paevals[i][j];
+        paevals[j][i] = -paevals[i][j];
       }
     }
     PAEContactMatrix dummyMat = new PAEContactMatrix(sq, paevals);
@@ -1573,7 +1573,8 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     float[][] vals = ContactMatrix.fromFloatStringToContacts(content,
             sq.getLength(), sq.getLength());
     assertEquals(vals[3][4], paevals[3][4]);
-    dummyMat.setGroupSet(GroupSet.makeGroups(dummyMat, 0.5f, false));
+    assertEquals(vals[4][3], paevals[4][3]);
+    dummyMat.setGroupSet(GroupSet.makeGroups(dummyMat, false,0.5f, false));
     Assert.assertNotSame(dummyMat.getNewick(), "");
     AlignmentAnnotation paeCm = sq.addContactList(dummyMat);
     al.addAnnotation(paeCm);
index 44a6a02..0f5bd4d 100644 (file)
@@ -195,8 +195,10 @@ public class SiftsClientTest
     SiftsSettings.setCacheThresholdInDays("2");
     SiftsSettings.setFailSafePIDThreshold("70");
     PDBfile pdbFile;
+    
     pdbFile = new PDBfile(false, false, false,
             "test/jalview/io/" + testPDBId + ".pdb", DataSourceType.FILE);
+    // TODO: this uses a network connection - we should mock the sifts testPDBId.xml.gz
     siftsClient = new SiftsClient(pdbFile);
   }
 
@@ -205,6 +207,12 @@ public class SiftsClientTest
   {
     siftsClient = null;
   }
+  
+  @Test(groups= {"Functional"})
+  public void testSIFTsDownloadURL() {
+    String expectedUrl = "https://ftp.ebi.ac.uk/pub/databases/msd/sifts/split_xml/xy/1xyz.sifts.xml.gz";
+    Assert.assertEquals(SiftsClient.getDownloadUrlFor("1xyz.sifts.xml.gz"), expectedUrl);
+  }
 
   @Test(groups = { "Network" })
   public void getSIFTsFileTest() throws SiftsException, IOException
@@ -215,7 +223,7 @@ public class SiftsClientTest
     long t1 = siftsFile.lastModified();
 
     // re-read file should be returned from cache
-    siftsFile = SiftsClient.downloadSiftsFile(testPDBId);
+    siftsFile = SiftsClient.getSiftsFile(testPDBId);
     FileAssert.assertFile(siftsFile);
     long t2 = siftsFile.lastModified();
     assertEquals(t1, t2);
@@ -368,7 +376,7 @@ public class SiftsClientTest
   {
     SequenceI invalidTestSeq = new Sequence("testSeq", "ABCDEFGH");
     DBRefEntry invalidDBRef = new DBRefEntry();
-    invalidDBRef.setAccessionId("BLAR");
+    invalidDBRef.setAccessionId("BLAR"); // note no version is set, so also invalid
     invalidTestSeq.addDBRef(invalidDBRef);
     siftsClient.getValidSourceDBRef(invalidTestSeq);
   }
diff --git a/utils/biotools/Jalview.json b/utils/biotools/Jalview.json
new file mode 100644 (file)
index 0000000..69aa95c
--- /dev/null
@@ -0,0 +1,417 @@
+{
+  "name": "Jalview",
+  "description": "Jalview is a free program for multiple sequence alignment editing, visualisation and analysis. Use it to view and edit sequence alignments, analyse them with phylogenetic trees and principal components analysis (PCA) plots and explore molecular structures and annotation.",
+  "homepage": "https://www.jalview.org/",
+  "biotoolsID": "Jalview",
+  "biotoolsCURIE": "biotools:Jalview",
+  "version": [
+    "2.11.2.7"
+  ],
+  "relation": [
+    {
+      "biotoolsID": "jabaws",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "chimera",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "chimerax",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "pymol",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "bioconda",
+      "type": "includedIn"
+    },
+    {
+      "biotoolsID": "3d-beacons",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "uniprot",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "pfam",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "ensembl",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "pdb",
+      "type": "uses"
+    },
+    {
+      "biotoolsID": "rfam",
+      "type": "uses"
+    }
+  ],
+  "function": [
+    {
+      "operation": [
+        {
+          "uri": "http://edamontology.org/operation_0564",
+          "term": "Sequence visualisation"
+        },
+        {
+          "uri": "http://edamontology.org/operation_0324",
+          "term": "Phylogenetic tree analysis"
+        },
+        {
+          "uri": "http://edamontology.org/operation_3081",
+          "term": "Sequence alignment editing"
+        }
+      ],
+      "input": [
+        {
+          "data": {
+            "uri": "http://edamontology.org/data_0863",
+            "term": "Sequence alignment"
+          },
+          "format": [
+            {
+              "uri": "http://edamontology.org/format_1939",
+              "term": "GFF3-seq"
+            },
+            {
+              "uri": "http://edamontology.org/format_1982",
+              "term": "ClustalW format"
+            },
+            {
+              "uri": "http://edamontology.org/format_1961",
+              "term": "Stockholm format"
+            },
+            {
+              "uri": "http://edamontology.org/format_1984",
+              "term": "FASTA-aln"
+            },
+            {
+              "uri": "http://edamontology.org/format_1938",
+              "term": "GFF2-seq"
+            },
+            {
+              "uri": "http://edamontology.org/format_1929",
+              "term": "FASTA"
+            },
+            {
+              "uri": "http://edamontology.org/format_1948",
+              "term": "nbrf/pir"
+            },
+            {
+              "uri": "http://edamontology.org/format_3774",
+              "term": "BioJSON (Jalview)"
+            },
+            {
+              "uri": "http://edamontology.org/format_1997",
+              "term": "PHYLIP format"
+            },
+            {
+              "uri": "http://edamontology.org/format_3313",
+              "term": "BLC"
+            },
+            {
+              "uri": "http://edamontology.org/format_3311",
+              "term": "RNAML"
+            },
+            {
+              "uri": "http://edamontology.org/format_1947",
+              "term": "GCG MSF"
+            },
+            {
+              "uri": "http://edamontology.org/format_3015",
+              "term": "Pileup"
+            },
+            {
+              "uri": "http://edamontology.org/format_1477",
+              "term": "mmCIF"
+            },
+            {
+              "uri": "http://edamontology.org/format_3016",
+              "term": "VCF"
+            },
+            {
+              "uri": "http://edamontology.org/format_1915",
+              "term": "Format"
+            }
+          ]
+        },
+        {
+          "data": {
+            "uri": "http://edamontology.org/data_0886",
+            "term": "Structure alignment"
+          },
+          "format": [
+            {
+              "uri": "http://edamontology.org/format_1476",
+              "term": "PDB"
+            }
+          ]
+        }
+      ],
+      "output": [
+        {
+          "data": {
+            "uri": "http://edamontology.org/data_0863",
+            "term": "Sequence alignment"
+          },
+          "format": [
+            {
+              "uri": "http://edamontology.org/format_1948",
+              "term": "nbrf/pir"
+            },
+            {
+              "uri": "http://edamontology.org/format_3464",
+              "term": "JSON"
+            },
+            {
+              "uri": "http://edamontology.org/format_1961",
+              "term": "Stockholm format"
+            },
+            {
+              "uri": "http://edamontology.org/format_1929",
+              "term": "FASTA"
+            },
+            {
+              "uri": "http://edamontology.org/format_1997",
+              "term": "PHYLIP format"
+            },
+            {
+              "uri": "http://edamontology.org/format_3313",
+              "term": "BLC"
+            },
+            {
+              "uri": "http://edamontology.org/format_3774",
+              "term": "BioJSON (Jalview)"
+            },
+            {
+              "uri": "http://edamontology.org/format_1947",
+              "term": "GCG MSF"
+            },
+            {
+              "uri": "http://edamontology.org/format_3015",
+              "term": "Pileup"
+            },
+            {
+              "uri": "http://edamontology.org/format_1982",
+              "term": "ClustalW format"
+            }
+          ]
+        },
+        {
+          "data": {
+            "uri": "http://edamontology.org/data_2884",
+            "term": "Plot"
+          },
+          "format": [
+            {
+              "uri": "http://edamontology.org/format_3603",
+              "term": "PNG"
+            },
+            {
+              "uri": "http://edamontology.org/format_2331",
+              "term": "HTML"
+            },
+            {
+              "uri": "http://edamontology.org/format_3466",
+              "term": "EPS"
+            },
+            {
+              "uri": "http://edamontology.org/format_3604",
+              "term": "SVG"
+            },
+            {
+              "uri": "http://edamontology.org/format_1915",
+              "term": "Format"
+            }
+          ]
+        }
+      ],
+      "note": "Other Input formats:\nAMSA (.amsa);\nJnetFile (.concise, .jnet);\nPFAM (.pfam);\nSubstitution matrix (.matrix);\nJalview Project File (.jvp);\nJalview Feature File (.features, .jvfeatures);\nJalview Annotations File (.annotations, .jvannotations);\n\n...\nOther Output formats:\nPFAM (.pfam);\nBioJS (.biojs) (interactive HTML/Javascript);\nJalview Project File (.jvp);"
+    }
+  ],
+  "toolType": [
+    "Desktop application"
+  ],
+  "topic": [
+    {
+      "uri": "http://edamontology.org/topic_0080",
+      "term": "Sequence analysis"
+    },
+    {
+      "uri": "http://edamontology.org/topic_0092",
+      "term": "Data visualisation"
+    }
+  ],
+  "operatingSystem": [
+    "Linux",
+    "Windows",
+    "Mac"
+  ],
+  "license": "GPL-3.0",
+  "maturity": "Mature",
+  "cost": "Free of charge",
+  "accessibility": "Open access",
+  "elixirPlatform": [
+    "Tools"
+  ],
+  "elixirNode": [
+    "UK"
+  ],
+  "link": [
+    {
+      "url": "https://discourse.jalview.org/",
+      "type": [
+        "Discussion forum"
+      ]
+    },
+    {
+      "url": "https://issues.jalview.org/",
+      "type": [
+        "Issue tracker"
+      ]
+    },
+    {
+      "url": "https://www.jalview.org/development/jalview_develop/",
+      "type": [
+        "Other"
+      ],
+      "note": "Latest development version"
+    },
+    {
+      "url": "https://source.jalview.org/crucible/browse/jalview",
+      "type": [
+        "Repository"
+      ]
+    },
+    {
+      "url": "https://twitter.com/Jalview",
+      "type": [
+        "Social media"
+      ],
+      "note": "Twitter feed"
+    },
+    {
+      "url": "https://www.youtube.com/channel/UCIjpnvZB770yz7ftbrJ0tfw",
+      "type": [
+        "Social media"
+      ],
+      "note": "YouTube training videos"
+    }
+  ],
+  "download": [
+    {
+      "url": "https://www.jalview.org/download",
+      "type": "Downloads page"
+    },
+    {
+      "url": "https://www.jalview.org/download/source/",
+      "type": "Source code"
+    },
+    {
+      "url": "https://www.jalview.org/download/?os=all",
+      "type": "Binaries",
+      "note": "Binaries for all platforms"
+    },
+    {
+      "url": "https://www.jalview.org/favicon.svg",
+      "type": "Icon"
+    },
+    {
+      "url": "https://www.jalview.org/download/other/jar/",
+      "type": "Binaries",
+      "note": "Executable JAR file"
+    }
+  ],
+  "documentation": [
+    {
+      "url": "https://www.jalview.org/about/citation",
+      "type": [
+        "Citation instructions"
+      ]
+    },
+    {
+      "url": "https://www.jalview.org/training/",
+      "type": [
+        "Training material"
+      ],
+      "note": "Hands-on exercises, Training courses and Training videos"
+    },
+    {
+      "url": "https://www.jalview.org/help/faq",
+      "type": [
+        "FAQ"
+      ]
+    },
+    {
+      "url": "https://www.jalview.org/help/documentation/",
+      "type": [
+        "User manual"
+      ]
+    }
+  ],
+  "publication": [
+    {
+      "doi": "10.1093/bioinformatics/btp033",
+      "metadata": {
+        "title": "Jalview Version 2-A multiple sequence alignment editor and analysis workbench",
+        "abstract": "Summary: Jalview Version 2 is a system for interactive WYSIWYG editing, analysis and annotation of multiple sequence alignments. Core features include keyboard and mouse-based editing, multiple views and alignment overviews, and linked structure display with Jmol. Jalview 2 is available in two forms: a lightweight Java applet for use in web applications, and a powerful desktop application that employs web services for sequence alignment, secondary structure prediction and the retrieval of alignments, sequences, annotation and structures from public databases and any DAS 1.53 compliant sequence or annotation server. © 2009 The Author(s).",
+        "date": "2009-05-07T00:00:00Z",
+        "citationCount": 5999,
+        "authors": [
+          {
+            "name": "Waterhouse A.M."
+          },
+          {
+            "name": "Procter J.B."
+          },
+          {
+            "name": "Martin D.M.A."
+          },
+          {
+            "name": "Clamp M."
+          },
+          {
+            "name": "Barton G.J."
+          }
+        ],
+        "journal": "Bioinformatics"
+      }
+    }
+  ],
+  "credit": [
+    {
+      "name": "Jim Procter",
+      "url": "http://www.lifesci.dundee.ac.uk/people/jim-procter",
+      "orcidid": "https://orcid.org/0000-0002-7865-7382",
+      "typeEntity": "Person",
+      "typeRole": [
+        "Primary contact"
+      ]
+    },
+    {
+      "name": "Geoff Barton",
+      "url": "https://www.lifesci.dundee.ac.uk/people/geoff-barton",
+      "orcidid": "https://orcid.org/0000-0002-9014-5355"
+    }
+  ],
+  "owner": "ben_s",
+  "additionDate": "2019-02-13T17:01:40Z",
+  "lastUpdate": "2023-07-22T09:24:44.755337Z",
+  "editPermission": {
+    "type": "group",
+    "authors": [
+      "ben_s",
+      "jimprocter"
+    ]
+  },
+  "validated": 1,
+  "homepage_status": 0,
+  "elixir_badge": 0
+}
diff --git a/utils/biotools/README.md b/utils/biotools/README.md
new file mode 100644 (file)
index 0000000..948a751
--- /dev/null
@@ -0,0 +1,13 @@
+This is the JSON representation of the latest Jalview release's record on bio.tools
+
+To update:
+1. go to https://bio.tools/Jalview
+2. log in and scroll down to the 'Update Record' button to open the edit interface.
+3. Make any chances to the entry - press Validate to ensure all is good
+4. Select the JSON tab and copy paste into
+
+``
+cat > utils/biotools/Jalview.json
+``
+
+Thanks to Herve Menager for the tutorial on storing bio.tools records with the tool's software repository at [CoFest 2023](https://www.open-bio.org/events/bosc-2023/obf-bosc-collaborationfest-2023)
\ No newline at end of file
index 1527c79..4d3762e 100644 (file)
@@ -1,6 +1,8 @@
---- a/build.gradle     2021-09-21 09:52:04.653972716 +0100
-+++ b/build.gradle     2021-09-21 09:52:18.117985307 +0100
-@@ -2,56 +2,12 @@
+diff --git a/build.gradle b/build.gradle
+index ca599a85a..ce7f13634 100644
+--- a/build.gradle
++++ b/build.gradle
+@@ -2,66 +2,12 @@
   * For properties set within build.gradle, use camelCaseNoSpace.
   */
  import org.apache.tools.ant.filters.ReplaceTokens
 -import org.gradle.plugins.ide.eclipse.model.Output
 -import org.gradle.plugins.ide.eclipse.model.Library
 -import java.security.MessageDigest
+-import java.util.regex.Matcher
 -import groovy.transform.ExternalizeMethods
 -import groovy.util.XmlParser
 -import groovy.xml.XmlUtil
+-import groovy.json.JsonBuilder
 -import com.vladsch.flexmark.util.ast.Node
 -import com.vladsch.flexmark.html.HtmlRenderer
 -import com.vladsch.flexmark.parser.Parser
 -import com.vladsch.flexmark.ext.autolink.AutolinkExtension
 -import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
 -import com.vladsch.flexmark.ext.toc.TocExtension
+-import com.google.common.hash.HashCode
+-import com.google.common.hash.Hashing
+-import com.google.common.io.Files
+-import org.jsoup.Jsoup
+-import org.jsoup.nodes.Element
 -
 -buildscript {
 -  repositories {
@@ -32,6 +41,8 @@
 -  }
 -  dependencies {
 -    classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
+-    classpath "org.jsoup:jsoup:1.14.3"
+-    classpath "com.eowise:gradle-imagemagick:0.5.1"
 -  }
 -}
 -
@@ -42,9 +53,9 @@
 -  id 'eclipse'
 -  id "com.diffplug.gradle.spotless" version "3.28.0"
 -  id 'com.github.johnrengelman.shadow' version '4.0.3'
--  id 'com.install4j.gradle' version '8.0.10'
--  id 'com.dorongold.task-tree' version '1.5' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
--  id 'com.palantir.git-version' version '0.12.3'
+-  id 'com.install4j.gradle' version '10.0.3'
+-  id 'com.dorongold.task-tree' version '2.1.1' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
+-  id 'com.palantir.git-version' version '0.13.0' apply false
 -}
 -
 -repositories {
  }
  
 -
+-
  // in ext the values are cast to Object. Ensure string values are cast as String (and not GStringImpl) for later use
  def string(Object o) {
    return o == null ? "" : o.toString()
-@@ -92,23 +48,15 @@
+@@ -102,34 +48,20 @@ def overrideProperties(String propsFileName, boolean output = false) {
    }
  }
  
 +project.ext {
    jalviewDirAbsolutePath = file(jalviewDir).getAbsolutePath()
    jalviewDirRelativePath = jalviewDir
+-  date = new Date()
  
 -  getdownChannelName = CHANNEL.toLowerCase()
 -  // default to "default". Currently only has different cosmetics for "develop", "release", "default"
 -  propertiesChannelName = ["develop", "release", "test-release", "jalviewjs", "jalviewjs-release" ].contains(getdownChannelName) ? getdownChannelName : "default"
+-  channelDirName = propertiesChannelName
 -  // Import channel_properties
+-  if (getdownChannelName.startsWith("develop-")) {
+-    channelDirName = "develop-SUFFIX"
+-  }
+-  channelDir = string("${jalviewDir}/${channel_properties_dir}/${channelDirName}")
 +  propertiesChannelName = "release"
-   channelDir = string("${jalviewDir}/${channel_properties_dir}/${propertiesChannelName}")
++  channelDir = string("${jalviewDir}/${channel_properties_dir}/${propertiesChannelName}")
    channelGradleProperties = string("${channelDir}/channel_gradle.properties")
+-  channelPropsFile = string("${channelDir}/${resource_dir}/${channel_props}")
    overrideProperties(channelGradleProperties, false)
 -  // local build environment properties
 -  // can be "projectDir/local.properties"
    ////  
    // Import releaseProps from the RELEASE file
    // or a file specified via JALVIEW_RELEASE_FILE if defined
-@@ -128,41 +76,6 @@
+   // Expect jalview.version and target release branch in jalview.release        
+-  releaseProps = new Properties();
++  def releaseProps = new Properties();
+   def releasePropFile = findProperty("JALVIEW_RELEASE_FILE");
+   def defaultReleasePropFile = "${jalviewDirAbsolutePath}/RELEASE";
+   try {
+@@ -144,42 +76,6 @@ ext {
    if (findProperty("JALVIEW_VERSION")==null || "".equals(JALVIEW_VERSION)) {
      JALVIEW_VERSION = releaseProps.get("jalview.version")
    }
+-  println("JALVIEW_VERSION is set to '${JALVIEW_VERSION}'")
 -  
 -  // this property set when running Eclipse headlessly
 -  j2sHeadlessBuildProperty = string("net.sf.j2s.core.headlessbuild")
  
    // essentials
    bareSourceDir = string(source_dir)
-@@ -173,218 +86,18 @@
+@@ -190,273 +86,18 @@ ext {
  
    classesDir = string("${jalviewDir}/${classes_dir}")
  
 -  cloverTestClassesDir = file("${cloverBuildDir}/clover-test-classes")
 -  //cloverTestClassesDir = cloverClassesDir
 -  cloverDb = string("${cloverBuildDir}/clover.db")
--
++  useClover = false
 -  testSourceDir = useClover ? cloverTestInstrDir : testDir
 -  testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
--
--  getdownWebsiteDir = string("${jalviewDir}/${getdown_website_dir}/${JAVA_VERSION}")
++  resourceClassesDir = classesDir
+-  channelSuffix = ""
+-  backgroundImageText = BACKGROUNDIMAGETEXT
+-  getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
+-  getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
+-  getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
+-  getdownFullArchiveDir = null
+-  getdownTextLines = []
+-  getdownLaunchJvl = null
+-  getdownVersionLaunchJvl = null
 -  buildDist = true
 -  buildProperties = null
--
++  testSourceDir = testDir
++  testClassesDir = "${jalviewDir}/${test_output_dir}"
 -  // the following values might be overridden by the CHANNEL switch
 -  getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
 -  getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
+-  getdownArchiveAppBase = getdown_archive_base
 -  getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
 -  getdownAppDistDir = getdown_app_dir_alt
 -  getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
--  getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
+-  getdownImagesBuildDir = string("${buildDir}/imagemagick/getdown")
++  buildProperties = string("${classesDir}/${build_properties_file}")
+   getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
 -  reportRsyncCommand = false
 -  jvlChannelName = CHANNEL.toLowerCase()
 -  install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build
 -  install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
--  install4jDMGBackgroundImage = "${install4j_images_dir}/${install4j_dmg_background}"
+-  install4jDMGBackgroundImageDir = "${install4j_images_dir}"
+-  install4jDMGBackgroundImageBuildDir = "build/imagemagick/install4j"
+-  install4jDMGBackgroundImageFile = "${install4j_dmg_background}"
 -  install4jInstallerName = "${jalview_name} Non-Release Installer"
 -  install4jExecutableName = install4j_executable_name
 -  install4jExtraScheme = "jalviewx"
 -  install4jWindowsIconsFile = string("${install4j_images_dir}/${install4j_windows_icons_file}")
 -  install4jPngIconFile = string("${install4j_images_dir}/${install4j_png_icon_file}")
 -  install4jBackground = string("${install4j_images_dir}/${install4j_background}")
+-  install4jBuildDir = "${install4j_build_dir}/${JAVA_VERSION}"
+-  install4jCheckSums = true
+-
+-  applicationName = "${jalview_name}"
 -  switch (CHANNEL) {
 -
 -    case "BUILD":
 -      testng_excluded_groups = "Not-bamboo"
 -    }
 -    install4jExtraScheme = "jalviewb"
+-    backgroundImageText = true
 -    break
-+  useClover = false
+-
 -    case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
 -    getdownAppDistDir = getdown_app_dir_release
+-    getdownSetAppBaseProperty = true
 -    reportRsyncCommand = true
 -    install4jSuffix = ""
 -    install4jInstallerName = "${jalview_name} Installer"
 -    case "ARCHIVELOCAL":
 -    getdownChannelName = string("archive/${JALVIEW_VERSION}")
 -    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
--    getdownAppBase = file(getdownWebsiteDir).toURI().toString()
+-    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
 -    if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
--      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
+-      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution [did not find '${ARCHIVEDIR}/${package_dir}']")
 -    } else {
 -      package_dir = string("${ARCHIVEDIR}/${package_dir}")
 -      buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
 -    install4jExtraScheme = "jalviewa"
 -    break
 -
+-    case ~/^DEVELOP-([\.\-\w]*)$/:
+-    def suffix = Matcher.lastMatcher[0][1]
+-    reportRsyncCommand = true
+-    getdownSetAppBaseProperty = true
+-    JALVIEW_VERSION=JALVIEW_VERSION+"-d${suffix}-${buildDate}"
+-    install4jSuffix = "Develop ${suffix}"
+-    install4jExtraScheme = "jalviewd"
+-    install4jInstallerName = "${jalview_name} Develop ${suffix} Installer"
+-    getdownChannelName = string("develop-${suffix}")
+-    getdownChannelDir = string("${getdown_website_dir}/${getdownChannelName}")
+-    getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
+-    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
+-    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
+-    channelSuffix = string(suffix)
+-    backgroundImageText = true
+-    break
+-
 -    case "DEVELOP":
 -    reportRsyncCommand = true
 -    getdownSetAppBaseProperty = true
 -    install4jSuffix = "Develop"
 -    install4jExtraScheme = "jalviewd"
 -    install4jInstallerName = "${jalview_name} Develop Installer"
+-    backgroundImageText = true
 -    break
 -
 -    case "TEST-RELEASE":
 -    reportRsyncCommand = true
+-    getdownSetAppBaseProperty = true
 -    // Don't ignore transpile errors for release build
 -    if (jalviewjs_ignore_transpile_errors.equals("true")) {
 -      jalviewjs_ignore_transpile_errors = "false"
 -    install4jSuffix = "Test"
 -    install4jExtraScheme = "jalviewt"
 -    install4jInstallerName = "${jalview_name} Test Installer"
+-    backgroundImageText = true
 -    break
 -
 -    case ~/^SCRATCH(|-[-\w]*)$/:
 -    install4jSuffix = "Test-Local"
 -    install4jExtraScheme = "jalviewt"
 -    install4jInstallerName = "${jalview_name} Test Installer"
+-    backgroundImageText = true
 -    break
 -
 -    case [ "LOCAL", "JALVIEWJS" ]:
 -    JALVIEW_VERSION = "TEST"
--    getdownAppBase = file(getdownWebsiteDir).toURI().toString()
+-    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
+-    getdownArchiveAppBase = file("${jalviewDir}/${getdown_archive_dir}").toURI().toString()
 -    getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
 -    install4jExtraScheme = "jalviewl"
+-    install4jCheckSums = false
 -    break
 -
 -    default: // something wrong specified
 -    break
 -
 -  }
+-  JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
+-  hugoDataJsonFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_data_installers_dir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
+-  hugoArchiveMdFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_version_archive_dir}/Version-${JALVIEW_VERSION_UNDERSCORES}/_index.md")
 -  // override getdownAppBase if requested
 -  if (findProperty("getdown_appbase_override") != null) {
 -    // revert to LOCAL if empty string
 -    if (string(getdown_appbase_override) == "") {
--      getdownAppBase = file(getdownWebsiteDir).toURI().toString()
+-      getdownAppBase = file(getdownAppBaseDir).toURI().toString()
 -      getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
 -    } else if (string(getdown_appbase_override).startsWith("file://")) {
 -      getdownAppBase = string(getdown_appbase_override)
 -  jvlChannelName = jvlChannelName.replaceAll("[^\\w\\-]+", "_")
 -  // install4j application and folder names
 -  if (install4jSuffix == "") {
--    install4jApplicationName = "${jalview_name}"
 -    install4jBundleId = "${install4j_bundle_id}"
 -    install4jWinApplicationId = install4j_release_win_application_id
 -  } else {
--    install4jApplicationName = "${jalview_name} ${install4jSuffix}"
+-    applicationName = "${jalview_name} ${install4jSuffix}"
 -    install4jBundleId = "${install4j_bundle_id}-" + install4jSuffix.toLowerCase()
 -    // add int hash of install4jSuffix to the last part of the application_id
 -    def id = install4j_release_win_application_id
 -  }
 -  // sanitise folder and id names
 -  // install4jApplicationFolder = e.g. "Jalview Build"
--  install4jApplicationFolder = install4jApplicationName
+-  install4jApplicationFolder = applicationName
 -                                    .replaceAll("[\"'~:/\\\\\\s]", "_") // replace all awkward filename chars " ' ~ : / \
 -                                    .replaceAll("_+", "_") // collapse __
--  install4jInternalId = install4jApplicationName
+-  install4jInternalId = applicationName
 -                                    .replaceAll(" ","_")
 -                                    .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
 -                                    .replaceAll("_+", "") // collapse __
 -                                    //.replaceAll("_*-_*", "-") // collapse _-_
--  install4jUnixApplicationFolder = install4jApplicationName
+-  install4jUnixApplicationFolder = applicationName
 -                                    .replaceAll(" ","_")
 -                                    .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
 -                                    .replaceAll("_+", "_") // collapse __
 -                                    .toLowerCase()
 -
 -  getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local"
--  getdownAppDir = string("${getdownWebsiteDir}/${getdownAppDistDir}")
--  //getdownJ11libDir = "${getdownWebsiteDir}/${getdown_j11lib_dir}"
--  getdownResourceDir = string("${getdownWebsiteDir}/${getdown_resource_dir}")
--  getdownInstallDir = string("${getdownWebsiteDir}/${getdown_install_dir}")
+-  getdownAppDir = string("${getdownAppBaseDir}/${getdownAppDistDir}")
+-  //getdownJ11libDir = "${getdownAppBaseDir}/${getdown_j11lib_dir}"
+-  getdownResourceDir = string("${getdownAppBaseDir}/${getdown_resource_dir}")
+-  getdownInstallDir = string("${getdownAppBaseDir}/${getdown_install_dir}")
 -  getdownFilesDir = string("${jalviewDir}/${getdown_files_dir}/${JAVA_VERSION}/")
 -  getdownFilesInstallDir = string("${getdownFilesDir}/${getdown_install_dir}")
 -  /* compile without modules -- using classpath libraries
 -  modules_compileClasspath = fileTree(dir: "${jalviewDir}/${j11modDir}", include: ["*.jar"])
 -  modules_runtimeClasspath = modules_compileClasspath
 -  */
--  def details = versionDetails()
--  gitHash = details.gitHash
--  gitBranch = details.branchName
-+  resourceClassesDir = classesDir
-+
-+  testSourceDir = testDir
-+  testClassesDir = "${jalviewDir}/${test_output_dir}"
+-
+-  gitHash = "SOURCE"
+-  gitBranch = "Source"
+-  try {
+-    apply plugin: "com.palantir.git-version"
+-    def details = versionDetails()
+-    gitHash = details.gitHash
+-    gitBranch = details.branchName
+-  } catch(org.gradle.api.internal.plugins.PluginApplicationException e) {
+-    println("Not in a git repository. Using git values from RELEASE properties file.")
+-    gitHash = releaseProps.getProperty("git.hash")
+-    gitBranch = releaseProps.getProperty("git.branch")
+-  } catch(java.lang.RuntimeException e1) {
+-    throw new GradleException("Error with git-version plugin.  Directory '.git' exists but versionDetails() cannot be found.")
+-  }
  
-+  buildProperties = string("${classesDir}/${build_properties_file}")
-+  getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
-+
 +  install4jApplicationName = "${jalview_name}"
 +  
    println("Using a ${CHANNEL} profile.")
  
    additional_compiler_args = []
-@@ -396,71 +109,16 @@
+@@ -468,65 +109,16 @@ ext {
      libDistDir = j8libDir
      compile_source_compatibility = 1.8
      compile_target_compatibility = 1.8
 -    '--add-modules', j11modules
 -    ]
 -     */
--  } else if (JAVA_VERSION.equals("12") || JAVA_VERSION.equals("13")) {
--    JAVA_INTEGER_VERSION = JAVA_VERSION
--    libDir = j11libDir
--    libDistDir = j11libDir
--    compile_source_compatibility = JAVA_VERSION
--    compile_target_compatibility = JAVA_VERSION
+-  } else if (JAVA_VERSION.equals("17")) {
+-    JAVA_INTEGER_VERSION = string("17")
+-    libDir = j17libDir
+-    libDistDir = j17libDir
+-    compile_source_compatibility = 17
+-    compile_target_compatibility = 17
 -    getdownAltJavaMinVersion = string(findProperty("getdown_alt_java11_min_version"))
 -    getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java11_max_version"))
 -    getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java11_txt_multi_java_location"))
--    eclipseJavaRuntimeName = string("JavaSE-11")
+-    eclipseJavaRuntimeName = string("JavaSE-17")
 -    /* compile without modules -- using classpath libraries
 -    additional_compiler_args += [
 -    '--module-path', modules_compileClasspath.asPath,
 -  // for install4j
 -  JAVA_MIN_VERSION = JAVA_VERSION
 -  JAVA_MAX_VERSION = JAVA_VERSION
--  def jreInstallsDir = string(jre_installs_dir)
+-  jreInstallsDir = string(jre_installs_dir)
 -  if (jreInstallsDir.startsWith("~/")) {
 -    jreInstallsDir = System.getProperty("user.home") + jreInstallsDir.substring(1)
 -  }
--  macosJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-mac-x64/jre")
--  macosJavaVMTgz = string("${jreInstallsDir}/tgz/jre-${JAVA_INTEGER_VERSION}-mac-x64.tar.gz")
--  windowsJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-windows-x64/jre")
--  windowsJavaVMTgz = string("${jreInstallsDir}/tgz/jre-${JAVA_INTEGER_VERSION}-windows-x64.tar.gz")
--  linuxJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-linux-x64/jre")
--  linuxJavaVMTgz = string("${jreInstallsDir}/tgz/jre-${JAVA_INTEGER_VERSION}-linux-x64.tar.gz")
 -  install4jDir = string("${jalviewDir}/${install4j_utils_dir}")
 -  install4jConfFileName = string("jalview-install4j-conf.install4j")
 -  install4jConfFile = file("${install4jDir}/${install4jConfFileName}")
    resourceBuildDir = string("${buildDir}/resources")
    resourcesBuildDir = string("${resourceBuildDir}/resources_build")
    helpBuildDir = string("${resourceBuildDir}/help_build")
-@@ -474,31 +132,6 @@
+@@ -540,39 +132,6 @@ ext {
    helpSourceDir = string("${helpParentDir}/${help_dir}")
    helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")
  
+-  convertBinary = null
+-  convertBinaryExpectedLocation = imagemagick_convert
+-  if (convertBinaryExpectedLocation.startsWith("~/")) {
+-    convertBinaryExpectedLocation = System.getProperty("user.home") + convertBinaryExpectedLocation.substring(1)
+-  }
+-  if (file(convertBinaryExpectedLocation).exists()) {
+-    convertBinary = convertBinaryExpectedLocation
+-  }
 -
 -  relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
 -  jalviewjsBuildDir = string("${relativeBuildDir}/jalviewjs")
    // ENDEXT
  }
  
-@@ -517,27 +150,12 @@
+@@ -591,27 +150,12 @@ sourceSets {
      compileClasspath = files(sourceSets.main.java.outputDir)
      compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
  
    }
  
    test {
-@@ -557,453 +175,41 @@
+@@ -631,453 +175,41 @@ sourceSets {
      runtimeClasspath = compileClasspath
      runtimeClasspath += files(sourceSets.test.resources.srcDirs)
    }
 -  if (cloverreport_jvmargs.length() > 0) {
 -    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
 -  }
--
 -  def argsList = [
 -    "--alwaysreport",
 -    "--initstring",
 -
 -  args argsList.toArray()
 -}
-+    compileClasspath = files( sourceSets.test.java.outputDir )
-+    compileClasspath += sourceSets.main.compileClasspath
-+    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**   REMOVE_THIS_GAP  /*.jar"])
+-
+-
 -task cloverReport {
 -  group = "Verification"
 -  description = "Creates clover reports"
 -
 -
 -compileCloverJava {
--
++    compileClasspath = files( sourceSets.test.java.outputDir )
++    compileClasspath += sourceSets.main.compileClasspath
++    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**   REMOVE_THIS_GAP  /*.jar"])
 -  doFirst {
 -    sourceCompatibility = compile_source_compatibility
 -    targetCompatibility = compile_target_compatibility
 -  // JBP->BS should the print statement in doFirst refer to compile_target_compatibility ?
    sourceCompatibility = compile_source_compatibility
    targetCompatibility = compile_target_compatibility
-   options.compilerArgs = additional_compiler_args
+-  options.compilerArgs += additional_compiler_args
 -  options.encoding = "UTF-8"
++  options.compilerArgs = additional_compiler_args
    doFirst {
      print ("Setting target compatibility to "+compile_target_compatibility+"\n")
    }
  compileTestJava {
 -  sourceCompatibility = compile_source_compatibility
 -  targetCompatibility = compile_target_compatibility
--  options.compilerArgs = additional_compiler_args
+-  options.compilerArgs += additional_compiler_args
    doFirst {
 +    sourceCompatibility = compile_source_compatibility
 +    targetCompatibility = compile_target_compatibility
      print ("Setting target compatibility to "+targetCompatibility+"\n")
    }
  }
-@@ -1017,7 +223,6 @@
+@@ -1091,7 +223,6 @@ clean {
  
  
  cleanTest {
    doFirst {
      delete sourceSets.test.java.outputDir
    }
-@@ -1031,85 +236,6 @@
+@@ -1100,89 +231,11 @@ cleanTest {
+ // format is a string like date.format("dd MMMM yyyy")
+ def getDate(format) {
++  def date = new Date()
+   return date.format(format)
  }
  
  
  task copyDocs(type: Copy) {
    def inputDir = "${jalviewDir}/${doc_dir}"
    def outputDir = "${docBuildDir}/${doc_dir}"
-@@ -1140,27 +266,6 @@
+@@ -1213,235 +266,6 @@ task copyDocs(type: Copy) {
  }
  
  
 -}
 -
 -
+-def hugoTemplateSubstitutions(String input, Map extras=null) {
+-  def replacements = [
+-    DATE: getDate("yyyy-MM-dd"),
+-    CHANNEL: propertiesChannelName,
+-    APPLICATION_NAME: applicationName,
+-    GIT_HASH: gitHash,
+-    GIT_BRANCH: gitBranch,
+-    VERSION: JALVIEW_VERSION,
+-    JAVA_VERSION: JAVA_VERSION,
+-    VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
+-    DRAFT: "false",
+-    JVL_HEADER: ""
+-  ]
+-  def output = input
+-  if (extras != null) {
+-    extras.each{ k, v ->
+-      output = output.replaceAll("__${k}__", ((v == null)?"":v))
+-    }
+-  }
+-  replacements.each{ k, v ->
+-    output = output.replaceAll("__${k}__", ((v == null)?"":v))
+-  }
+-  return output
+-}
+-
+-def mdFileComponents(File mdFile, def dateOnly=false) {
+-  def map = [:]
+-  def content = ""
+-  if (mdFile.exists()) {
+-    def inFrontMatter = false
+-    def firstLine = true
+-    mdFile.eachLine { line ->
+-      if (line.matches("---")) {
+-        def prev = inFrontMatter
+-        inFrontMatter = firstLine
+-        if (inFrontMatter != prev)
+-          return false
+-      }
+-      if (inFrontMatter) {
+-        def m = null
+-        if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
+-          map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
+-        } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
+-          map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
+-        } else if (m = line =~ /^channel:\s*(\S+)/) {
+-          map["channel"] = m[0][1]
+-        } else if (m = line =~ /^version:\s*(\S+)/) {
+-          map["version"] = m[0][1]
+-        } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
+-          map[ m[0][1] ] = m[0][2]
+-        }
+-        if (dateOnly && map["date"] != null) {
+-          return false
+-        }
+-      } else {
+-        if (dateOnly)
+-          return false
+-        content += line+"\n"
+-      }
+-      firstLine = false
+-    }
+-  }
+-  return dateOnly ? map["date"] : [map, content]
+-}
+-
+-task hugoTemplates {
+-  group "website"
+-  description "Create partially populated md pages for hugo website build"
+-
+-  def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
+-  def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
+-  def templateFiles = fileTree(dir: hugoTemplatesDir)
+-  def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+-  def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+-  def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
+-  def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")
+-
+-  doFirst {
+-    // specific release template for version archive
+-    def changes = ""
+-    def whatsnew = null
+-    def givenDate = null
+-    def givenChannel = null
+-    def givenVersion = null
+-    if (CHANNEL == "RELEASE") {
+-      def (map, content) = mdFileComponents(releaseMdFile)
+-      givenDate = map.date
+-      givenChannel = map.channel
+-      givenVersion = map.version
+-      changes = content
+-      if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
+-        throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
+-      }
+-
+-      if (whatsnewMdFile.exists())
+-        whatsnew = whatsnewMdFile.text
+-    }
+-
+-    def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
+-    def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []
+-
+-    def changesHugo = null
+-    if (changes != null) {
+-      changesHugo = '<div class="release_notes">\n\n'
+-      def inSection = false
+-      changes.eachLine { line ->
+-        def m = null
+-        if (m = line =~ /^##([^#].*)$/) {
+-          if (inSection) {
+-            changesHugo += "</div>\n\n"
+-          }
+-          def section = m[0][1].trim()
+-          section = section.toLowerCase()
+-          section = section.replaceAll(/ +/, "_")
+-          section = section.replaceAll(/[^a-z0-9_\-]/, "")
+-          changesHugo += "<div class=\"${section}\">\n\n"
+-          inSection = true
+-        } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
+-          def comment = m[0][2].trim()
+-          if (comment != "") {
+-            comment = comment.replaceAll('"', "&quot;")
+-            def issuekeys = []
+-            comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
+-            def newline = m[0][1]
+-            if (comment.trim() != "")
+-              newline += "{{<comment>}}${comment}{{</comment>}}  "
+-            newline += m[0][3].trim()
+-            if (issuekeys.size() > 0)
+-              newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
+-            if (m[0][4] != null)
+-              newline += m[0][4]
+-            line = newline
+-          }
+-        }
+-        changesHugo += line+"\n"
+-      }
+-      if (inSection) {
+-        changesHugo += "\n</div>\n\n"
+-      }
+-      changesHugo += '</div>'
+-    }
+-
+-    templateFiles.each{ templateFile ->
+-      def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
+-      def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
+-      def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )
+-
+-      def outPathName = string("${hugoBuildDir}/$newRelPathName")
+-
+-      copy {
+-        from templateFile
+-        rename(templateFile.getName(), newFileName)
+-        into outPathName
+-      }
+-
+-      def newFile = file("${outPathName}/${newFileName}".toString())
+-      def content = newFile.text
+-      newFile.text = hugoTemplateSubstitutions(content,
+-        [
+-          WHATSNEW: whatsnew,
+-          CHANGES: changesHugo,
+-          DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
+-          DRAFT: givenDate == null ? "true" : "false",
+-          JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
+-          JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
+-        ]
+-      )
+-    }
+-
+-  }
+-
+-  inputs.file(oldJvlFile)
+-  inputs.dir(hugoTemplatesDir)
+-  inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
+-  inputs.property("CHANNEL", { CHANNEL })
+-}
+-
+-def getMdDate(File mdFile) {
+-  return mdFileComponents(mdFile, true)
+-}
+-
+-def getMdSections(String content) {
+-  def sections = [:]
+-  def sectionContent = ""
+-  def sectionName = null
+-  content.eachLine { line ->
+-    def m = null
+-    if (m = line =~ /^##([^#].*)$/) {
+-      if (sectionName != null) {
+-        sections[sectionName] = sectionContent
+-        sectionName = null
+-        sectionContent = ""
+-      }
+-      sectionName = m[0][1].trim()
+-      sectionName = sectionName.toLowerCase()
+-      sectionName = sectionName.replaceAll(/ +/, "_")
+-      sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
+-    } else if (sectionName != null) {
+-      sectionContent += line+"\n"
+-    }
+-  }
+-  if (sectionContent != null) {
+-    sections[sectionName] = sectionContent
+-  }
+-  return sections
+-}
+-
+-
  task copyHelp(type: Copy) {
    def inputDir = helpSourceDir
    def outputDir = "${helpBuildDir}/${help_dir}"
-@@ -1242,24 +347,15 @@
+@@ -1476,7 +300,6 @@ task copyHelp(type: Copy) {
+   outputs.dir(outputDir)
+ }
+-
+ task releasesTemplates {
+   group "help"
+   description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
+@@ -1491,6 +314,7 @@ task releasesTemplates {
+   def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
+   doFirst {
++    def JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
+     def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+     def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+@@ -1505,7 +329,7 @@ task releasesTemplates {
+     def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
+     def releaseFilesDates = releaseFiles.collectEntries {
+-      [(it): getMdDate(it)]
++      [(it): getDate("")]
+     }
+     releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
+@@ -1513,96 +337,13 @@ task releasesTemplates {
+     def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
+     def versionTemplate = m[0][1]
+-    MutableDataSet options = new MutableDataSet()
+-
+-    def extensions = new ArrayList<>()
+-    options.set(Parser.EXTENSIONS, extensions)
+-    options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)
+-
+-    Parser parser = Parser.builder(options).build()
+-    HtmlRenderer renderer = HtmlRenderer.builder(options).build()
+-
+-    def actualVersions = releaseFiles.collect { rf ->
+-      def (rfMap, rfContent) = mdFileComponents(rf)
+-      return rfMap.version
+-    }
+     def versionsHtml = ""
+     def linkedVersions = []
+-    releaseFiles.reverse().each { rFile ->
+-      def (rMap, rContent) = mdFileComponents(rFile)
+-
+-      def versionLink = ""
+-      def partialVersion = ""
+-      def firstPart = true
+-      rMap.version.split("\\.").each { part ->
+-        def displayPart = ( firstPart ? "" : "." ) + part
+-        partialVersion += displayPart
+-        if (
+-            linkedVersions.contains(partialVersion)
+-            || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
+-            ) {
+-          versionLink += displayPart
+-        } else {
+-          versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
+-          linkedVersions += partialVersion
+-        }
+-        firstPart = false
+-      }
+-      def displayDate = releaseFilesDates[rFile].format("dd/MM/yyyy")
+-
+-      def lm = null
+-      def rContentProcessed = ""
+-      rContent.eachLine { line ->
+-        if (lm = line =~ /^(\s*-)(\s*<!--[^>]*?-->)(.*)$/) {
+-          line = "${lm[0][1]}${lm[0][3]}${lm[0][2]}"
+-      } else if (lm = line =~ /^###([^#]+.*)$/) {
+-          line = "_${lm[0][1].trim()}_"
+-        }
+-        rContentProcessed += line + "\n"
+-      }
+-
+-      def rContentSections = getMdSections(rContentProcessed)
+-      def rVersion = versionTemplate
+-      if (rVersion != "") {
+-        def rNewFeatures = rContentSections["new_features"]
+-        def rIssuesResolved = rContentSections["issues_resolved"]
+-        Node newFeaturesNode = parser.parse(rNewFeatures)
+-        String newFeaturesHtml = renderer.render(newFeaturesNode)
+-        Node issuesResolvedNode = parser.parse(rIssuesResolved)
+-        String issuesResolvedHtml = renderer.render(issuesResolvedNode)
+-        rVersion = hugoTemplateSubstitutions(rVersion,
+-          [
+-            VERSION: rMap.version,
+-            VERSION_LINK: versionLink,
+-            DISPLAY_DATE: displayDate,
+-            NEW_FEATURES: newFeaturesHtml,
+-            ISSUES_RESOLVED: issuesResolvedHtml
+-          ]
+-        )
+-        versionsHtml += rVersion
+-      }
+-    }
+     releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
+-    releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
+     releasesHtmlFile.text = releasesTemplate
+-    if (whatsnewMdFile.exists()) {
+-      def wnDisplayDate = releaseFilesDates[releaseMdFile] != null ? releaseFilesDates[releaseMdFile].format("dd MMMM yyyy") : ""
+-      def whatsnewMd = hugoTemplateSubstitutions(whatsnewMdFile.text)
+-      Node whatsnewNode = parser.parse(whatsnewMd)
+-      String whatsnewHtml = renderer.render(whatsnewNode)
+-      whatsnewHtml = whatsnewTemplateFile.text.replaceAll("__WHATS_NEW__", whatsnewHtml)
+-      whatsnewHtmlFile.text = hugoTemplateSubstitutions(whatsnewHtml,
+-        [
+-            VERSION: JALVIEW_VERSION,
+-          DISPLAY_DATE: wnDisplayDate
+-        ]
+-      )
+-    } else if (gradle.taskGraph.hasTask(":linkCheck")) {
+-      whatsnewHtmlFile.text = "Development build " + getDate("yyyy-MM-dd HH:mm:ss")
+-    }
+-
++    whatsnewHtmlFile.text = "Debian build " + getDate("yyyy-MM-dd HH:mm:ss")
+   }
+   inputs.file(releasesTemplateFile)
+@@ -1613,7 +354,6 @@ task releasesTemplates {
+   outputs.file(whatsnewHtmlFile)
+ }
+-
+ task copyResources(type: Copy) {
+   group = "build"
+   description = "Copy (and make text substitutions in) the resources dir to the build area"
+@@ -1653,44 +393,22 @@ task copyChannelResources(type: Copy) {
+   def inputDir = "${channelDir}/${resource_dir}"
+   def outputDir = resourcesBuildDir
+-  from(inputDir) {
+-    include(channel_props)
+-    filter(ReplaceTokens,
+-      beginToken: '__',
+-      endToken: '__',
+-      tokens: [
+-        'SUFFIX': channelSuffix
+-      ]
+-    )
+-  }
+-  from(inputDir) {
+-    exclude(channel_props)
+-  }
++  from inputDir
+   into outputDir
+   inputs.dir(inputDir)
    outputs.dir(outputDir)
  }
  
 -  property "BUILD_DATE", getDate("HH:mm:ss dd MMMM yyyy")
 -  property "VERSION", JALVIEW_VERSION
 -  property "INSTALLATION", INSTALLATION+" git-commit:"+gitHash+" ["+gitBranch+"]"
+-  property "JAVA_COMPILE_VERSION", JAVA_INTEGER_VERSION
 -  if (getdownSetAppBaseProperty) {
 -    property "GETDOWNAPPBASE", getdownAppBase
 -    property "GETDOWNAPPDISTDIR", getdownAppDistDir
    outputs.file(outputFile)
  }
  
-@@ -1293,7 +389,6 @@
-   dependsOn buildResources
+@@ -1725,123 +443,31 @@ task prepare {
    dependsOn copyDocs
    dependsOn copyHelp
+   dependsOn releasesTemplates
 -  dependsOn convertMdFiles
    dependsOn buildIndices
  }
  
-@@ -1306,12 +401,7 @@
- //testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
+ compileJava.dependsOn prepare
+ run.dependsOn compileJava
+-compileTestJava.dependsOn compileJava
+-
++//run.dependsOn prepare
++//testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
  test {
-   dependsOn prepare
+-  group = "Verification"
+-  description = "Runs all testTaskN tasks)"
 -
 -  if (useClover) {
 -    dependsOn cloverClasses
--   } else { //?
--    dependsOn compileJava //?
+-  } else { //?
+-    dependsOn testClasses
+-  }
+-
+-  // not running tests in this task
+-  exclude "**/*"
+-}
+-/* testTask0 is the main test task */
+-task testTask0(type: Test) {
+-  group = "Verification"
+-  description = "The main test task. Runs all non-testTaskN-labelled tests (unless excluded)"
+-  useTestNG() {
+-    includeGroups testng_groups.split(",")
+-    excludeGroups testng_excluded_groups.split(",")
+-    tasks.withType(Test).matching {it.name.startsWith("testTask") && it.name != name}.all {t -> excludeGroups t.name}
+-    preserveOrder true
+-    useDefaultListeners=true
+-  }
+-}
+-
+-/* separated tests */
+-task testTask1(type: Test) {
+-  group = "Verification"
+-  description = "Tests that need to be isolated from the main test run"
+-  useTestNG() {
+-    includeGroups name
+-    excludeGroups testng_excluded_groups.split(",")
+-    preserveOrder true
+-    useDefaultListeners=true
 -  }
+-}
++  dependsOn prepare
 +  dependsOn compileJava //?
  
+-/* insert more testTaskNs here -- change N to next digit or other string */
+-/*
+-task testTaskN(type: Test) {
+-  group = "Verification"
+-  description = "Tests that need to be isolated from the main test run"
    useTestNG() {
-     includeGroups testng_groups
-@@ -1323,6 +413,7 @@
+-    includeGroups name
+-    excludeGroups testng_excluded_groups.split(",")
++    includeGroups testng_groups
++    excludeGroups testng_excluded_groups
+     preserveOrder true
+     useDefaultListeners=true
+   }
+-}
+-*/
+-
+-/*
+- * adapted from https://medium.com/@wasyl/pretty-tests-summary-in-gradle-744804dd676c
+- * to summarise test results from all Test tasks
+- */
+-/* START of test tasks results summary */
+-import groovy.time.TimeCategory
+-import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+-import org.gradle.api.tasks.testing.logging.TestLogEvent
+-rootProject.ext.testsResults = [] // Container for tests summaries
+-
+-tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { testTask ->
+-
+-  // from original test task
+-  if (useClover) {
+-    dependsOn cloverClasses
+-  } else { //?
+-    dependsOn testClasses //?
+-  }
+-
+-  // run main tests first
+-  if (!testTask.name.equals("testTask0"))
+-    testTask.mustRunAfter "testTask0"
+-
+-  testTask.testLogging { logging ->
+-    events TestLogEvent.FAILED
+-//      TestLogEvent.SKIPPED,
+-//      TestLogEvent.STANDARD_OUT,
+-//      TestLogEvent.STANDARD_ERROR
+-
+-    exceptionFormat TestExceptionFormat.FULL
+-    showExceptions true
+-    showCauses true
+-    showStackTraces true
+-
+-    info.events = [ TestLogEvent.FAILED ]
+-  }
+-
+-
+-
+-  ignoreFailures = true // Always try to run all tests for all modules
+-
+-  afterSuite { desc, result ->
+-    if (desc.parent)
+-      return // Only summarize results for whole modules
+-
+-    def resultsInfo = [testTask.project.name, testTask.name, result, TimeCategory.minus(new Date(result.endTime), new Date(result.startTime)), testTask.reports.html.entryPoint]
+-
+-    rootProject.ext.testsResults.add(resultsInfo)
+-  }
+-  // from original test task
    maxHeapSize = "1024m"
  
    workingDir = jalviewDir
    def testLaf = project.findProperty("test_laf")
    if (testLaf != null) {
      println("Setting Test LaF to '${testLaf}'")
-@@ -1338,9 +429,6 @@
+@@ -1857,143 +483,8 @@ tasks.withType(Test).matching {t -> t.getName().startsWith("testTask")}.all { te
    jvmArgs += additional_compiler_args
  
    doFirst {
+-    // this is not perfect yet -- we should only add the commandLineIncludePatterns to the
+-    // testTasks that include the tests, and exclude all from the others.
+-    // get --test argument
+-    filter.commandLineIncludePatterns = test.filter.commandLineIncludePatterns
+-    // do something with testTask.getCandidateClassFiles() to see if the test should silently finish because of the
+-    // commandLineIncludePatterns not matching anything.  Instead we are doing setFailOnNoMatchingTests(false) below
+-
+-
 -    if (useClover) {
 -      println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
 -    }
+-  }
+-
+-
+-  /* don't fail on no matching tests (so --tests will run across all testTasks) */
+-  testTask.filter.setFailOnNoMatchingTests(false)
+-
+-  /* ensure the "test" task dependsOn all the testTasks */
+-  test.dependsOn testTask
+-}
+-
+-gradle.buildFinished {
+-    def allResults = rootProject.ext.testsResults
+-
+-    if (!allResults.isEmpty()) {
+-        printResults allResults
+-        allResults.each {r ->
+-          if (r[2].resultType == TestResult.ResultType.FAILURE)
+-            throw new GradleException("Failed tests!")
+-        }
+-    }
+-}
+-
+-private static String colString(styler, col, colour, text) {
+-  return col?"${styler[colour](text)}":text
+-}
+-
+-private static String getSummaryLine(s, pn, tn, rt, rc, rs, rf, rsk, t, col) {
+-  def colour = 'black'
+-  def text = rt
+-  def nocol = false
+-  if (rc == 0) {
+-    text = "-----"
+-    nocol = true
+-  } else {
+-    switch(rt) {
+-      case TestResult.ResultType.SUCCESS:
+-        colour = 'green'
+-        break;
+-      case TestResult.ResultType.FAILURE:
+-        colour = 'red'
+-        break;
+-      default:
+-        nocol = true
+-        break;
+-    }
    }
+-  StringBuilder sb = new StringBuilder()
+-  sb.append("${pn}")
+-  if (tn != null)
+-    sb.append(":${tn}")
+-  sb.append(" results: ")
+-  sb.append(colString(s, col && !nocol, colour, text))
+-  sb.append(" (")
+-  sb.append("${rc} tests, ")
+-  sb.append(colString(s, col && rs > 0, 'green', rs))
+-  sb.append(" successes, ")
+-  sb.append(colString(s, col && rf > 0, 'red', rf))
+-  sb.append(" failures, ")
+-  sb.append("${rsk} skipped) in ${t}")
+-  return sb.toString()
+-}
+-
+-private static void printResults(allResults) {
+-
+-    // styler from https://stackoverflow.com/a/56139852
+-    def styler = 'black red green yellow blue magenta cyan white'.split().toList().withIndex(30).collectEntries { key, val -> [(key) : { "\033[${val}m${it}\033[0m" }] }
+-
+-    def maxLength = 0
+-    def failedTests = false
+-    def summaryLines = []
+-    def totalcount = 0
+-    def totalsuccess = 0
+-    def totalfail = 0
+-    def totalskip = 0
+-    def totaltime = TimeCategory.getSeconds(0)
+-    // sort on project name then task name
+-    allResults.sort {a, b -> a[0] == b[0]? a[1]<=>b[1]:a[0] <=> b[0]}.each {
+-      def projectName = it[0]
+-      def taskName = it[1]
+-      def result = it[2]
+-      def time = it[3]
+-      def report = it[4]
+-      def summaryCol = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, true)
+-      def summaryPlain = getSummaryLine(styler, projectName, taskName, result.resultType, result.testCount, result.successfulTestCount, result.failedTestCount, result.skippedTestCount, time, false)
+-      def reportLine = "Report file: ${report}"
+-      def ls = summaryPlain.length()
+-      def lr = reportLine.length()
+-      def m = [ls, lr].max()
+-      if (m > maxLength)
+-        maxLength = m
+-      def info = [ls, summaryCol, reportLine]
+-      summaryLines.add(info)
+-      failedTests |= result.resultType == TestResult.ResultType.FAILURE
+-      totalcount += result.testCount
+-      totalsuccess += result.successfulTestCount
+-      totalfail += result.failedTestCount
+-      totalskip += result.skippedTestCount
+-      totaltime += time
+-    }
+-    def totalSummaryCol = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, true)
+-    def totalSummaryPlain = getSummaryLine(styler, "OVERALL", "", failedTests?TestResult.ResultType.FAILURE:TestResult.ResultType.SUCCESS, totalcount, totalsuccess, totalfail, totalskip, totaltime, false)
+-    def tls = totalSummaryPlain.length()
+-    if (tls > maxLength)
+-      maxLength = tls
+-    def info = [tls, totalSummaryCol, null]
+-    summaryLines.add(info)
+-
+-    def allSummaries = []
+-    for(sInfo : summaryLines) {
+-      def ls = sInfo[0]
+-      def summary = sInfo[1]
+-      def report = sInfo[2]
+-
+-      StringBuilder sb = new StringBuilder()
+-      sb.append("│" + summary + " " * (maxLength - ls) + "│")
+-      if (report != null) {
+-        sb.append("\n│" + report + " " * (maxLength - report.length()) + "│")
+-      }
+-      allSummaries += sb.toString()
+-    }
+-
+-    println "┌${"${"─" * maxLength}"}┐"
+-    println allSummaries.join("\n├${"${"─" * maxLength}"}┤\n")
+-    println "└${"${"─" * maxLength}"}┘"
  }
+-/* END of test tasks results summary */
  
-@@ -1420,1752 +508,7 @@
+ task compileLinkCheck(type: JavaCompile) {
+@@ -2052,7 +543,7 @@ jar {
+   manifest {
+     attributes "Main-Class": main_class,
+     "Permissions": "all-permissions",
+-    "Application-Name": applicationName,
++    "Application-Name": install4jApplicationName,
+     "Codebase": application_codebase,
+     "Implementation-Version": JALVIEW_VERSION
+   }
+@@ -2060,8 +551,6 @@ jar {
+   def outputDir = "${jalviewDir}/${package_dir}"
+   destinationDirectory = file(outputDir)
+   archiveFileName = rootProject.name+".jar"
+-  duplicatesStrategy "EXCLUDE"
+-
+   exclude "cache*/**"
+   exclude "*.jar"
+@@ -2073,2137 +562,7 @@ jar {
    sourceSets.main.resources.srcDirs.each{ dir ->
      inputs.dir(dir)
    }
 -  }
 -  manifest {
 -    attributes "Implementation-Version": JALVIEW_VERSION,
--    "Application-Name": install4jApplicationName
+-    "Application-Name": applicationName
 -  }
+-
+-  duplicatesStrategy "INCLUDE"
+-
 -  mainClassName = shadow_jar_main_class
 -  mergeServiceFiles()
 -  classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
 -  minimize()
 -}
 -
+-task getdownImagesCopy() {
+-  inputs.dir getdownImagesDir
+-  outputs.dir getdownImagesBuildDir
+-
+-  doFirst {
+-    copy {
+-      from(getdownImagesDir) {
+-        include("*getdown*.png")
+-      }
+-      into getdownImagesBuildDir
+-    }
+-  }
+-}
+-
+-task getdownImagesProcess() {
+-  dependsOn getdownImagesCopy
+-
+-  doFirst {
+-    if (backgroundImageText) {
+-      if (convertBinary == null) {
+-        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+-      }
+-      if (!project.hasProperty("getdown_background_image_text_suffix_cmd")) {
+-        throw new StopExecutionException("No property 'getdown_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+-      }
+-      fileTree(dir: getdownImagesBuildDir, include: "*background*.png").getFiles().each { file ->
+-        exec {
+-          executable convertBinary
+-          args = [
+-            file.getPath(),
+-            '-font', getdown_background_image_text_font,
+-            '-fill', getdown_background_image_text_colour,
+-            '-draw', sprintf(getdown_background_image_text_suffix_cmd, channelSuffix),
+-            '-draw', sprintf(getdown_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+-            '-draw', sprintf(getdown_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+-            file.getPath()
+-          ]
+-        }
+-      }
+-    }
+-  }
+-}
+-
+-task getdownImages() {
+-  dependsOn getdownImagesProcess
+-}
 -
 -task getdownWebsite() {
 -  group = "distribution"
 -  description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
+-
+-  dependsOn getdownImages
 -  if (buildDist) {
 -    dependsOn makeDist
 -  }
 -
 -  def getdownWebsiteResourceFilenames = []
--  def getdownTextString = ""
 -  def getdownResourceDir = getdownResourceDir
 -  def getdownResourceFilenames = []
 -
 -  doFirst {
 -    // clean the getdown website and files dir before creating getdown folders
--    delete getdownWebsiteDir
+-    delete getdownAppBaseDir
 -    delete getdownFilesDir
 -
 -    copy {
 -    }
 -    getdownWebsiteResourceFilenames += "${getdownAppDistDir}/${getdown_build_properties}"
 -
--    // set some getdown_txt_ properties then go through all properties looking for getdown_txt_...
+-    copy {
+-      from channelPropsFile
+-      filter(ReplaceTokens,
+-        beginToken: '__',
+-        endToken: '__',
+-        tokens: [
+-          'SUFFIX': channelSuffix
+-        ]
+-      )
+-      into getdownAppBaseDir
+-    }
+-    getdownWebsiteResourceFilenames += file(channelPropsFile).getName()
+-
+-    // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
 -    def props = project.properties.sort { it.key }
 -    if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
 -      props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
 -    if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
 -      props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
 -    }
--    if (getdownImagesDir != null && file(getdownImagesDir).exists()) {
--      props.put("getdown_txt_ui.background_image", "${getdownImagesDir}/${getdown_background_image}")
--      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesDir}/${getdown_instant_background_image}")
--      props.put("getdown_txt_ui.error_background", "${getdownImagesDir}/${getdown_error_background}")
--      props.put("getdown_txt_ui.progress_image", "${getdownImagesDir}/${getdown_progress_image}")
+-    if (getdownImagesBuildDir != null && file(getdownImagesBuildDir).exists()) {
+-      props.put("getdown_txt_ui.background_image", "${getdownImagesBuildDir}/${getdown_background_image}")
+-      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesBuildDir}/${getdown_instant_background_image}")
+-      props.put("getdown_txt_ui.error_background", "${getdownImagesBuildDir}/${getdown_error_background}")
+-      props.put("getdown_txt_ui.progress_image", "${getdownImagesBuildDir}/${getdown_progress_image}")
 -      props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
 -      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
 -    }
 -
 -    props.put("getdown_txt_title", jalview_name)
--    props.put("getdown_txt_ui.name", install4jApplicationName)
+-    props.put("getdown_txt_ui.name", applicationName)
 -
 -    // start with appbase
--    getdownTextString += "appbase = ${getdownAppBase}\n"
+-    getdownTextLines += "appbase = ${getdownAppBase}"
 -    props.each{ prop, val ->
 -      if (prop.startsWith("getdown_txt_") && val != null) {
 -        if (prop.startsWith("getdown_txt_multi_")) {
 -          def key = prop.substring(18)
 -          val.split(",").each{ v ->
--            def line = "${key} = ${v}\n"
--            getdownTextString += line
+-            def line = "${key} = ${v}"
+-            getdownTextLines += line
 -          }
 -        } else {
 -          // file values rationalised
 -            }
 -          }
 -          if (! prop.startsWith("getdown_txt_resource")) {
--            def line = prop.substring(12) + " = ${val}\n"
--            getdownTextString += line
+-            def line = prop.substring(12) + " = ${val}"
+-            getdownTextLines += line
 -          }
 -        }
 -      }
 -    }
 -
 -    getdownWebsiteResourceFilenames.each{ filename ->
--      getdownTextString += "resource = ${filename}\n"
+-      getdownTextLines += "resource = ${filename}"
 -    }
 -    getdownResourceFilenames.each{ filename ->
 -      copy {
 -      if (s.exists()) {
 -        copy {
 -          from s
--          into "${getdownWebsiteDir}/${getdown_wrapper_script_dir}"
+-          into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
 -        }
--        getdownTextString += "resource = ${getdown_wrapper_script_dir}/${script}\n"
+-        getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
 -      }
 -    }
 -
 -        codeFiles += f
 -      }
 -    }
--    codeFiles.sort().each{f ->
+-    def jalviewJar = jar.archiveFileName.getOrNull()
+-    // put jalview.jar first for CLASSPATH and .properties files reasons
+-    codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
 -      def name = f.getName()
--      def line = "code = ${getdownAppDistDir}/${name}\n"
--      getdownTextString += line
+-      def line = "code = ${getdownAppDistDir}/${name}"
+-      getdownTextLines += line
 -      copy {
 -        from f.getPath()
 -        into getdownAppDir
 -    def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
 -    j11libFiles.sort().each{f ->
 -    def name = f.getName()
--    def line = "code = ${getdown_j11lib_dir}/${name}\n"
--    getdownTextString += line
+-    def line = "code = ${getdown_j11lib_dir}/${name}"
+-    getdownTextLines += line
 -    copy {
 -    from f.getPath()
 -    into getdownJ11libDir
 -     */
 -
 -    // getdown-launcher.jar should not be in main application class path so the main application can move it when updated.  Listed as a resource so it gets updated.
--    //getdownTextString += "class = " + file(getdownLauncher).getName() + "\n"
--    getdownTextString += "resource = ${getdown_launcher_new}\n"
--    getdownTextString += "class = ${main_class}\n"
+-    //getdownTextLines += "class = " + file(getdownLauncher).getName()
+-    getdownTextLines += "resource = ${getdown_launcher_new}"
+-    getdownTextLines += "class = ${main_class}"
 -    // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
 -    if (getdownSetAppBaseProperty) {
--      getdownTextString += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}\n"
--      getdownTextString += "jvmarg = -Dgetdownappbase=${getdownAppBase}\n"
+-      getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
+-      getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
 -    }
 -
--    def getdown_txt = file("${getdownWebsiteDir}/getdown.txt")
--    getdown_txt.write(getdownTextString)
+-    def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
+-    getdownTxt.write(getdownTextLines.join("\n"))
 -
--    def getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
--    def launchJvl = file("${getdownWebsiteDir}/${getdownLaunchJvl}")
+-    getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
+-    def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
 -    launchJvl.write("appbase=${getdownAppBase}")
 -
 -    // files going into the getdown website dir: getdown-launcher.jar
 -    copy {
 -      from getdownLauncher
 -      rename(file(getdownLauncher).getName(), getdown_launcher_new)
--      into getdownWebsiteDir
+-      into getdownAppBaseDir
 -    }
 -
 -    // files going into the getdown website dir: getdown-launcher(-local).jar
 -      if (file(getdownLauncher).getName() != getdown_launcher) {
 -        rename(file(getdownLauncher).getName(), getdown_launcher)
 -      }
--      into getdownWebsiteDir
+-      into getdownAppBaseDir
 -    }
 -
 -    // files going into the getdown website dir: ./install dir and files
 -    if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
 -      copy {
--        from getdown_txt
+-        from getdownTxt
 -        from getdownLauncher
 -        from "${getdownAppDir}/${getdown_build_properties}"
 -        if (file(getdownLauncher).getName() != getdown_launcher) {
 -
 -    // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
 -    copy {
--      from getdown_txt
+-      from getdownTxt
 -      from launchJvl
 -      from getdownLauncher
--      from "${getdownWebsiteDir}/${getdown_build_properties}"
+-      from "${getdownAppBaseDir}/${getdown_build_properties}"
+-      from "${getdownAppBaseDir}/${channel_props}"
 -      if (file(getdownLauncher).getName() != getdown_launcher) {
 -        rename(file(getdownLauncher).getName(), getdown_launcher)
 -      }
 -      into getdownFilesDir
 -    }
 -
--    // and ./resources (not all downloaded by getdown)
+-    // and ./resource (not all downloaded by getdown)
 -    copy {
 -      from getdownResourceDir
 -      into "${getdownFilesDir}/${getdown_resource_dir}"
 -  if (buildDist) {
 -    inputs.dir("${jalviewDir}/${package_dir}")
 -  }
--  outputs.dir(getdownWebsiteDir)
+-  outputs.dir(getdownAppBaseDir)
 -  outputs.dir(getdownFilesDir)
 -}
 -
 -    classpath = files(getdownLauncher)
 -  }
 -  main = "com.threerings.getdown.tools.Digester"
--  args getdownWebsiteDir
--  inputs.dir(getdownWebsiteDir)
--  outputs.file("${getdownWebsiteDir}/digest2.txt")
+-  args getdownAppBaseDir
+-  inputs.dir(getdownAppBaseDir)
+-  outputs.file("${getdownAppBaseDir}/digest2.txt")
 -}
 -
 -
 -  dependsOn getdownDigest
 -  doLast {
 -    if (reportRsyncCommand) {
--      def fromDir = getdownWebsiteDir + (getdownWebsiteDir.endsWith('/')?'':'/')
+-      def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
 -      def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
 -      println "LIKELY RSYNC COMMAND:"
 -      println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
 -    }
 -  }
 -}
+-
+-task getdownArchiveBuild() {
+-  group = "distribution"
+-  description = "Put files in the archive dir to go on the website"
+-
+-  dependsOn getdownWebsite
+-
+-  def v = "v${JALVIEW_VERSION_UNDERSCORES}"
+-  def vDir = "${getdownArchiveDir}/${v}"
+-  getdownFullArchiveDir = "${vDir}/getdown"
+-  getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
+-
+-  def vAltDir = "alt_${v}"
+-  def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
+-
+-  doFirst {
+-    // cleanup old "old" dir
+-    delete getdownArchiveDir
+-
+-    def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
+-    getdownArchiveTxt.getParentFile().mkdirs()
+-    def getdownArchiveTextLines = []
+-    def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
+-
+-    // the libdir
+-    copy {
+-      from "${getdownAppBaseDir}/${getdownAppDistDir}"
+-      into "${getdownFullArchiveDir}/${vAltDir}"
+-    }
+-
+-    getdownTextLines.each { line ->
+-      line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
+-      line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
+-      line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
+-      line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
+-      line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
+-      line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
+-      // remove the existing resource = resource/ or bin/ lines
+-      if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
+-        getdownArchiveTextLines += line
+-      }
+-    }
+-
+-    // the resource dir -- add these files as resource lines in getdown.txt
+-    copy {
+-      from "${archiveImagesDir}"
+-      into "${getdownFullArchiveDir}/${getdown_resource_dir}"
+-      eachFile { file ->
+-        getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
+-      }
+-    }
+-
+-    getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
+-
+-    def vLaunchJvl = file(getdownVersionLaunchJvl)
+-    vLaunchJvl.getParentFile().mkdirs()
+-    vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
+-    def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
+-    def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
+-    // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
+-    //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
+-    java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
+-
+-    // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
+-    copy {
+-      from getdownLauncher
+-      from "${getdownAppBaseDir}/${getdownLaunchJvl}"
+-      from "${getdownAppBaseDir}/${getdown_launcher_new}"
+-      from "${getdownAppBaseDir}/${channel_props}"
+-      if (file(getdownLauncher).getName() != getdown_launcher) {
+-        rename(file(getdownLauncher).getName(), getdown_launcher)
+-      }
+-      into getdownFullArchiveDir
+-    }
+-
+-  }
+-}
+-
+-task getdownArchiveDigest(type: JavaExec) {
+-  group = "distribution"
+-  description = "Digest the getdown archive folder"
+-
+-  dependsOn getdownArchiveBuild
+-
+-  doFirst {
+-    classpath = files(getdownLauncher)
+-    args getdownFullArchiveDir
+-  }
+-  main = "com.threerings.getdown.tools.Digester"
+-  inputs.dir(getdownFullArchiveDir)
+-  outputs.file("${getdownFullArchiveDir}/digest2.txt")
+-}
 -
+-task getdownArchive() {
+-  group = "distribution"
+-  description = "Build the website archive dir with getdown digest"
+-
+-  dependsOn getdownArchiveBuild
+-  dependsOn getdownArchiveDigest
+-}
 -
 -tasks.withType(JavaCompile) {
 -      options.encoding = 'UTF-8'
 -
 -clean {
 -  doFirst {
--    delete getdownWebsiteDir
+-    delete getdownAppBaseDir
 -    delete getdownFilesDir
+-    delete getdownArchiveDir
 -  }
 -}
 -
 -      }
 -    }
 -
+-    // disable install screen for OSX dmg (for 2.11.2.0)
+-    install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
+-      macosArchive.attributes().remove('executeSetupApp')
+-      macosArchive.attributes().remove('setupAppId')
+-    }
+-
 -    // turn off checksum creation for LOCAL channel
 -    def e = install4jConfigXml.application[0]
--    if (CHANNEL == "LOCAL") {
--      e.'@createChecksums' = "false"
--    } else {
--      e.'@createChecksums' = "true"
--    }
+-    e.'@createChecksums' = string(install4jCheckSums)
 -
 -    // put file association actions where placeholder action is
 -    def install4jFileAssociationsText = install4jFileAssociationsFile.text
 -  }
 -}
 -
+-task cleanInstallersDataFiles {
+-  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
+-  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
+-  def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
+-  doFirst {
+-    delete installersOutputTxt
+-    delete installersSha256
+-    delete hugoDataJsonFile
+-  }
+-}
+-
+-task install4jDMGBackgroundImageCopy {
+-  inputs.file "${install4jDMGBackgroundImageDir}/${install4jDMGBackgroundImageFile}"
+-  outputs.dir "${install4jDMGBackgroundImageBuildDir}"
+-  doFirst {
+-    copy {
+-      from(install4jDMGBackgroundImageDir) {
+-        include(install4jDMGBackgroundImageFile)
+-      }
+-      into install4jDMGBackgroundImageBuildDir
+-    }
+-  }
+-}
+-
+-task install4jDMGBackgroundImageProcess {
+-  dependsOn install4jDMGBackgroundImageCopy
+-
+-  doFirst {
+-    if (backgroundImageText) {
+-      if (convertBinary == null) {
+-        throw new StopExecutionException("No ImageMagick convert binary installed at '${convertBinaryExpectedLocation}'")
+-      }
+-      if (!project.hasProperty("install4j_background_image_text_suffix_cmd")) {
+-        throw new StopExecutionException("No property 'install4j_background_image_text_suffix_cmd' defined. See channel_gradle.properties for channel ${CHANNEL}")
+-      }
+-      fileTree(dir: install4jDMGBackgroundImageBuildDir, include: "*.png").getFiles().each { file ->
+-        exec {
+-          executable convertBinary
+-          args = [
+-            file.getPath(),
+-            '-font', install4j_background_image_text_font,
+-            '-fill', install4j_background_image_text_colour,
+-            '-draw', sprintf(install4j_background_image_text_suffix_cmd, channelSuffix),
+-            '-draw', sprintf(install4j_background_image_text_commit_cmd, "git-commit: ${gitHash}"),
+-            '-draw', sprintf(install4j_background_image_text_date_cmd, getDate("yyyy-MM-dd HH:mm:ss")),
+-            file.getPath()
+-          ]
+-        }
+-      }
+-    }
+-  }
+-}
+-
+-task install4jDMGBackgroundImage {
+-  dependsOn install4jDMGBackgroundImageProcess
+-}
 -
--task installers(type: com.install4j.gradle.Install4jTask) {
+-task installerFiles(type: com.install4j.gradle.Install4jTask) {
 -  group = "distribution"
 -  description = "Create the install4j installers"
 -  dependsOn getdown
 -  dependsOn copyInstall4jTemplate
+-  dependsOn cleanInstallersDataFiles
+-  dependsOn install4jDMGBackgroundImage
 -
 -  projectFile = install4jConfFile
 -
 -    filesMd5 = filesMd5.substring(0,8)
 -  }
 -  def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"
--  // make install4jBuildDir relative to jalviewDir
--  def install4jBuildDir = "${install4j_build_dir}/${JAVA_VERSION}"
 -
 -  variables = [
 -    'JALVIEW_NAME': jalview_name,
--    'JALVIEW_APPLICATION_NAME': install4jApplicationName,
+-    'JALVIEW_APPLICATION_NAME': applicationName,
 -    'JALVIEW_DIR': "../..",
 -    'OSX_KEYSTORE': OSX_KEYSTORE,
 -    'OSX_APPLEID': OSX_APPLEID,
 -    'JAVA_VERSION': JAVA_VERSION,
 -    'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
 -    'VERSION': JALVIEW_VERSION,
--    'MACOS_JAVA_VM_DIR': macosJavaVMDir,
--    'WINDOWS_JAVA_VM_DIR': windowsJavaVMDir,
--    'LINUX_JAVA_VM_DIR': linuxJavaVMDir,
--    'MACOS_JAVA_VM_TGZ': macosJavaVMTgz,
--    'WINDOWS_JAVA_VM_TGZ': windowsJavaVMTgz,
--    'LINUX_JAVA_VM_TGZ': linuxJavaVMTgz,
 -    'COPYRIGHT_MESSAGE': install4j_copyright_message,
 -    'BUNDLE_ID': install4jBundleId,
 -    'INTERNAL_ID': install4jInternalId,
 -    'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
 -    'MACOS_DMG_DS_STORE': install4jDMGDSStore,
--    'MACOS_DMG_BG_IMAGE': install4jDMGBackgroundImage,
+-    'MACOS_DMG_BG_IMAGE': "${install4jDMGBackgroundImageBuildDir}/${install4jDMGBackgroundImageFile}",
 -    'WRAPPER_LINK': getdownWrapperLink,
 -    'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
 -    'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
+-    'BATCH_WRAPPER_SCRIPT': getdown_batch_wrapper_script,
 -    'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
 -    'INSTALLER_NAME': install4jInstallerName,
 -    'INSTALL4J_UTILS_DIR': install4j_utils_dir,
--    'GETDOWN_WEBSITE_DIR': getdown_website_dir,
+-    'GETDOWN_CHANNEL_DIR': getdownChannelDir,
 -    'GETDOWN_FILES_DIR': getdown_files_dir,
 -    'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
 -    'GETDOWN_DIST_DIR': getdownAppDistDir,
 -    'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
 -    'PNG_ICON_FILE': install4jPngIconFile,
 -    'BACKGROUND': install4jBackground,
+-  ]
 -
+-  def varNameMap = [
+-    'mac': 'MACOS',
+-    'windows': 'WINDOWS',
+-    'linux': 'LINUX'
 -  ]
+-  
+-  // these are the bundled OS/architecture VMs needed by install4j
+-  def osArch = [
+-    [ "mac", "x64" ],
+-    [ "mac", "aarch64" ],
+-    [ "windows", "x64" ],
+-    [ "linux", "x64" ],
+-    [ "linux", "aarch64" ]
+-  ]
+-  osArch.forEach { os, arch ->
+-    variables[ sprintf("%s_%s_JAVA_VM_DIR", varNameMap[os], arch.toUpperCase(Locale.ROOT)) ] = sprintf("%s/jre-%s-%s-%s/jre", jreInstallsDir, JAVA_INTEGER_VERSION, os, arch)
+-    // N.B. For some reason install4j requires the below filename to have underscores and not hyphens
+-    // otherwise running `gradle installers` generates a non-useful error:
+-    // `install4j: compilation failed. Reason: java.lang.NumberFormatException: For input string: "windows"`
+-    variables[ sprintf("%s_%s_JAVA_VM_TGZ", varNameMap[os], arch.toUpperCase(Locale.ROOT)) ] = sprintf("%s/tgz/jre_%s_%s_%s.tar.gz", jreInstallsDir, JAVA_INTEGER_VERSION, os, arch)
+-  }
 -
 -  //println("INSTALL4J VARIABLES:")
 -  //variables.each{k,v->println("${k}=${v}")}
 -  }
 -  //verbose=true
 -
--  inputs.dir(getdownWebsiteDir)
+-  inputs.dir(getdownAppBaseDir)
 -  inputs.file(install4jConfFile)
 -  inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
--  inputs.dir(macosJavaVMDir)
--  inputs.dir(windowsJavaVMDir)
 -  outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
 -}
 -
+-def getDataHash(File myFile) {
+-  HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
+-  return myFile.exists()
+-  ? [
+-      "file" : myFile.getName(),
+-      "filesize" : myFile.length(),
+-      "sha256" : hash.toString()
+-    ]
+-  : null
+-}
+-
+-def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
+-  def hash = [
+-    "channel" : getdownChannelName,
+-    "date" : getDate("yyyy-MM-dd HH:mm:ss"),
+-    "git-commit" : "${gitHash} [${gitBranch}]",
+-    "version" : JALVIEW_VERSION
+-  ]
+-  // install4j installer files
+-  if (installersOutputTxt.exists()) {
+-    def idHash = [:]
+-    installersOutputTxt.readLines().each { def line ->
+-      if (line.startsWith("#")) {
+-        return;
+-      }
+-      line.replaceAll("\n","")
+-      def vals = line.split("\t")
+-      def filename = vals[3]
+-      def filesize = file(filename).length()
+-      filename = filename.replaceAll(/^.*\//, "")
+-      hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
+-      idHash."${filename}" = vals[0]
+-    }
+-    if (install4jCheckSums && installersSha256.exists()) {
+-      installersSha256.readLines().each { def line ->
+-        if (line.startsWith("#")) {
+-          return;
+-        }
+-        line.replaceAll("\n","")
+-        def vals = line.split(/\s+\*?/)
+-        def filename = vals[1]
+-        def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
+-      }
+-    }
+-  }
+-
+-  [
+-    "JAR": shadowJar.archiveFile, // executable JAR
+-    "JVL": getdownVersionLaunchJvl, // version JVL
+-    "SOURCE": sourceDist.archiveFile // source TGZ
+-  ].each { key, value ->
+-    def file = file(value)
+-    if (file.exists()) {
+-      def fileHash = getDataHash(file)
+-      if (fileHash != null) {
+-        hash."${key}" = fileHash;
+-      }
+-    }
+-  }
+-  return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
+-}
+-
+-task staticMakeInstallersJsonFile {
+-  doFirst {
+-    def output = findProperty("i4j_output")
+-    def sha256 = findProperty("i4j_sha256")
+-    def json = findProperty("i4j_json")
+-    if (output == null || sha256 == null || json == null) {
+-      throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
+-    }
+-    writeDataJsonFile(file(output), file(sha256), file(json))
+-  }
+-}
+-
+-task installers {
+-  dependsOn installerFiles
+-}
+-
 -
 -spotless {
 -  java {
 -  }
 -}
 -
+-task createSourceReleaseProperties(type: WriteProperties) {
+-  group = "distribution"
+-  description = "Create the source RELEASE properties file"
+-  
+-  def sourceTarBuildDir = "${buildDir}/sourceTar"
+-  def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
+-  outputFile (sourceReleasePropertiesFile)
+-
+-  doFirst {
+-    releaseProps.each{ key, val -> property key, val }
+-    property "git.branch", gitBranch
+-    property "git.hash", gitHash
+-  }
+-
+-  outputs.file(outputFile)
+-}
 -
 -task sourceDist(type: Tar) {
 -  group "distribution"
 -
 -  dependsOn createBuildProperties
 -  dependsOn convertMdFiles
+-  dependsOn eclipseAllPreferences
+-  dependsOn createSourceReleaseProperties
+-
 -
--  def VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
--  def outputFileName = "${project.name}_${VERSION_UNDERSCORES}.tar.gz"
+-  def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
 -  archiveFileName = outputFileName
 -  
 -  compression Compression.GZIP
 -    "*locales/**",
 -    "utils/InstallAnywhere",
 -    "**/*.log",
+-    "RELEASE",
 -  ] 
 -  def PROCESS_FILES=[
 -    "AUTHORS",
 -    "FEATURETODO",
 -    "LICENSE",
 -    "**/README",
--    "RELEASE",
 -    "THIRDPARTYLIBS",
 -    "TESTNG",
 -    "build.gradle",
 -    "**/*.sh",
 -  ]
 -  def INCLUDE_FILES=[
--    ".settings/org.eclipse.jdt.core.jalview.prefs",
+-    ".classpath",
+-    ".settings/org.eclipse.buildship.core.prefs",
+-    ".settings/org.eclipse.jdt.core.prefs"
 -  ]
 -
 -  from(jalviewDir) {
 -    exclude ("utils/InstallAnywhere")
 -
 -    exclude (getdown_files_dir)
--    exclude (getdown_website_dir)
+-    // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
+-    //exclude (getdown_website_dir)
+-    //exclude (getdown_archive_dir)
 -
 -    // exluding these as not using jars as modules yet
 -    exclude ("${j11modDir}/**/*.jar")
 -    })
 -  }
 -
+-  def sourceTarBuildDir = "${buildDir}/sourceTar"
+-  from(sourceTarBuildDir) {
+-    // this includes the appended RELEASE properties file
+-  }
 -}
 -
+-task dataInstallersJson {
+-  group "website"
+-  description "Create the installers-VERSION.json data file for installer files created"
+-
+-  mustRunAfter installers
+-  mustRunAfter shadowJar
+-  mustRunAfter sourceDist
+-  mustRunAfter getdownArchive
+-
+-  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
+-  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
+-
+-  if (installersOutputTxt.exists()) {
+-    inputs.file(installersOutputTxt)
+-  }
+-  if (install4jCheckSums && installersSha256.exists()) {
+-    inputs.file(installersSha256)
+-  }
+-  [
+-    shadowJar.archiveFile, // executable JAR
+-    getdownVersionLaunchJvl, // version JVL
+-    sourceDist.archiveFile // source TGZ
+-  ].each { fileName ->
+-    if (file(fileName).exists()) {
+-      inputs.file(fileName)
+-    }
+-  }
+-
+-  outputs.file(hugoDataJsonFile)
+-
+-  doFirst {
+-    writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
+-  }
+-}
 -
 -task helppages {
+-  group "help"
+-  description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
+-
 -  dependsOn copyHelp
 -  dependsOn pubhtmlhelp
 -  
 -  preserve {
 -    include "**"
 -  }
+-
+-  // should this be exclude really ?
+-  duplicatesStrategy "INCLUDE"
+-
 -  outputs.files outputFiles
 -  inputs.files inputFiles
 -}
 -      println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
 -    }
 -  }
+-
 -  //def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview/org.eclipse.jdt.core"
 -  def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview"
 -  executable(eclipseBinary)
 -          new org.apache.tools.ant.util.TeeOutputStream(
 -            logErrFOS,
 -            stderr),
--          errorOutput)
+-          System.err)
 -    } else {
 -      standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
 -        logOutFOS,
 -task eclipseAutoBuildTask {
 -  //dependsOn jalviewjsIDE_checkJ2sPlugin
 -  //dependsOn jalviewjsIDE_PrepareSite
-+  outputs.file("${outputDir}/${archiveFileName}")
- }
+-}
+-
 -
 -task jalviewjs {
 -  group "JalviewJS"
 -  description "Build the site"
 -  dependsOn jalviewjsBuildSite
--}
++  outputs.file("${outputDir}/${archiveFileName}")
+ }
index a0e8cd9..ce7f136 100644 (file)
@@ -300,6 +300,59 @@ task copyHelp(type: Copy) {
   outputs.dir(outputDir)
 }
 
+task releasesTemplates {
+  group "help"
+  description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
+
+  dependsOn copyHelp
+
+  def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
+  def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
+  def releasesHtmlFile = file("${helpBuildDir}/${help_dir}/${releases_html}")
+  def whatsnewHtmlFile = file("${helpBuildDir}/${help_dir}/${whatsnew_html}")
+  def releasesMdDir = "${jalviewDir}/${releases_dir}"
+  def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
+
+  doFirst {
+    def JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
+    def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+    def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+
+    if (CHANNEL == "RELEASE") {
+      if (!releaseMdFile.exists()) {
+        throw new GradleException("File ${releaseMdFile} must be created for RELEASE")
+      }
+      if (!whatsnewMdFile.exists()) {
+        throw new GradleException("File ${whatsnewMdFile} must be created for RELEASE")
+      }
+    }
+
+    def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
+    def releaseFilesDates = releaseFiles.collectEntries {
+      [(it): getDate("")]
+    }
+    releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
+
+    def releasesTemplate = releasesTemplateFile.text
+    def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
+    def versionTemplate = m[0][1]
+
+    def versionsHtml = ""
+    def linkedVersions = []
+
+    releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
+    releasesHtmlFile.text = releasesTemplate
+
+    whatsnewHtmlFile.text = "Debian build " + getDate("yyyy-MM-dd HH:mm:ss")
+  }
+
+  inputs.file(releasesTemplateFile)
+  inputs.file(whatsnewTemplateFile)
+  inputs.dir(releasesMdDir)
+  inputs.dir(whatsnewMdDir)
+  outputs.file(releasesHtmlFile)
+  outputs.file(whatsnewHtmlFile)
+}
 
 task copyResources(type: Copy) {
   group = "build"
@@ -389,6 +442,7 @@ task prepare {
   dependsOn buildResources
   dependsOn copyDocs
   dependsOn copyHelp
+  dependsOn releasesTemplates
   dependsOn buildIndices
 }
 
@@ -453,7 +507,7 @@ task linkCheck(type: JavaExec) {
   def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
   classpath = files("${jalviewDir}/${utils_dir}")
   main = "HelpLinksChecker"
-  workingDir = jalviewDir
+  workingDir = "${helpBuildDir}"
   args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]
 
   def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
index 3bc98ae..b0887e3 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<install4j version="10.0.5" transformSequenceNumber="10">
+<install4j version="10.0.6" transformSequenceNumber="10">
   <directoryPresets config="bin/Jalview" />
   <application name="${compiler:JALVIEW_APPLICATION_NAME}" applicationId="${compiler:WINDOWS_APPLICATION_ID}" mediaDir="${compiler:BUILD_DIR}" lzmaCompression="true" shortName="${compiler:INTERNAL_ID}" publisher="University of Dundee" publisherWeb="https://www.jalview.org/" version="${compiler:JALVIEW_VERSION}" allPathsRelative="true" macVolumeId="5aac4968c304f65" javaMinVersion="${compiler:JAVA_MIN_VERSION}" javaMaxVersion="${compiler:JAVA_MAX_VERSION}" allowBetaVM="true" jdkMode="jdk" jdkName="JDK 11.0">
     <searchSequence>
@@ -1477,7 +1477,7 @@ ${compiler:JALVIEW_APPLICATION_NAME} will now launch.</property>
       </exclude>
       <jreBundle jreBundleSource="preCreated" includedJre="${compiler:WINDOWS_X64_JAVA_VM_TGZ}" manualJreEntry="true" />
     </windows>
-    <macosArchive name="macOS x64 Disk Image" id="878" customizedId="MACOS-X64-DMG" mediaFileName="${compiler:APPLICATION_FOLDER}-${compiler:JALVIEW_VERSION}-${compiler:sys.platform}-x64-java_${compiler:JAVA_INTEGER_VERSION}" volumeName="${compiler:INSTALLER_NAME}" launcherId="737" setupAppId="2746">
+    <macosArchive name="macOS x64 (intel) Disk Image" id="878" customizedId="MACOS-X64-DMG" mediaFileName="${compiler:APPLICATION_FOLDER}-${compiler:JALVIEW_VERSION}-${compiler:sys.platform}-x64-java_${compiler:JAVA_INTEGER_VERSION}" volumeName="${compiler:INSTALLER_NAME}" launcherId="737" setupAppId="2746">
       <exclude>
         <entry defaultFileset="true" />
         <entry filesetId="2803" />
@@ -1494,7 +1494,7 @@ ${compiler:JALVIEW_APPLICATION_NAME} will now launch.</property>
         <symlink name="${compiler:JALVIEW_APPLICATION_NAME}.app/Contents/Resources/app/jre/Contents/Home/bin/${compiler:JALVIEW_NAME}" target="java" />
       </topLevelFiles>
     </macosArchive>
-    <macosArchive name="macOS aarch64 Disk Image" id="2796" customizedId="MACOS-AARCH64-DMG" mediaFileName="${compiler:APPLICATION_FOLDER}-${compiler:JALVIEW_VERSION}-${compiler:sys.platform}-aarch64-java_${compiler:JAVA_INTEGER_VERSION}" volumeName="${compiler:INSTALLER_NAME}" architecture="aarch64" launcherId="737" setupAppId="2746">
+    <macosArchive name="macOS aarch64 (Apple Silicon) Disk Image" id="2796" customizedId="MACOS-AARCH64-DMG" mediaFileName="${compiler:APPLICATION_FOLDER}-${compiler:JALVIEW_VERSION}-${compiler:sys.platform}-aarch64-java_${compiler:JAVA_INTEGER_VERSION}" volumeName="${compiler:INSTALLER_NAME}" architecture="aarch64" launcherId="737" setupAppId="2746">
       <exclude>
         <entry defaultFileset="true" />
         <entry filesetId="2801" />
diff --git a/utils/jalviewjs/chromium_test/jalview_bin_Jalview-stderr.html b/utils/jalviewjs/chromium_test/jalview_bin_Jalview-stderr.html
new file mode 100644 (file)
index 0000000..bf7a678
--- /dev/null
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>SwingJS test Jalview</title><meta charset="utf-8" />
+<script src="swingjs/swingjs2.js"></script>
+<script>
+if (!self.SwingJS)alert('swingjs2.js was not found. It needs to be in swingjs folder in the same directory as ' + document.location.href)
+Info = {
+  code: null,
+  main: "jalview.bin.Jalview",
+  core: "NONE",
+       width: 850,
+       height: 550,
+  readyFunction: null,
+       serverURL: 'https://chemapps.stolaf.edu/jmol/jsmol/php/jsmol.php',
+       j2sPath: 'swingjs/j2s',
+       console: window.console,
+       allowjavascript: true
+}
+</script>
+</head>
+<body>
+<script>
+// we define console.err because swingjs2.js calls it instead of console.error
+window.console.err = function() {
+       this.error.apply(this,arguments);
+}
+SwingJS.getApplet('testApplet', Info)
+getClassList = function(){J2S._saveFile('_j2sclasslist.txt', Clazz.ClassFilesLoaded.sort().join('\n'))}
+</script>
+<div style="position:absolute;left:900px;top:30px;width:600px;height:300px;">
+<div id="sysoutdiv" contentEditable="true" style="border:1px solid green;width:100%;height:95%;overflow:auto"></div>
+This is System.out. <a href="javascript:testApplet._clearConsole()">clear it</a> <br>Add ?j2snocore to URL to see full class list; ?j2sdebug to use uncompressed j2s/core files <br><a href="javascript:getClassList()">get _j2sClassList.txt</a>
+</div>
+</body>
+</html>