Merge branch 'task/JAL-3608_property_set_laf_and_tests' into develop
authorJim Procter <jprocter@issues.jalview.org>
Wed, 12 Aug 2020 10:15:54 +0000 (11:15 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Wed, 12 Aug 2020 10:15:54 +0000 (11:15 +0100)
198 files changed:
build.gradle
doc/building.html
doc/building.md
doc/releaseprocess.html [new file with mode: 0644]
examples/groovy/ComputePeptideVariants.groovy [new file with mode: 0644]
gradle.properties
help/help/help.jhm
help/help/helpTOC.xml
help/help/html/features/clarguments.html
help/help/html/features/importvcf.html
help/help/html/features/seqfeaturereport.html [new file with mode: 0644]
help/help/html/features/seqfeaturesrep.png [new file with mode: 0644]
help/help/html/features/splitView.html
help/help/html/index.html
help/help/html/memory.html
help/help/html/menus/popupMenu.html
help/help/html/releases.html
help/help/html/webServices/AACon.html
help/help/html/webServices/msaclient.html
help/help/html/whatsNew.html
j11lib/Jmol-14.29.17-no_netscape.jar [deleted file]
j11lib/Jmol-15.1.3.jar [new file with mode: 0644]
j11lib/htsjdk-2.12.0.jar [deleted file]
j11lib/htsjdk-2.23.0.jar [new file with mode: 0644]
j11lib/intervalstore-v1.0.jar [deleted file]
j11lib/intervalstore-v1.1.jar [new file with mode: 0644]
j8lib/htsjdk-2.12.0.jar [deleted file]
j8lib/htsjdk-2.23.0.jar [new file with mode: 0644]
j8lib/intervalstore-v1.0.jar [deleted file]
j8lib/intervalstore-v1.1.jar [new file with mode: 0644]
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraManager.java
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
src/jalview/analysis/GeneticCodeI.java
src/jalview/analysis/GeneticCodes.java
src/jalview/api/FinderI.java
src/jalview/api/structures/JalviewStructureDisplayI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AppletJmol.java
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/appletgui/ExtJmol.java
src/jalview/appletgui/UserDefinedColours.java
src/jalview/bin/Cache.java
src/jalview/bin/GetMemory.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewTaskbar.java
src/jalview/bin/Launcher.java
src/jalview/bin/MemorySetting.java
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/GeneLociI.java
src/jalview/datamodel/GeneLocus.java
src/jalview/datamodel/MappedFeatures.java
src/jalview/datamodel/Point.java
src/jalview/datamodel/SearchResults.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/features/FeatureAttributeType.java
src/jalview/datamodel/features/FeatureAttributes.java
src/jalview/datamodel/features/FeatureMatcher.java
src/jalview/datamodel/features/FeatureMatcherI.java
src/jalview/datamodel/features/FeatureMatcherSet.java
src/jalview/datamodel/features/FeatureMatcherSetI.java
src/jalview/datamodel/features/FeatureSource.java
src/jalview/datamodel/features/FeatureSourceI.java
src/jalview/datamodel/features/FeatureSources.java
src/jalview/datamodel/features/FeatureStore.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/ext/android/ContainerHelpers.java
src/jalview/ext/android/SparseDoubleArray.java
src/jalview/ext/android/SparseIntArray.java
src/jalview/ext/android/SparseShortArray.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblMap.java
src/jalview/ext/htsjdk/VCFReader.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/pymol/PymolCommands.java [new file with mode: 0644]
src/jalview/ext/pymol/PymolManager.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/AtomSpecModel.java [deleted file]
src/jalview/ext/rbvi/chimera/ChimeraCommands.java
src/jalview/ext/rbvi/chimera/ChimeraListener.java
src/jalview/ext/rbvi/chimera/ChimeraXCommands.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/ChimeraXManager.java [new file with mode: 0644]
src/jalview/ext/rbvi/chimera/JalviewChimeraBinding.java
src/jalview/fts/core/FTSRestClient.java
src/jalview/gui/APQHandlers.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AnnotationColourChooser.java
src/jalview/gui/AnnotationColumnChooser.java
src/jalview/gui/AnnotationRowFilter.java
src/jalview/gui/AppJmol.java
src/jalview/gui/AppJmolBinding.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/ChimeraXViewFrame.java [new file with mode: 0644]
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JalviewBooleanRadioButtons.java
src/jalview/gui/JalviewChimeraBindingModel.java
src/jalview/gui/JalviewChimeraXBindingModel.java [new file with mode: 0644]
src/jalview/gui/OptsAndParamsPage.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/PymolBindingModel.java [new file with mode: 0644]
src/jalview/gui/PymolViewer.java [new file with mode: 0644]
src/jalview/gui/SeqPanel.java
src/jalview/gui/Slider.java [new file with mode: 0644]
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/ViewSelectionMenu.java
src/jalview/io/BackupFilenameFilter.java
src/jalview/io/BackupFilenameParts.java
src/jalview/io/BackupFiles.java
src/jalview/io/BackupFilesPresetEntry.java
src/jalview/io/FeaturesFile.java
src/jalview/io/FileParse.java
src/jalview/io/IntKeyStringValueEntry.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/VamsasAppDatastore.java
src/jalview/io/vamsas/Rangetype.java
src/jalview/io/vcf/VCFLoader.java
src/jalview/javascript/MouseOverStructureListener.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureViewer.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/structure/AtomSpec.java
src/jalview/structure/AtomSpecModel.java [new file with mode: 0644]
src/jalview/structure/StructureCommand.java [new file with mode: 0644]
src/jalview/structure/StructureCommandI.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsBase.java [new file with mode: 0644]
src/jalview/structure/StructureCommandsI.java [new file with mode: 0644]
src/jalview/structure/StructureSelectionManager.java
src/jalview/structures/models/AAStructureBindingModel.java
src/jalview/util/DBRefUtils.java
src/jalview/util/JSONUtils.java
src/jalview/util/MathUtils.java
src/jalview/util/ShortcutKeyMaskExWrapper.java
src/jalview/util/ShortcutKeyMaskExWrapper11.java
src/jalview/util/ShortcutKeyMaskExWrapper8.java
src/jalview/util/ShortcutKeyMaskExWrapperI.java
src/jalview/util/matcher/Condition.java
src/jalview/util/matcher/Matcher.java
src/jalview/util/matcher/MatcherI.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/HttpClientUtils.java
src/jalview/ws/dbsources/Pfam.java
src/jalview/ws/dbsources/PfamFull.java
src/jalview/ws/dbsources/PfamSeed.java
src/jalview/ws/dbsources/Rfam.java
src/jalview/ws/dbsources/RfamFull.java
src/jalview/ws/dbsources/RfamSeed.java
src/jalview/ws/dbsources/Xfam.java
src/jalview/ws/sifts/SiftsClient.java
src/mc_view/AppletPDBViewer.java
test/jalview/datamodel/SearchResultsTest.java
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/features/FeatureMatcherTest.java
test/jalview/datamodel/features/SequenceFeaturesTest.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/pymol/PymolCommandsTest.java [new file with mode: 0644]
test/jalview/ext/pymol/PymolManagerTest.java [new file with mode: 0644]
test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java [deleted file]
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraConnect.java
test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java [new file with mode: 0644]
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/gui/FeatureSettingsTest.java
test/jalview/gui/FreeUpMemoryTest.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/structure/AtomSpecModelTest.java [new file with mode: 0644]
test/jalview/structure/AtomSpecTest.java
test/jalview/structure/StructureCommandTest.java [new file with mode: 0644]
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java
test/jalview/util/DBRefUtilsTest.java
test/jalview/util/matcher/MatcherTest.java
test/jalview/ws/dbsources/PfamFullTest.java
test/jalview/ws/dbsources/PfamSeedTest.java
test/jalview/ws/dbsources/RfamFullTest.java
test/jalview/ws/dbsources/RfamSeedTest.java
utils/MessageBundleChecker.java
utils/clover/lib/clover-ant-4.4.1.jar [new file with mode: 0644]
utils/cmd-nox.sh [new file with mode: 0755]
utils/doc/github.css [moved from doc/github.css with 100% similarity]
utils/eclipse/org.eclipse.jdt.ui.prefs
utils/gradle-nox.sh [new symlink]
utils/install4j/install4j8_template.install4j

index 453ad5d..bfd7a8f 100644 (file)
@@ -1,3 +1,6 @@
+/* Convention for properties.  Read from gradle.properties, use lower_case_underlines for property names.
+ * For properties set within build.gradle, use camelCaseNoSpace.
+ */
 import org.apache.tools.ant.filters.ReplaceTokens
 import org.gradle.internal.os.OperatingSystem
 import org.gradle.plugins.ide.internal.generator.PropertiesPersistableConfigurationObject
@@ -9,7 +12,16 @@ import java.security.MessageDigest
 import groovy.transform.ExternalizeMethods
 import groovy.util.XmlParser
 import groovy.xml.XmlUtil
-
+import com.vladsch.flexmark.util.ast.Node
+import com.vladsch.flexmark.html.HtmlRenderer
+import com.vladsch.flexmark.parser.Parser
+import com.vladsch.flexmark.util.data.MutableDataSet
+import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
+import com.vladsch.flexmark.ext.tables.TablesExtension
+import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
+import com.vladsch.flexmark.ext.autolink.AutolinkExtension
+import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
+import com.vladsch.flexmark.ext.toc.TocExtension
 
 buildscript {
   repositories {
@@ -17,7 +29,7 @@ buildscript {
     mavenLocal()
   }
   dependencies {
-    classpath 'org.openclover:clover:4.4.1'
+    classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
   }
 }
 
@@ -30,6 +42,7 @@ plugins {
   id 'com.github.johnrengelman.shadow' version '4.0.3'
   id 'com.install4j.gradle' version '8.0.4'
   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'
 }
 
 repositories {
@@ -82,6 +95,26 @@ ext {
     }
   }
 
+  ////  
+  // Import releaseProps from the RELEASE file
+  // or a file specified via JALVIEW_RELEASE_FILE if defined
+  // Expect jalview.version and target release branch in jalview.release        
+  def releaseProps = new Properties();
+  def releasePropFile = findProperty("JALVIEW_RELEASE_FILE");
+  def defaultReleasePropFile = "${jalviewDirAbsolutePath}/RELEASE";
+  try {
+    (new File(releasePropFile!=null ? releasePropFile : defaultReleasePropFile)).withInputStream { 
+     releaseProps.load(it)
+    }
+  } catch (Exception fileLoadError) {
+    throw new Error("Couldn't load release properties file "+(releasePropFile==null ? defaultReleasePropFile : "from custom location: releasePropFile"),fileLoadError);
+  }
+  ////
+  // Set JALVIEW_VERSION if it is not already set
+  if (findProperty(JALVIEW_VERSION)==null || "".equals(JALVIEW_VERSION)) {
+    JALVIEW_VERSION = releaseProps.get("jalview.version")
+  }
+  
   // this property set when running Eclipse headlessly
   j2sHeadlessBuildProperty = string("net.sf.j2s.core.headlessbuild")
   // this property set by Eclipse
@@ -103,8 +136,7 @@ ext {
   J2S_ENABLED = (project.hasProperty('j2s.compiler.status') && project['j2s.compiler.status'] != null && project['j2s.compiler.status'] == "enable")
   if (J2S_ENABLED) {
     println("J2S ENABLED")
-  }
-  
+  } 
   /* *-/
   System.properties.sort { it.key }.each {
     key, val -> println("SYSTEM PROPERTY ${key}='${val}'")
@@ -120,21 +152,25 @@ ext {
   sourceDir = string("${jalviewDir}/${bareSourceDir}")
   resourceDir = string("${jalviewDir}/${resource_dir}")
   bareTestSourceDir = string(test_source_dir)
-  testSourceDir = string("${jalviewDir}/${bareTestSourceDir}")
+  testDir = string("${jalviewDir}/${bareTestSourceDir}")
 
-  // clover
-  cloverInstrDir = file("${buildDir}/${cloverSourcesInstrDir}")
-  cloverDb = string("${buildDir}/clover/clover.db")
   classesDir = string("${jalviewDir}/${classes_dir}")
-  if (clover.equals("true")) {
-    use_clover = true
-    classesDir = string("${buildDir}/${cloverClassesDir}")
-  } else {
-    use_clover = false
-    classesDir = string("${jalviewDir}/${classes_dir}")
-  }
 
-  classes = classesDir
+  // clover
+  useClover = clover.equals("true")
+  cloverBuildDir = "${buildDir}/clover"
+  cloverInstrDir = file("${cloverBuildDir}/clover-instr")
+  cloverClassesDir = file("${cloverBuildDir}/clover-classes")
+  cloverReportDir = file("${buildDir}/reports/clover")
+  cloverTestInstrDir = file("${cloverBuildDir}/clover-test-instr")
+  cloverTestClassesDir = file("${cloverBuildDir}/clover-test-classes")
+  //cloverTestClassesDir = cloverClassesDir
+  cloverDb = string("${cloverBuildDir}/clover.db")
+
+  resourceClassesDir = useClover ? cloverClassesDir : classesDir
+
+  testSourceDir = useClover ? cloverTestInstrDir : testDir
+  testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"
 
   getdownWebsiteDir = string("${jalviewDir}/${getdown_website_dir}/${JAVA_VERSION}")
   buildDist = true
@@ -163,8 +199,8 @@ ext {
     getdownAppBase = string("${bamboo_channelbase}/${bamboo_planKey}${bamboo_getdown_channel_suffix}/${JAVA_VERSION}")
     jvlChannelName += "_${getdownChannelName}"
     // automatically add the test group Not-bamboo for exclusion 
-    if ("".equals(testngExcludedGroups)) { 
-      testngExcludedGroups = "Not-bamboo"
+    if ("".equals(testng_excluded_groups)) { 
+      testng_excluded_groups = "Not-bamboo"
     }
     install4jExtraScheme = "jalviewb"
     break
@@ -172,11 +208,6 @@ ext {
     case "RELEASE":
     getdownAppDistDir = getdown_app_dir_release
     reportRsyncCommand = true
-    // Don't ignore transpile errors for release build
-    if (jalviewjs_ignore_transpile_errors.equals("true")) {
-      jalviewjs_ignore_transpile_errors = "false"
-      println("Setting jalviewjs_ignore_transpile_errors to 'false'")
-    }
     install4jSuffix = ""
     install4jDSStore = "DS_Store"
     install4jDMGBackgroundImage = "jalview_dmg_background.png"
@@ -187,11 +218,11 @@ ext {
     getdownChannelName = CHANNEL.toLowerCase()+"/${JALVIEW_VERSION}"
     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
     getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
-    if (!file("${ARCHIVEDIR}/${packageDir}").exists()) {
+    if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
       throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
     } else {
-      packageDir = string("${ARCHIVEDIR}/${packageDir}")
-      buildProperties = string("${buildDir}/archive/${build_properties_file}")
+      package_dir = string("${ARCHIVEDIR}/${package_dir}")
+      buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
       buildDist = false
     }
     reportRsyncCommand = true
@@ -202,11 +233,11 @@ ext {
     getdownChannelName = string("archive/${JALVIEW_VERSION}")
     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
     getdownAppBase = file(getdownWebsiteDir).toURI().toString()
-    if (!file("${ARCHIVEDIR}/${packageDir}").exists()) {
+    if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
       throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
     } else {
-      packageDir = string("${ARCHIVEDIR}/${packageDir}")
-      buildProperties = string("${buildDir}/archive/${build_properties_file}")
+      package_dir = string("${ARCHIVEDIR}/${package_dir}")
+      buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
       buildDist = false
     }
     reportRsyncCommand = true
@@ -217,6 +248,11 @@ ext {
 
     case "DEVELOP":
     reportRsyncCommand = true
+    
+    // DEVELOP-RELEASE is usually associated with a Jalview release series so set the version
+    JALVIEW_VERSION=JALVIEW_VERSION+"-develop"
+    
+    install4jSuffix = "Develop"
     install4jDSStore = "DS_Store-DEVELOP"
     install4jDMGBackgroundImage = "jalview_dmg_background-DEVELOP.png"
     install4jExtraScheme = "jalviewd"
@@ -230,7 +266,7 @@ ext {
       jalviewjs_ignore_transpile_errors = "false"
       println("Setting jalviewjs_ignore_transpile_errors to 'false'")
     }
-    JALVIEW_VERSION = "TEST"
+    JALVIEW_VERSION = JALVIEW_VERSION+"-test"
     install4jSuffix = "Test"
     install4jDSStore = "DS_Store-TEST-RELEASE"
     install4jDMGBackgroundImage = "jalview_dmg_background-TEST.png"
@@ -240,6 +276,8 @@ ext {
 
     case ~/^SCRATCH(|-[-\w]*)$/:
     getdownChannelName = CHANNEL
+    JALVIEW_VERSION = JALVIEW_VERSION+"-"+CHANNEL
+    
     getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
     getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
     reportRsyncCommand = true
@@ -262,6 +300,7 @@ ext {
     break
 
     case "LOCAL":
+    JALVIEW_VERSION = "TEST"
     getdownAppBase = file(getdownWebsiteDir).toURI().toString()
     getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
     install4jExtraScheme = "jalviewl"
@@ -320,8 +359,9 @@ ext {
   modules_compileClasspath = fileTree(dir: "${jalviewDir}/${j11modDir}", include: ["*.jar"])
   modules_runtimeClasspath = modules_compileClasspath
   */
-  gitHash = string("")
-  gitBranch = string("")
+  def details = versionDetails()
+  gitHash = details.gitHash
+  gitBranch = details.branchName
 
   println("Using a ${CHANNEL} profile.")
 
@@ -401,8 +441,8 @@ ext {
 
 
 
-  buildingHTML = string("${jalviewDir}/${docDir}/building.html")
-  helpFile = string("${classesDir}/${help_dir}/help.jhm")
+  buildingHTML = string("${jalviewDir}/${doc_dir}/building.html")
+  helpFile = string("${resourceClassesDir}/${help_dir}/help.jhm")
   helpParentDir = string("${jalviewDir}/${help_parent_dir}")
   helpSourceDir = string("${helpParentDir}/${help_dir}")
 
@@ -446,10 +486,9 @@ sourceSets {
       srcDirs += helpParentDir
     }
 
-    jar.destinationDir = file("${jalviewDir}/${packageDir}")
+    jar.destinationDir = file("${jalviewDir}/${package_dir}")
 
     compileClasspath = files(sourceSets.main.java.outputDir)
-    //compileClasspath += files(sourceSets.main.resources.srcDirs)
     compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
 
     runtimeClasspath = compileClasspath
@@ -457,18 +496,19 @@ sourceSets {
 
   clover {
     java {
-      srcDirs = [ cloverInstrDir ]
-      outputDir = file("${buildDir}/${cloverClassesDir}")
+      srcDirs cloverInstrDir
+      outputDir = cloverClassesDir
     }
 
     resources {
       srcDirs = sourceSets.main.resources.srcDirs
     }
-    compileClasspath = configurations.cloverRuntime + files( sourceSets.clover.java.outputDir )
-    compileClasspath += files(sourceSets.main.java.outputDir)
-    compileClasspath += sourceSets.main.compileClasspath
-    compileClasspath += fileTree(dir: "${jalviewDir}/${utilsDir}", include: ["**/*.jar"])
+
+    compileClasspath = files( sourceSets.clover.java.outputDir )
+    //compileClasspath += files( testClassesDir )
     compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
+    compileClasspath += fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
+    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])
 
     runtimeClasspath = compileClasspath
   }
@@ -476,42 +516,20 @@ sourceSets {
   test {
     java {
       srcDirs testSourceDir
-      outputDir = file("${jalviewDir}/${testOutputDir}")
+      outputDir = file(testClassesDir)
     }
 
     resources {
-      srcDirs = sourceSets.main.resources.srcDirs
+      srcDirs = useClover ? sourceSets.clover.resources.srcDirs : sourceSets.main.resources.srcDirs
     }
 
     compileClasspath = files( sourceSets.test.java.outputDir )
-
-    if (use_clover) {
-      compileClasspath = sourceSets.clover.compileClasspath
-    } else {
-      compileClasspath += files(sourceSets.main.java.outputDir)
-    }
-
-    compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
-    compileClasspath += fileTree(dir: "${jalviewDir}/${utilsDir}/testnglibs", include: ["**/*.jar"])
-    compileClasspath += fileTree(dir: "${jalviewDir}/${utilsDir}/testlibs", include: ["**/*.jar"])
+    compileClasspath += useClover ? sourceSets.clover.compileClasspath : sourceSets.main.compileClasspath
+    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])
 
     runtimeClasspath = compileClasspath
   }
-}
-
 
-// clover bits
-dependencies {
-  if (use_clover) {
-    cloverCompile 'org.openclover:clover:4.4.1'
-    testCompile 'org.openclover:clover:4.4.1'
-  }
-}
-
-
-configurations {
-  cloverRuntime
-  cloverRuntime.extendsFrom cloverCompile
 }
 
 
@@ -625,7 +643,6 @@ eclipse {
     javaRuntimeName = eclipseJavaRuntimeName
 
     // add in jalview project specific properties/preferences into eclipse core preferences
-    // and also the codestyle XML file
     file {
       withProperties { props ->
         def jalview_prefs = new Properties()
@@ -724,61 +741,209 @@ eclipseGroovyCorePreferences.mustRunAfter eclipseJdt
 /* end of eclipse preferences hack */
 
 
-task cloverInstr {
-  // only instrument source, we build test classes as normal
-  inputs.files files (sourceSets.main.allJava,sourceSets.test.allJava) // , fileTree(dir:"$jalviewDir/$testSourceDir", include: ["**/*.java"]))
-  outputs.dir cloverInstrDir
+// clover bits
 
+
+task cleanClover {
   doFirst {
-    delete cloverInstrDir
-    def argsList = [
-      "--initstring",
-      cloverDb,
-      "-d",
-      cloverInstrDir.getPath(),
-    ]
-    argsList.addAll(
-      inputs.files.files.collect(
-        { file -> file.absolutePath }
-      )
+    delete cloverBuildDir
+    delete cloverReportDir
+  }
+}
+
+
+task cloverInstrJava(type: JavaExec) {
+  group = "Verification"
+  description = "Create clover instrumented source java files"
+
+  dependsOn cleanClover
+
+  inputs.files(sourceSets.main.allJava)
+  outputs.dir(cloverInstrDir)
+
+  //classpath = fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
+  classpath = sourceSets.clover.compileClasspath
+  main = "com.atlassian.clover.CloverInstr"
+
+  def argsList = [
+    "--encoding",
+    "UTF-8",
+    "--initstring",
+    cloverDb,
+    "--destdir",
+    cloverInstrDir.getPath(),
+  ]
+  def srcFiles = sourceSets.main.allJava.files
+  argsList.addAll(
+    srcFiles.collect(
+      { file -> file.absolutePath }
     )
-    String[] args = argsList.toArray()
-    println("About to instrument "+args.length +" files")
-    com.atlassian.clover.CloverInstr.mainImpl(args)
+  )
+  args argsList.toArray()
+
+  doFirst {
+    delete cloverInstrDir
+    println("Clover: About to instrument "+srcFiles.size() +" files")
   }
 }
 
 
+task cloverInstrTests(type: JavaExec) {
+  group = "Verification"
+  description = "Create clover instrumented source test files"
+
+  dependsOn cleanClover
+
+  inputs.files(testDir)
+  outputs.dir(cloverTestInstrDir)
+
+  classpath = sourceSets.clover.compileClasspath
+  main = "com.atlassian.clover.CloverInstr"
+
+  def argsList = [
+    "--encoding",
+    "UTF-8",
+    "--initstring",
+    cloverDb,
+    "--srcdir",
+    testDir,
+    "--destdir",
+    cloverTestInstrDir.getPath(),
+  ]
+  args argsList.toArray()
+
+  doFirst {
+    delete cloverTestInstrDir
+    println("Clover: About to instrument test files")
+  }
+}
+
+
+task cloverInstr {
+  group = "Verification"
+  description = "Create clover instrumented all source files"
+
+  dependsOn cloverInstrJava
+  dependsOn cloverInstrTests
+}
+
+
 cloverClasses.dependsOn cloverInstr
 
 
-task cloverReport {
+task cloverConsoleReport(type: JavaExec) {
   group = "Verification"
-  description = "Creates the Clover report"
-  inputs.dir "${buildDir}/clover"
-  outputs.dir "${reportsDir}/clover"
+  description = "Creates clover console report"
+
   onlyIf {
     file(cloverDb).exists()
   }
-  doFirst {
-    def argsList = [
-      "--initstring",
-      cloverDb,
-      "-o",
-      "${reportsDir}/clover"
-    ]
-    String[] args = argsList.toArray()
-    com.atlassian.clover.reporters.html.HtmlReporter.runReport(args)
 
-    // and generate ${reportsDir}/clover/clover.xml
-    args = [
-      "--initstring",
-      cloverDb,
-      "-o",
-      "${reportsDir}/clover/clover.xml"
-    ].toArray()
-    com.atlassian.clover.reporters.xml.XMLReporter.runReport(args)
+  inputs.dir cloverClassesDir
+
+  classpath = sourceSets.clover.runtimeClasspath
+  main = "com.atlassian.clover.reporters.console.ConsoleReporter"
+
+  if (cloverreport_mem.length() > 0) {
+    maxHeapSize = cloverreport_mem
+  }
+  if (cloverreport_jvmargs.length() > 0) {
+    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
+  }
+
+  def argsList = [
+    "--alwaysreport",
+    "--initstring",
+    cloverDb,
+    "--unittests"
+  ]
+
+  args argsList.toArray()
+}
+
+
+task cloverHtmlReport(type: JavaExec) {
+  group = "Verification"
+  description = "Creates clover HTML report"
+
+  onlyIf {
+    file(cloverDb).exists()
+  }
+
+  def cloverHtmlDir = cloverReportDir
+  inputs.dir cloverClassesDir
+  outputs.dir cloverHtmlDir
+
+  classpath = sourceSets.clover.runtimeClasspath
+  main = "com.atlassian.clover.reporters.html.HtmlReporter"
+
+  if (cloverreport_mem.length() > 0) {
+    maxHeapSize = cloverreport_mem
+  }
+  if (cloverreport_jvmargs.length() > 0) {
+    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
+  }
+
+  def argsList = [
+    "--alwaysreport",
+    "--initstring",
+    cloverDb,
+    "--outputdir",
+    cloverHtmlDir
+  ]
+
+  if (cloverreport_html_options.length() > 0) {
+    argsList += cloverreport_html_options.split(" ")
+  }
+
+  args argsList.toArray()
+}
+
+
+task cloverXmlReport(type: JavaExec) {
+  group = "Verification"
+  description = "Creates clover XML report"
+
+  onlyIf {
+    file(cloverDb).exists()
   }
+
+  def cloverXmlFile = "${cloverReportDir}/clover.xml"
+  inputs.dir cloverClassesDir
+  outputs.file cloverXmlFile
+
+  classpath = sourceSets.clover.runtimeClasspath
+  main = "com.atlassian.clover.reporters.xml.XMLReporter"
+
+  if (cloverreport_mem.length() > 0) {
+    maxHeapSize = cloverreport_mem
+  }
+  if (cloverreport_jvmargs.length() > 0) {
+    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
+  }
+
+  def argsList = [
+    "--alwaysreport",
+    "--initstring",
+    cloverDb,
+    "--outfile",
+    cloverXmlFile
+  ]
+
+  if (cloverreport_xml_options.length() > 0) {
+    argsList += cloverreport_xml_options.split(" ")
+  }
+
+  args argsList.toArray()
+}
+
+
+task cloverReport {
+  group = "Verification"
+  description = "Creates clover reports"
+
+  dependsOn cloverXmlReport
+  dependsOn cloverHtmlReport
 }
 
 
@@ -790,43 +955,29 @@ compileCloverJava {
     options.compilerArgs += additional_compiler_args
     print ("Setting target compatibility to "+targetCompatibility+"\n")
   }
-  classpath += configurations.cloverRuntime
-}
-
-
-task cleanClover {
-  doFirst {
-    delete cloverInstrDir
-    delete cloverDb
-  }
+  //classpath += configurations.cloverRuntime
 }
 // end clover bits
 
 
 compileJava {
-       
+  // 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.encoding = "UTF-8"
   doFirst {
-    print ("Setting target compatibility to "+targetCompatibility+"\n")
+    print ("Setting target compatibility to "+compile_target_compatibility+"\n")
   }
 
 }
 
 
 compileTestJava {
-  if (use_clover) {
-    dependsOn compileCloverJava
-    classpath += configurations.cloverRuntime
-  } else {
-    classpath += sourceSets.main.runtimeClasspath
-  }
+  sourceCompatibility = compile_source_compatibility
+  targetCompatibility = compile_target_compatibility
+  options.compilerArgs = additional_compiler_args
   doFirst {
-    sourceCompatibility = compile_source_compatibility
-    targetCompatibility = compile_target_compatibility
-    options.compilerArgs = additional_compiler_args
     print ("Setting target compatibility to "+targetCompatibility+"\n")
   }
 }
@@ -854,33 +1005,10 @@ def getDate(format) {
 }
 
 
-task setGitVals {
-  def hashStdOut = new ByteArrayOutputStream()
-  exec {
-    commandLine "git", "rev-parse", "--short", "HEAD"
-    standardOutput = hashStdOut
-    ignoreExitValue true
-  }
-
-  def branchStdOut = new ByteArrayOutputStream()
-  exec {
-    commandLine "git", "rev-parse", "--abbrev-ref", "HEAD"
-    standardOutput = branchStdOut
-    ignoreExitValue true
-  }
-
-  gitHash = hashStdOut.toString().trim()
-  gitBranch = branchStdOut.toString().trim()
-
-  outputs.upToDateWhen { false }
-}
-
-
 task createBuildProperties(type: WriteProperties) {
   group = "build"
   description = "Create the ${buildProperties} file"
   
-  dependsOn setGitVals
   inputs.dir(sourceDir)
   inputs.dir(resourceDir)
   file(buildProperties).getParentFile().mkdirs()
@@ -909,53 +1037,116 @@ task cleanBuildingHTML(type: Delete) {
 }
 
 
-task convertBuildingMD(type: Exec) {
+def convertMdToHtml (FileTree mdFiles, File cssFile) {
+  MutableDataSet options = new MutableDataSet()
+
+  def extensions = new ArrayList<>()
+  extensions.add(AnchorLinkExtension.create()) 
+  extensions.add(AutolinkExtension.create())
+  extensions.add(StrikethroughExtension.create())
+  extensions.add(TaskListExtension.create())
+  extensions.add(TablesExtension.create())
+  extensions.add(TocExtension.create())
+  
+  options.set(Parser.EXTENSIONS, extensions)
+
+  // set GFM table parsing options
+  options.set(TablesExtension.WITH_CAPTION, false)
+  options.set(TablesExtension.COLUMN_SPANS, false)
+  options.set(TablesExtension.MIN_HEADER_ROWS, 1)
+  options.set(TablesExtension.MAX_HEADER_ROWS, 1)
+  options.set(TablesExtension.APPEND_MISSING_COLUMNS, true)
+  options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)
+  options.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
+  // GFM anchor links
+  options.set(AnchorLinkExtension.ANCHORLINKS_SET_ID, false)
+  options.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor")
+  options.set(AnchorLinkExtension.ANCHORLINKS_SET_NAME, true)
+  options.set(AnchorLinkExtension.ANCHORLINKS_TEXT_PREFIX, "<span class=\"octicon octicon-link\"></span>")
+
+  Parser parser = Parser.builder(options).build()
+  HtmlRenderer renderer = HtmlRenderer.builder(options).build()
+
+  mdFiles.each { mdFile ->
+    // add table of contents
+    def mdText = "[TOC]\n"+mdFile.text
+
+    // grab the first top-level title
+    def title = null
+    def titleRegex = /(?m)^#(\s+|([^#]))(.*)/
+    def matcher = mdText =~ titleRegex
+    if (matcher.size() > 0) {
+      // matcher[0][2] is the first character of the title if there wasn't any whitespace after the #
+      title = (matcher[0][2] != null ? matcher[0][2] : "")+matcher[0][3]
+    }
+    // or use the filename if none found
+    if (title == null) {
+      title = mdFile.getName()
+    }
+
+    Node document = parser.parse(mdText)
+    String htmlBody = renderer.render(document)
+    def htmlText = '''<html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+    <meta name="generator" content="flexmark" />
+'''
+    htmlText += ((title != null) ? "  <title>${title}</title>" : '' )
+    htmlText += '''
+    <style type="text/css">code{white-space: pre;}</style>
+'''
+    htmlText += ((cssFile != null) ? cssFile.text : '')
+    htmlText += '''</head>
+  <body>
+'''
+    htmlText += htmlBody
+    htmlText += '''
+  </body>
+</html>
+'''
+
+    def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
+    def htmlFile = file(htmlFilePath)
+    htmlFile.text = htmlText
+  }
+}
+
+
+task convertMdFiles {
   dependsOn cleanBuildingHTML
-  def buildingMD = "${jalviewDir}/${docDir}/building.md"
-  def css = "${jalviewDir}/${docDir}/github.css"
+  def mdFiles = fileTree(dir: "${jalviewDir}/${doc_dir}", include: "*.md")
+  def cssFile = file("${jalviewDir}/${flexmark_css}")
 
-  def pandoc = null
-  pandoc_exec.split(",").each {
-    if (file(it.trim()).exists()) {
-      pandoc = it.trim()
-      return true
-    }
+  doLast {
+    convertMdToHtml(mdFiles, cssFile)
   }
 
-  def buildtoolsPandoc = System.getProperty("user.home")+"/buildtools/pandoc/bin/pandoc"
-  if ((pandoc == null || ! file(pandoc).exists()) && file(buildtoolsPandoc).exists()) {
-    pandoc = System.getProperty("user.home")+"/buildtools/pandoc/bin/pandoc"
-  }
+  inputs.files(mdFiles)
+  inputs.file(cssFile)
 
-  doFirst {
-    if (pandoc != null && file(pandoc).exists()) {
-        commandLine pandoc, '-s', '-o', buildingHTML, '--metadata', 'pagetitle="Building Jalview from Source"', '--toc', '-H', css, buildingMD
-    } else {
-        println("Cannot find pandoc. Skipping convert building.md to HTML")
-        throw new StopExecutionException("Cannot find pandoc. Skipping convert building.md to HTML")
-    }
+  def htmlFiles = []
+  mdFiles.each { mdFile ->
+    def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
+    htmlFiles.add(file(htmlFilePath))
   }
-
-  ignoreExitValue true
-
-  inputs.file(buildingMD)
-  inputs.file(css)
-  outputs.file(buildingHTML)
+  outputs.files(htmlFiles)
 }
 
 
 task syncDocs(type: Sync) {
-  dependsOn convertBuildingMD
-  def syncDir = "${classesDir}/${docDir}"
-  from fileTree("${jalviewDir}/${docDir}")
+  dependsOn convertMdFiles
+  def syncDir = "${classesDir}/${doc_dir}"
+  from fileTree("${jalviewDir}/${doc_dir}")
   into syncDir
-
 }
 
 
 task copyHelp(type: Copy) {
   def inputDir = helpSourceDir
-  def outputDir = "${classesDir}/${help_dir}"
+  def outputDir = "${resourceClassesDir}/${help_dir}"
   from(inputDir) {
     exclude '**/*.gif'
     exclude '**/*.jpg'
@@ -983,7 +1174,7 @@ task copyHelp(type: Copy) {
 
 
 task syncLib(type: Sync) {
-  def syncDir = "${classesDir}/${libDistDir}"
+  def syncDir = "${resourceClassesDir}/${libDistDir}"
   from fileTree("${jalviewDir}/${libDistDir}")
   into syncDir
 }
@@ -993,7 +1184,7 @@ task syncResources(type: Sync) {
   dependsOn createBuildProperties
   from resourceDir
   include "**/*.*"
-  into "${classesDir}"
+  into "${resourceClassesDir}"
   preserve {
     include "**"
   }
@@ -1010,18 +1201,17 @@ task prepare {
 //testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
 test {
   dependsOn prepare
-  dependsOn compileJava
-  if (use_clover) {
-    dependsOn cloverInstr
-  }
+  //dependsOn compileJava ////? DELETE
 
-  if (use_clover) {
-    print("Running tests " + (use_clover?"WITH":"WITHOUT") + " clover [clover="+use_clover+"]\n")
+  if (useClover) {
+    dependsOn cloverClasses
+   } else { //?
+     dependsOn compileJava //?
   }
 
   useTestNG() {
-    includeGroups testngGroups
-    excludeGroups testngExcludedGroups
+    includeGroups testng_groups
+    excludeGroups testng_excluded_groups
     preserveOrder true
     useDefaultListeners=true
   }
@@ -1039,6 +1229,11 @@ test {
   targetCompatibility = compile_target_compatibility
   jvmArgs += additional_compiler_args
 
+  doFirst {
+    if (useClover) {
+      println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
+    }
+  }
 }
 
 
@@ -1064,22 +1259,22 @@ task buildIndices(type: JavaExec) {
 
 task compileLinkCheck(type: JavaCompile) {
   options.fork = true
-  classpath = files("${jalviewDir}/${utilsDir}")
-  destinationDir = file("${jalviewDir}/${utilsDir}")
-  source = fileTree(dir: "${jalviewDir}/${utilsDir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])
+  classpath = files("${jalviewDir}/${utils_dir}")
+  destinationDir = file("${jalviewDir}/${utils_dir}")
+  source = fileTree(dir: "${jalviewDir}/${utils_dir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])
 
-  inputs.file("${jalviewDir}/${utilsDir}/HelpLinksChecker.java")
-  inputs.file("${jalviewDir}/${utilsDir}/HelpLinksChecker.java")
-  outputs.file("${jalviewDir}/${utilsDir}/HelpLinksChecker.class")
-  outputs.file("${jalviewDir}/${utilsDir}/BufferedLineReader.class")
+  inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
+  inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
+  outputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.class")
+  outputs.file("${jalviewDir}/${utils_dir}/BufferedLineReader.class")
 }
 
 
 task linkCheck(type: JavaExec) {
   dependsOn prepare, compileLinkCheck
 
-  def helpLinksCheckerOutFile = file("${jalviewDir}/${utilsDir}/HelpLinksChecker.out")
-  classpath = files("${jalviewDir}/${utilsDir}")
+  def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
+  classpath = files("${jalviewDir}/${utils_dir}")
   main = "HelpLinksChecker"
   workingDir = jalviewDir
   args = [ "${classesDir}/${help_dir}", "-nointernet" ]
@@ -1100,12 +1295,12 @@ task linkCheck(type: JavaExec) {
 // import the pubhtmlhelp target
 ant.properties.basedir = "${jalviewDir}"
 ant.properties.helpBuildDir = "${jalviewDirAbsolutePath}/${classes_dir}/${help_dir}"
-ant.importBuild "${utilsDir}/publishHelp.xml"
+ant.importBuild "${utils_dir}/publishHelp.xml"
 
 
 task cleanPackageDir(type: Delete) {
   doFirst {
-    delete fileTree(dir: "${jalviewDir}/${packageDir}", include: "*.jar")
+    delete fileTree(dir: "${jalviewDir}/${package_dir}", include: "*.jar")
   }
 }
 
@@ -1116,13 +1311,13 @@ jar {
   dependsOn createBuildProperties
 
   manifest {
-    attributes "Main-Class": mainClass,
+    attributes "Main-Class": main_class,
     "Permissions": "all-permissions",
     "Application-Name": "Jalview Desktop",
     "Codebase": application_codebase
   }
 
-  destinationDir = file("${jalviewDir}/${packageDir}")
+  destinationDir = file("${jalviewDir}/${package_dir}")
   archiveName = rootProject.name+".jar"
 
   exclude "cache*/**"
@@ -1132,20 +1327,20 @@ jar {
   exclude "**/*.jar.*"
 
   inputs.dir(classesDir)
-  outputs.file("${jalviewDir}/${packageDir}/${archiveName}")
+  outputs.file("${jalviewDir}/${package_dir}/${archiveName}")
 }
 
 
 task copyJars(type: Copy) {
   from fileTree(dir: classesDir, include: "**/*.jar").files
-  into "${jalviewDir}/${packageDir}"
+  into "${jalviewDir}/${package_dir}"
 }
 
 
 // doing a Sync instead of Copy as Copy doesn't deal with "outputs" very well
 task syncJars(type: Sync) {
   from fileTree(dir: "${jalviewDir}/${libDistDir}", include: "**/*.jar").files
-  into "${jalviewDir}/${packageDir}"
+  into "${jalviewDir}/${package_dir}"
   preserve {
     include jar.archiveName
   }
@@ -1161,7 +1356,7 @@ task makeDist {
   dependsOn cleanPackageDir
   dependsOn syncJars
   dependsOn jar
-  outputs.dir("${jalviewDir}/${packageDir}")
+  outputs.dir("${jalviewDir}/${package_dir}")
 }
 
 
@@ -1173,6 +1368,7 @@ task cleanDist {
 
 shadowJar {
   group = "distribution"
+  description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar"
   if (buildDist) {
     dependsOn makeDist
   }
@@ -1182,7 +1378,7 @@ shadowJar {
   manifest {
     attributes 'Implementation-Version': JALVIEW_VERSION
   }
-  mainClassName = shadowJarMainClass
+  mainClassName = shadow_jar_main_class
   mergeServiceFiles()
   classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
   minimize()
@@ -1274,7 +1470,7 @@ task getdownWebsite() {
     }
 
     def codeFiles = []
-    fileTree(file(packageDir)).each{ f ->
+    fileTree(file(package_dir)).each{ f ->
       if (f.isDirectory()) {
         def files = fileTree(dir: f, include: ["*"]).getFiles()
         codeFiles += files
@@ -1311,7 +1507,7 @@ task getdownWebsite() {
     // 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 = ${mainClass}\n"
+    getdownTextString += "class = ${main_class}\n"
 
     def getdown_txt = file("${getdownWebsiteDir}/getdown.txt")
     getdown_txt.write(getdownTextString)
@@ -1369,7 +1565,7 @@ task getdownWebsite() {
   }
 
   if (buildDist) {
-    inputs.dir("${jalviewDir}/${packageDir}")
+    inputs.dir("${jalviewDir}/${package_dir}")
   }
   outputs.dir(getdownWebsiteDir)
   outputs.dir(getdownFilesDir)
@@ -1378,8 +1574,10 @@ task getdownWebsite() {
 
 // a helper task to allow getdown digest of any dir: `gradle getdownDigestDir -PDIGESTDIR=/path/to/my/random/getdown/dir
 task getdownDigestDir(type: JavaExec) {
+  group "Help"
+  description "A task to run a getdown Digest on a dir with getdown.txt. Provide a DIGESTDIR property via -PDIGESTDIR=..."
+
   def digestDirPropertyName = "DIGESTDIR"
-  description = "Digest a local dir (-P${digestDirPropertyName}=...) for getdown"
   doFirst {
     classpath = files(getdownLauncher)
     def digestDir = findProperty(digestDirPropertyName)
@@ -1504,7 +1702,8 @@ task copyInstall4jTemplate {
     // NB we're deleting the /other/ one!
     // Also remove the examples subdir from non-release versions
     def customizedIdToDelete = "PROGRAM_GROUP_RELEASE"
-    if (CHANNEL=="RELEASE") {
+    // 2.11.1.0 NOT releasing with the Examples folder in the Program Group
+    if (false && CHANNEL=="RELEASE") { // remove 'false && ' to include Examples folder in RELEASE channel
       customizedIdToDelete = "PROGRAM_GROUP_NON_RELEASE"
     } else {
       // remove the examples subdir from Full File Set
@@ -1545,7 +1744,6 @@ clean {
 task installers(type: com.install4j.gradle.Install4jTask) {
   group = "distribution"
   description = "Create the install4j installers"
-  dependsOn setGitVals
   dependsOn getdown
   dependsOn copyInstall4jTemplate
 
@@ -1644,7 +1842,11 @@ spotless {
 
 
 task sourceDist(type: Tar) {
+  group "distribution"
+  description "Create a source .tar.gz file for distribution"
   
+  dependsOn convertMdFiles
+
   def VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
   def outputFileName = "${project.name}_${VERSION_UNDERSCORES}.tar.gz"
   // cater for buildship < 3.1 [3.0.1 is max version in eclipse 2018-09]
@@ -2419,7 +2621,7 @@ def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inpu
       beginToken: '_',
       endToken: '_',
       tokens: [
-        'MAIN': '"'+mainClass+'"',
+        'MAIN': '"'+main_class+'"',
         'CODE': "null",
         'NAME': jalviewjsJalviewTemplateName+" [core ${coreName}]",
         'COREKEY': jalviewjs_core_key,
@@ -2720,7 +2922,7 @@ task jalviewjsIDE_PrepareSite {
   description "Sync libs and resources to site dir, but not closure cores"
 
   dependsOn jalviewjsIDE_SyncSiteAll
-  dependsOn cleanJalviewjsTransferSite
+  //dependsOn cleanJalviewjsTransferSite // not sure why this clean is here -- will slow down a re-run of this task
 }
 
 
index cc5eb30..2ca2641 100644 (file)
+<html>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-  <meta http-equiv="Content-Style-Type" content="text/css" />
-  <meta name="generator" content="pandoc" />
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+    <meta name="generator" content="flexmark" />
   <title>Building Jalview from Source</title>
-  <style type="text/css">code{white-space: pre;}</style>
-  <style type="text/css">
-div.sourceCode { overflow-x: auto; }
-table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
-  margin: 0; padding: 0; vertical-align: baseline; border: none; }
-table.sourceCode { width: 100%; line-height: 100%; }
-td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
-td.sourceCode { padding-left: 5px; }
-code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
-code > span.dt { color: #902000; } /* DataType */
-code > span.dv { color: #40a070; } /* DecVal */
-code > span.bn { color: #40a070; } /* BaseN */
-code > span.fl { color: #40a070; } /* Float */
-code > span.ch { color: #4070a0; } /* Char */
-code > span.st { color: #4070a0; } /* String */
-code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
-code > span.ot { color: #007020; } /* Other */
-code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
-code > span.fu { color: #06287e; } /* Function */
-code > span.er { color: #ff0000; font-weight: bold; } /* Error */
-code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
-code > span.cn { color: #880000; } /* Constant */
-code > span.sc { color: #4070a0; } /* SpecialChar */
-code > span.vs { color: #4070a0; } /* VerbatimString */
-code > span.ss { color: #bb6688; } /* SpecialString */
-code > span.im { } /* Import */
-code > span.va { color: #19177c; } /* Variable */
-code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
-code > span.op { color: #666666; } /* Operator */
-code > span.bu { } /* BuiltIn */
-code > span.ex { } /* Extension */
-code > span.pp { color: #bc7a00; } /* Preprocessor */
-code > span.at { color: #7d9029; } /* Attribute */
-code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
-code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
-code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
-code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
-  </style>
-  <style>
-  @font-face {
-    font-family: octicons-link;
-    src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
-  }
-  
-  body {
-    -webkit-text-size-adjust: 100%;
-    text-size-adjust: 100%;
-    color: #333;
-    font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-    font-size: 16px;
-    line-height: 1.6;
-    word-wrap: break-word;
-    width: 728px;
-    max-width: 99%;
-    box-sizing: border-box;
-    padding: 30px 30px 8rem 30px;
-    margin-left: auto;
-    margin-right: auto;
-  }
-  
-  body a {
-    background-color: transparent;
-  }
-  
-  body a:active,
-  body a:hover {
-    outline: 0;
-  }
-  
-  body strong {
-    font-weight: bold;
-  }
-  
-  body h1 {
-    font-size: 2em;
-    margin: 0.67em 0;
-  }
-  
-  body img {
-    border: 0;
-  }
-  
-  body hr {
-    box-sizing: content-box;
-    height: 0;
-  }
-  
-  body pre {
-    overflow: auto;
-  }
-  
-  body code,
-  body kbd,
-  body pre {
-    font-family: monospace, monospace;
-    font-size: 1em;
-  }
-  
-  body input {
-    color: inherit;
-    font: inherit;
-    margin: 0;
-  }
-  
-  body html input[disabled] {
-    cursor: default;
-  }
-  
-  body input {
-    line-height: normal;
-  }
-  
-  body input[type="checkbox"] {
-    box-sizing: border-box;
-    padding: 0;
-  }
-  
-  body table {
-    border-collapse: collapse;
-    border-spacing: 0;
-  }
-  
-  body td,
-  body th {
-    padding: 0;
-  }
-  
-  body * {
-    box-sizing: border-box;
-  }
-  
-  body input {
-    font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-  }
-  
-  body a {
-    color: #4078c0;
-    text-decoration: none;
-  }
-  
-  body a:hover,
-  body a:active {
-    text-decoration: underline;
-  }
-  
-  body hr {
-    height: 0;
-    margin: 15px 0;
-    overflow: hidden;
-    background: transparent;
-    border: 0;
-    border-bottom: 1px solid #ddd;
-  }
-  
-  body hr:before {
-    display: table;
-    content: "";
-  }
-  
-  body hr:after {
-    display: table;
-    clear: both;
-    content: "";
-  }
-  
-  body h1,
-  body h2,
-  body h3,
-  body h4,
-  body h5,
-  body h6 {
-    margin-top: 15px;
-    margin-bottom: 15px;
-    line-height: 1.1;
-  }
-  
-  body h1 {
-    font-size: 30px;
-  }
-  
-  body h2 {
-    font-size: 21px;
-  }
-  
-  body h3 {
-    font-size: 16px;
-  }
-  
-  body h4 {
-    font-size: 14px;
-  }
-  
-  body h5 {
-    font-size: 12px;
-  }
-  
-  body h6 {
-    font-size: 11px;
-  }
-  
-  body blockquote {
-    margin: 0;
-  }
-  
-  body ul,
-  body ol {
-    padding: 0;
-    margin-top: 0;
-    margin-bottom: 0;
-  }
-  
-  body ol ol,
-  body ul ol {
-    list-style-type: lower-roman;
-  }
-  
-  body ul ul ol,
-  body ul ol ol,
-  body ol ul ol,
-  body ol ol ol {
-    list-style-type: lower-alpha;
-  }
-  
-  body dd {
-    margin-left: 0;
-  }
-  
-  body code {
-    font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
-    font-size: 12px;
-  }
-  
-  body pre {
-    margin-top: 0;
-    margin-bottom: 0;
-    font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
-  }
-  
-  body .select::-ms-expand {
-    opacity: 0;
-  }
-  
-  body .octicon {
-    font: normal normal normal 16px/1 octicons-link;
-    display: inline-block;
-    text-decoration: none;
-    text-rendering: auto;
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
-    user-select: none;
-  }
-  
-  body .octicon-link:before {
-    content: '\f05c';
-  }
-  
-  body:before {
-    display: table;
-    content: "";
-  }
-  
-  body:after {
-    display: table;
-    clear: both;
-    content: "";
-  }
-  
-  body>*:first-child {
-    margin-top: 0 !important;
-  }
-  
-  body>*:last-child {
-    margin-bottom: 0 !important;
-  }
-  
-  body a:not([href]) {
-    color: inherit;
-    text-decoration: none;
-  }
-  
-  body .anchor {
-    display: inline-block;
-    padding-right: 2px;
-    margin-left: -18px;
-  }
-  
-  body .anchor:focus {
-    outline: none;
-  }
-  
-  body h1,
-  body h2,
-  body h3,
-  body h4,
-  body h5,
-  body h6 {
-    margin-top: 1em;
-    margin-bottom: 16px;
-    font-weight: bold;
-    line-height: 1.4;
-  }
-  
-  body h1 .octicon-link,
-  body h2 .octicon-link,
-  body h3 .octicon-link,
-  body h4 .octicon-link,
-  body h5 .octicon-link,
-  body h6 .octicon-link {
-    color: #000;
-    vertical-align: middle;
-    visibility: hidden;
-  }
-  
-  body h1:hover .anchor,
-  body h2:hover .anchor,
-  body h3:hover .anchor,
-  body h4:hover .anchor,
-  body h5:hover .anchor,
-  body h6:hover .anchor {
-    text-decoration: none;
-  }
-  
-  body h1:hover .anchor .octicon-link,
-  body h2:hover .anchor .octicon-link,
-  body h3:hover .anchor .octicon-link,
-  body h4:hover .anchor .octicon-link,
-  body h5:hover .anchor .octicon-link,
-  body h6:hover .anchor .octicon-link {
-    visibility: visible;
-  }
-  
-  body h1 {
-    padding-bottom: 0.3em;
-    font-size: 1.75em;
-    line-height: 1.2;
-  }
-  
-  body h1 .anchor {
-    line-height: 1;
-  }
-  
-  body h2 {
-    padding-bottom: 0.3em;
-    font-size: 1.5em;
-    line-height: 1.225;
-  }
-  
-  body h2 .anchor {
-    line-height: 1;
-  }
-  
-  body h3 {
-    font-size: 1.25em;
-    line-height: 1.43;
-  }
-  
-  body h3 .anchor {
-    line-height: 1.2;
-  }
-  
-  body h4 {
-    font-size: 1em;
-  }
-  
-  body h4 .anchor {
-    line-height: 1.2;
-  }
-  
-  body h5 {
-    font-size: 1em;
-  }
-  
-  body h5 .anchor {
-    line-height: 1.1;
-  }
-  
-  body h6 {
-    font-size: 1em;
-    color: #777;
-  }
-  
-  body h6 .anchor {
-    line-height: 1.1;
-  }
-  
-  body p,
-  body blockquote,
-  body ul,
-  body ol,
-  body dl,
-  body table,
-  body pre {
-    margin-top: 0;
-    margin-bottom: 16px;
-  }
-  
-  body hr {
-    height: 4px;
-    padding: 0;
-    margin: 16px 0;
-    background-color: #e7e7e7;
-    border: 0 none;
-  }
-  
-  body ul,
-  body ol {
-    padding-left: 2em;
-  }
-  
-  body ul ul,
-  body ul ol,
-  body ol ol,
-  body ol ul {
-    margin-top: 0;
-    margin-bottom: 0;
-  }
-  
-  body li>p {
-    margin-top: 16px;
-  }
-  
-  body dl {
-    padding: 0;
-  }
-  
-  body dl dt {
-    padding: 0;
-    margin-top: 16px;
-    font-size: 1em;
-    font-style: italic;
-    font-weight: bold;
-  }
-  
-  body dl dd {
-    padding: 0 16px;
-    margin-bottom: 16px;
-  }
-  
-  body blockquote {
-    padding: 0 15px;
-    color: #777;
-    border-left: 4px solid #ddd;
-  }
-  
-  body blockquote>:first-child {
-    margin-top: 0;
-  }
-  
-  body blockquote>:last-child {
-    margin-bottom: 0;
-  }
-  
-  body table {
-    display: block;
-    width: 100%;
-    overflow: auto;
-    word-break: normal;
-    word-break: keep-all;
-  }
-  
-  body table th {
-    font-weight: bold;
-  }
-  
-  body table th,
-  body table td {
-    padding: 6px 13px;
-    border: 1px solid #ddd;
-  }
-  
-  body table tr {
-    background-color: #fff;
-    border-top: 1px solid #ccc;
-  }
-  
-  body table tr:nth-child(2n) {
-    background-color: #f8f8f8;
-  }
-  
-  body img {
-    max-width: 100%;
-    box-sizing: content-box;
-    background-color: #fff;
-  }
-  
-  body code {
-    padding: 0;
-    padding-top: 0;
-    padding-bottom: 0;
-    margin: 0;
-    font-size: 85%;
-    background-color: rgba(0,0,0,0.04);
-    border-radius: 3px;
-  }
-  
-  body code:before,
-  body code:after {
-    letter-spacing: -0.2em;
-    content: "\00a0";
-  }
-  
-  body pre>code {
-    padding: 0;
-    margin: 0;
-    font-size: 100%;
-    word-break: normal;
-    white-space: pre;
-    background: transparent;
-    border: 0;
-  }
-  
-  body .highlight {
-    margin-bottom: 16px;
-  }
-  
-  body .highlight pre,
-  body pre {
-    padding: 16px;
-    overflow: auto;
-    font-size: 85%;
-    line-height: 1.45;
-    background-color: #f7f7f7;
-    border-radius: 3px;
-  }
-  
-  body .highlight pre {
-    margin-bottom: 0;
-    word-break: normal;
-  }
-  
-  body pre {
-    word-wrap: normal;
-  }
-  
-  body pre code {
-    display: inline;
-    max-width: initial;
-    padding: 0;
-    margin: 0;
-    overflow: initial;
-    line-height: inherit;
-    word-wrap: normal;
-    background-color: transparent;
-    border: 0;
-  }
-  
-  body pre code:before,
-  body pre code:after {
-    content: normal;
-  }
-  
-  body kbd {
-    display: inline-block;
-    padding: 3px 5px;
-    font-size: 11px;
-    line-height: 10px;
-    color: #555;
-    vertical-align: middle;
-    background-color: #fcfcfc;
-    border: solid 1px #ccc;
-    border-bottom-color: #bbb;
-    border-radius: 3px;
-    box-shadow: inset 0 -1px 0 #bbb;
-  }
-  
-  body .pl-c {
-    color: #969896;
-  }
-  
-  body .pl-c1,
-  body .pl-s .pl-v {
-    color: #0086b3;
-  }
-  
-  body .pl-e,
-  body .pl-en {
-    color: #795da3;
-  }
-  
-  body .pl-s .pl-s1,
-  body .pl-smi {
-    color: #333;
-  }
-  
-  body .pl-ent {
-    color: #63a35c;
-  }
-  
-  body .pl-k {
-    color: #a71d5d;
-  }
-  
-  body .pl-pds,
-  body .pl-s,
-  body .pl-s .pl-pse .pl-s1,
-  body .pl-sr,
-  body .pl-sr .pl-cce,
-  body .pl-sr .pl-sra,
-  body .pl-sr .pl-sre {
-    color: #183691;
-  }
-  
-  body .pl-v {
-    color: #ed6a43;
-  }
-  
-  body .pl-id {
-    color: #b52a1d;
-  }
-  
-  body .pl-ii {
-    background-color: #b52a1d;
-    color: #f8f8f8;
-  }
-  
-  body .pl-sr .pl-cce {
-    color: #63a35c;
-    font-weight: bold;
-  }
-  
-  body .pl-ml {
-    color: #693a17;
-  }
-  
-  body .pl-mh,
-  body .pl-mh .pl-en,
-  body .pl-ms {
-    color: #1d3e81;
-    font-weight: bold;
-  }
-  
-  body .pl-mq {
-    color: #008080;
-  }
-  
-  body .pl-mi {
-    color: #333;
-    font-style: italic;
-  }
-  
-  body .pl-mb {
-    color: #333;
-    font-weight: bold;
-  }
-  
-  body .pl-md {
-    background-color: #ffecec;
-    color: #bd2c00;
-  }
-  
-  body .pl-mi1 {
-    background-color: #eaffea;
-    color: #55a532;
-  }
-  
-  body .pl-mdr {
-    color: #795da3;
-    font-weight: bold;
-  }
-  
-  body .pl-mo {
-    color: #1d3e81;
-  }
-  
-  body kbd {
-    display: inline-block;
-    padding: 3px 5px;
-    font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
-    line-height: 10px;
-    color: #555;
-    vertical-align: middle;
-    background-color: #fcfcfc;
-    border: solid 1px #ccc;
-    border-bottom-color: #bbb;
-    border-radius: 3px;
-    box-shadow: inset 0 -1px 0 #bbb;
-  }
-  
-  body .task-list-item {
-    list-style-type: none;
-  }
-  
-  body .task-list-item+.task-list-item {
-    margin-top: 3px;
-  }
-  
-  body .task-list-item input {
-    margin: 0 0.35em 0.25em -1.6em;
-    vertical-align: middle;
-  }
-  
-  body :checked+.radio-label {
-    z-index: 1;
-    position: relative;
-    border-color: #4078c0;
-  }
-  </style>
+    <style type="text/css">code{white-space: pre;}</style>
+<style>
+@font-face {
+  font-family: octicons-link;
+  src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
+}
+
+body {
+  -webkit-text-size-adjust: 100%;
+  text-size-adjust: 100%;
+  color: #333;
+  font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  font-size: 16px;
+  line-height: 1.6;
+  word-wrap: break-word;
+  width: 728px;
+  max-width: 99%;
+  box-sizing: border-box;
+  padding: 30px 30px 8rem 30px;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+body a {
+  background-color: transparent;
+}
+
+body a:active,
+body a:hover {
+  outline: 0;
+}
+
+body strong {
+  font-weight: bold;
+}
+
+body h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+body img {
+  border: 0;
+}
+
+body hr {
+  box-sizing: content-box;
+  height: 0;
+}
+
+body pre {
+  overflow: auto;
+}
+
+body code,
+body kbd,
+body pre {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+
+body input {
+  color: inherit;
+  font: inherit;
+  margin: 0;
+}
+
+body html input[disabled] {
+  cursor: default;
+}
+
+body input {
+  line-height: normal;
+}
+
+body input[type="checkbox"] {
+  box-sizing: border-box;
+  padding: 0;
+}
+
+body table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+body td,
+body th {
+  padding: 0;
+}
+
+body * {
+  box-sizing: border-box;
+}
+
+body input {
+  font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+body a {
+  color: #4078c0;
+  text-decoration: none;
+}
+
+body a:hover,
+body a:active {
+  text-decoration: underline;
+}
+
+body hr {
+  height: 0;
+  margin: 15px 0;
+  overflow: hidden;
+  background: transparent;
+  border: 0;
+  border-bottom: 1px solid #ddd;
+}
+
+body hr:before {
+  display: table;
+  content: "";
+}
+
+body hr:after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+body h1,
+body h2,
+body h3,
+body h4,
+body h5,
+body h6 {
+  margin-top: 15px;
+  margin-bottom: 15px;
+  line-height: 1.1;
+}
+
+body h1 {
+  font-size: 30px;
+}
+
+body h2 {
+  font-size: 21px;
+}
+
+body h3 {
+  font-size: 16px;
+}
+
+body h4 {
+  font-size: 14px;
+}
+
+body h5 {
+  font-size: 12px;
+}
+
+body h6 {
+  font-size: 11px;
+}
+
+body blockquote {
+  margin: 0;
+}
+
+body ul,
+body ol {
+  padding: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+body ol ol,
+body ul ol {
+  list-style-type: lower-roman;
+}
+
+body ul ul ol,
+body ul ol ol,
+body ol ul ol,
+body ol ol ol {
+  list-style-type: lower-alpha;
+}
+
+body dd {
+  margin-left: 0;
+}
+
+body code {
+  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font-size: 12px;
+}
+
+body pre {
+  margin-top: 0;
+  margin-bottom: 0;
+  font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+}
+
+body .select::-ms-expand {
+  opacity: 0;
+}
+
+body .octicon {
+  font: normal normal normal 16px/1 octicons-link;
+  display: inline-block;
+  text-decoration: none;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+body .octicon-link:before {
+  content: '\f05c';
+}
+
+body:before {
+  display: table;
+  content: "";
+}
+
+body:after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+body>*:first-child {
+  margin-top: 0 !important;
+}
+
+body>*:last-child {
+  margin-bottom: 0 !important;
+}
+
+body a:not([href]) {
+  color: inherit;
+  text-decoration: none;
+}
+
+body .anchor {
+  display: inline-block;
+  padding-right: 2px;
+  margin-left: -18px;
+}
+
+body .anchor:focus {
+  outline: none;
+}
+
+body h1,
+body h2,
+body h3,
+body h4,
+body h5,
+body h6 {
+  margin-top: 1em;
+  margin-bottom: 16px;
+  font-weight: bold;
+  line-height: 1.4;
+}
+
+body h1 .octicon-link,
+body h2 .octicon-link,
+body h3 .octicon-link,
+body h4 .octicon-link,
+body h5 .octicon-link,
+body h6 .octicon-link {
+  color: #000;
+  vertical-align: middle;
+  visibility: hidden;
+}
+
+body h1:hover .anchor,
+body h2:hover .anchor,
+body h3:hover .anchor,
+body h4:hover .anchor,
+body h5:hover .anchor,
+body h6:hover .anchor {
+  text-decoration: none;
+}
+
+body h1:hover .anchor .octicon-link,
+body h2:hover .anchor .octicon-link,
+body h3:hover .anchor .octicon-link,
+body h4:hover .anchor .octicon-link,
+body h5:hover .anchor .octicon-link,
+body h6:hover .anchor .octicon-link {
+  visibility: visible;
+}
+
+body h1 {
+  padding-bottom: 0.3em;
+  font-size: 1.75em;
+  line-height: 1.2;
+}
+
+body h1 .anchor {
+  line-height: 1;
+}
+
+body h2 {
+  padding-bottom: 0.3em;
+  font-size: 1.5em;
+  line-height: 1.225;
+}
+
+body h2 .anchor {
+  line-height: 1;
+}
+
+body h3 {
+  font-size: 1.25em;
+  line-height: 1.43;
+}
+
+body h3 .anchor {
+  line-height: 1.2;
+}
+
+body h4 {
+  font-size: 1em;
+}
+
+body h4 .anchor {
+  line-height: 1.2;
+}
+
+body h5 {
+  font-size: 1em;
+}
+
+body h5 .anchor {
+  line-height: 1.1;
+}
+
+body h6 {
+  font-size: 1em;
+  color: #777;
+}
+
+body h6 .anchor {
+  line-height: 1.1;
+}
+
+body p,
+body blockquote,
+body ul,
+body ol,
+body dl,
+body table,
+body pre {
+  margin-top: 0;
+  margin-bottom: 16px;
+}
+
+body hr {
+  height: 4px;
+  padding: 0;
+  margin: 16px 0;
+  background-color: #e7e7e7;
+  border: 0 none;
+}
+
+body ul,
+body ol {
+  padding-left: 2em;
+}
+
+body ul ul,
+body ul ol,
+body ol ol,
+body ol ul {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+body li>p {
+  margin-top: 16px;
+}
+
+body dl {
+  padding: 0;
+}
+
+body dl dt {
+  padding: 0;
+  margin-top: 16px;
+  font-size: 1em;
+  font-style: italic;
+  font-weight: bold;
+}
+
+body dl dd {
+  padding: 0 16px;
+  margin-bottom: 16px;
+}
+
+body blockquote {
+  padding: 0 15px;
+  color: #777;
+  border-left: 4px solid #ddd;
+}
+
+body blockquote>:first-child {
+  margin-top: 0;
+}
+
+body blockquote>:last-child {
+  margin-bottom: 0;
+}
+
+body table {
+  display: block;
+  width: 100%;
+  overflow: auto;
+  word-break: normal;
+  word-break: keep-all;
+}
+
+body table th {
+  font-weight: bold;
+}
+
+body table th,
+body table td {
+  padding: 6px 13px;
+  border: 1px solid #ddd;
+}
+
+body table tr {
+  background-color: #fff;
+  border-top: 1px solid #ccc;
+}
+
+body table tr:nth-child(2n) {
+  background-color: #f8f8f8;
+}
+
+body img {
+  max-width: 100%;
+  box-sizing: content-box;
+  background-color: #fff;
+}
+
+body code {
+  padding: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  margin: 0;
+  font-size: 85%;
+  background-color: rgba(0,0,0,0.04);
+  border-radius: 3px;
+}
+
+body code:before,
+body code:after {
+  letter-spacing: -0.2em;
+  content: "\00a0";
+}
+
+body pre>code {
+  padding: 0;
+  margin: 0;
+  font-size: 100%;
+  word-break: normal;
+  white-space: pre;
+  background: transparent;
+  border: 0;
+}
+
+body .highlight {
+  margin-bottom: 16px;
+}
+
+body .highlight pre,
+body pre {
+  padding: 16px;
+  overflow: auto;
+  font-size: 85%;
+  line-height: 1.45;
+  background-color: #f7f7f7;
+  border-radius: 3px;
+}
+
+body .highlight pre {
+  margin-bottom: 0;
+  word-break: normal;
+}
+
+body pre {
+  word-wrap: normal;
+}
+
+body pre code {
+  display: inline;
+  max-width: initial;
+  padding: 0;
+  margin: 0;
+  overflow: initial;
+  line-height: inherit;
+  word-wrap: normal;
+  background-color: transparent;
+  border: 0;
+}
+
+body pre code:before,
+body pre code:after {
+  content: normal;
+}
+
+body kbd {
+  display: inline-block;
+  padding: 3px 5px;
+  font-size: 11px;
+  line-height: 10px;
+  color: #555;
+  vertical-align: middle;
+  background-color: #fcfcfc;
+  border: solid 1px #ccc;
+  border-bottom-color: #bbb;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 #bbb;
+}
+
+body .pl-c {
+  color: #969896;
+}
+
+body .pl-c1,
+body .pl-s .pl-v {
+  color: #0086b3;
+}
+
+body .pl-e,
+body .pl-en {
+  color: #795da3;
+}
+
+body .pl-s .pl-s1,
+body .pl-smi {
+  color: #333;
+}
+
+body .pl-ent {
+  color: #63a35c;
+}
+
+body .pl-k {
+  color: #a71d5d;
+}
+
+body .pl-pds,
+body .pl-s,
+body .pl-s .pl-pse .pl-s1,
+body .pl-sr,
+body .pl-sr .pl-cce,
+body .pl-sr .pl-sra,
+body .pl-sr .pl-sre {
+  color: #183691;
+}
+
+body .pl-v {
+  color: #ed6a43;
+}
+
+body .pl-id {
+  color: #b52a1d;
+}
+
+body .pl-ii {
+  background-color: #b52a1d;
+  color: #f8f8f8;
+}
+
+body .pl-sr .pl-cce {
+  color: #63a35c;
+  font-weight: bold;
+}
+
+body .pl-ml {
+  color: #693a17;
+}
+
+body .pl-mh,
+body .pl-mh .pl-en,
+body .pl-ms {
+  color: #1d3e81;
+  font-weight: bold;
+}
+
+body .pl-mq {
+  color: #008080;
+}
+
+body .pl-mi {
+  color: #333;
+  font-style: italic;
+}
+
+body .pl-mb {
+  color: #333;
+  font-weight: bold;
+}
+
+body .pl-md {
+  background-color: #ffecec;
+  color: #bd2c00;
+}
+
+body .pl-mi1 {
+  background-color: #eaffea;
+  color: #55a532;
+}
+
+body .pl-mdr {
+  color: #795da3;
+  font-weight: bold;
+}
+
+body .pl-mo {
+  color: #1d3e81;
+}
+
+body kbd {
+  display: inline-block;
+  padding: 3px 5px;
+  font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  line-height: 10px;
+  color: #555;
+  vertical-align: middle;
+  background-color: #fcfcfc;
+  border: solid 1px #ccc;
+  border-bottom-color: #bbb;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 #bbb;
+}
+
+body .task-list-item {
+  list-style-type: none;
+}
+
+body .task-list-item+.task-list-item {
+  margin-top: 3px;
+}
+
+body .task-list-item input {
+  margin: 0 0.35em 0.25em -1.6em;
+  vertical-align: middle;
+}
+
+body :checked+.radio-label {
+  z-index: 1;
+  position: relative;
+  border-color: #4078c0;
+}
+</style>
 </head>
-<body>
-<div id="TOC">
+  <body>
 <ul>
-<li><a href="#building-jalview-from-source">Building Jalview from Source</a><ul>
 <li><a href="#tldr">tl;dr</a></li>
-<li><a href="#setting-up">Setting up</a><ul>
+<li><a href="#setting-up">Setting up</a>
+<ul>
 <li><a href="#java-11-compliant-jdk">Java 11 compliant JDK</a></li>
 <li><a href="#gradle-and-git">gradle and git</a></li>
-</ul></li>
-<li><a href="#downloading-the-jalview-source-tree">Downloading the Jalview source tree</a><ul>
+</ul>
+</li>
+<li><a href="#downloading-the-jalview-source-tree">Downloading the Jalview source tree</a>
+<ul>
 <li><a href="#whats-in-the-source-tree">What's in the source tree?</a></li>
-</ul></li>
-<li><a href="#building-jalview">Building Jalview</a><ul>
+</ul>
+</li>
+<li><a href="#building-jalview">Building Jalview</a>
+<ul>
 <li><a href="#minimal-jalview-build">Minimal Jalview Build</a></li>
 <li><a href="#jalview-in-a-jar-file">Jalview in a Jar File</a></li>
 <li><a href="#distributed-jar-files">Distributed Jar Files</a></li>
@@ -726,18 +693,19 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
 <li><a href="#building-the-getdown-launcher">Building the <code>getdown</code> launcher</a></li>
 <li><a href="#running-tests">Running tests</a></li>
 <li><a href="#installer-packaging-with-install4j">Installer packaging with <em>install4j</em></a></li>
-</ul></li>
+</ul>
+</li>
 <li><a href="#gradle-properties">Gradle properties</a></li>
 <li><a href="#enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</a></li>
-<li><a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a><ul>
+<li><a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a>
+<ul>
 <li><a href="#installing-eclipse-ide">Installing Eclipse IDE</a></li>
 <li><a href="#importing-jalview-as-an-eclipse-project">Importing Jalview as an Eclipse project</a></li>
-</ul></li>
-</ul></li>
 </ul>
-</div>
-<h1 id="building-jalview-from-source">Building Jalview from Source</h1>
-<h2 id="tldr">tl;dr</h2>
+</li>
+</ul>
+<h1 id="building-jalview-from-source"><a href="#building-jalview-from-source" name="building-jalview-from-source" class="anchor"><span class="octicon octicon-link"></span>Building Jalview from Source</a></h1>
+<h2 id="tldr"><a href="#tldr" name="tldr" class="anchor"><span class="octicon octicon-link"></span>tl;dr</a></h2>
 <pre><code># download
 git clone http://source.jalview.org/git/jalview.git
 # compile
@@ -750,411 +718,504 @@ java -jar build/libs/jalview-all-11.jar
 gradle getdown
 # use launcher
 cd getdown/files
-java -jar getdown-launcher.jar . jalview</code></pre>
-<h2 id="setting-up">Setting up</h2>
+java -jar getdown-launcher.jar . jalview
+</code></pre>
+<h2 id="setting-up"><a href="#setting-up" name="setting-up" class="anchor"><span class="octicon octicon-link"></span>Setting up</a></h2>
 <blockquote>
-<p>To get set up using <em>only</em> the Eclipse IDE (<a href="https://www.eclipse.org/" class="uri">https://www.eclipse.org/</a>) then please see the section <a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a></p>
+<p>To get set up using <em>only</em> the Eclipse IDE (<a href="https://www.eclipse.org/">https://www.eclipse.org/</a>) then please see the section <a href="#setting-up-in-eclipse-ide">Setting up in Eclipse IDE</a></p>
 </blockquote>
-<p>The method here is described in terms of using a command line. You can easily do this on linux or in a Terminal window in macOS. You can do it in Windows.</p>
+<p>The method here is described in terms of using a command line.  You can easily do this on linux or in a Terminal window in macOS.  You can do it in Windows.</p>
 <ul>
 <li>Java 11 compliant JDK</li>
 <li>gradle 5.2 or above</li>
 <li>git</li>
 </ul>
 <blockquote>
-<p>The versions and installation methods here are just suggestions (which we have tested so are known to work). If you need or wish to use different implementations (particularly you might need a bespoke JDK if you are on an exotic architecture) then the general build instructions should work with any gradle 5+. You should be able to compile the bytecode with any JDK Java 11+. The resulting bytecode (in particular the shadow jar) should be runnable in any JRE Java 1.8+. Remember that because Jalview and the getdown launcher are Java bytecode you can build on one system where you might have gradle, and run on another where you don't (JRE 1.8+ required).</p>
+<p>The versions and installation methods here are just suggestions (which we have tested
+so are known to work).  If you need or wish to use different implementations (particularly
+you might need a bespoke JDK if you are on an exotic architecture) then the general
+build instructions should work with any gradle 5+.  You should be able to compile the
+bytecode with any JDK Java 11+.  The resulting bytecode (in particular the shadow jar)
+should be runnable in any JRE Java 1.8+.  Remember that because Jalview and the getdown launcher
+are Java bytecode you can build on one system where you might have gradle, and run
+on another where you don't (JRE 1.8+ required).</p>
 </blockquote>
-<h3 id="java-11-compliant-jdk">Java 11 compliant JDK</h3>
-<h4 id="all-platforms">All platforms</h4>
-<p>We recommend obtaining an OpenJDK JDK 11 (since 11 is the long term support release) from AdoptOpenJDK: <a href="https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot" class="uri">https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot</a>, either the <em>Installer</em> or <code>.zip</code>/<code>.tar.gz</code> variants whichever you prefer (if you're not sure, choose the <em>Installer</em>).</p>
+<h3 id="java-11-compliant-jdk"><a href="#java-11-compliant-jdk" name="java-11-compliant-jdk" class="anchor"><span class="octicon octicon-link"></span>Java 11 compliant JDK</a></h3>
+<h4 id="all-platforms"><a href="#all-platforms" name="all-platforms" class="anchor"><span class="octicon octicon-link"></span>All platforms</a></h4>
+<p>We recommend obtaining an OpenJDK JDK 11 (since 11 is the long term support release) from AdoptOpenJDK: <a href="https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot">https://adoptopenjdk.net/?variant=openjdk11&amp;jvmVariant=hotspot</a>, either the <em>Installer</em> or <code>.zip</code>/<code>.tar.gz</code> variants whichever you prefer (if you're not sure, choose the <em>Installer</em>).</p>
 <blockquote>
-<h5 id="alternativecli-install-of-adoptopenjdk-11">Alternative/CLI install of AdoptOpenJDK 11</h5>
-<p>You can also install adoptopenjdk11 using either <code>brew</code> (macOS), <code>choco</code> (Windows) (see the section on <code>gradle</code> and <code>git</code> for more informaiton on <code>brew</code> and <code>choco</code>) or <code>yum</code> or <code>apt</code> (Linux):</p>
-<h6 id="alternative-for-macos-and-homebrew">alternative for MacOS and Homebrew</h6>
+<h5 id="alternativecli-install-of-adoptopenjdk-11"><a href="#alternativecli-install-of-adoptopenjdk-11" name="alternativecli-install-of-adoptopenjdk-11" class="anchor"><span class="octicon octicon-link"></span>Alternative/CLI install of AdoptOpenJDK 11</a></h5>
+<p>You can also install adoptopenjdk11 using either <code>brew</code> (macOS), <code>choco</code> (Windows)
+(see the section on <code>gradle</code> and <code>git</code> for more informaiton on <code>brew</code> and <code>choco</code>)
+or <code>yum</code> or <code>apt</code> (Linux):</p>
+<h6 id="alternative-for-macos-and-homebrew"><a href="#alternative-for-macos-and-homebrew" name="alternative-for-macos-and-homebrew" class="anchor"><span class="octicon octicon-link"></span>alternative for MacOS and Homebrew</a></h6>
 <pre><code>brew tap adoptopenjdk/openjdk
-brew cask install adoptopenjdk11</code></pre>
-<h6 id="alternative-for-windows-and-chocolatey">alternative for Windows and Chocolatey</h6>
-<pre><code>choco install adoptopenjdk11</code></pre>
-<h6 id="alternative-for-linux-with-yumapt">alternative for Linux with yum/apt</h6>
-<p>see <a href="https://adoptopenjdk.net/installation.html#linux-pkg" class="uri">https://adoptopenjdk.net/installation.html#linux-pkg</a></p>
+brew cask install adoptopenjdk11
+</code></pre>
+<h6 id="alternative-for-windows-and-chocolatey"><a href="#alternative-for-windows-and-chocolatey" name="alternative-for-windows-and-chocolatey" class="anchor"><span class="octicon octicon-link"></span>alternative for Windows and Chocolatey</a></h6>
+<pre><code>choco install adoptopenjdk11
+</code></pre>
+<h6 id="alternative-for-linux-with-yumapt"><a href="#alternative-for-linux-with-yumapt" name="alternative-for-linux-with-yumapt" class="anchor"><span class="octicon octicon-link"></span>alternative for Linux with yum/apt</a></h6>
+<p>see <a href="https://adoptopenjdk.net/installation.html#linux-pkg">https://adoptopenjdk.net/installation.html#linux-pkg</a></p>
 </blockquote>
-<h3 id="gradle-and-git">gradle and git</h3>
+<h3 id="gradle-and-git"><a href="#gradle-and-git" name="gradle-and-git" class="anchor"><span class="octicon octicon-link"></span>gradle and git</a></h3>
 <p>You should be able to install the latest (or sufficiently recent) versions of gradle and git using your OS package manager.</p>
-<h4 id="macos">MacOS</h4>
-<p>we recommend using <code>brew</code>, which can be installed following the instructions at <a href="https://brew.sh/" class="uri">https://brew.sh/</a>. After installing <code>brew</code>, open a Terminal window and type in (using an Administrator privileged user):</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">brew</span> install gradle git</code></pre></div>
+<h4 id="macos"><a href="#macos" name="macos" class="anchor"><span class="octicon octicon-link"></span>MacOS</a></h4>
+<p>we recommend using <code>brew</code>, which can be installed following the instructions at <a href="https://brew.sh/">https://brew.sh/</a>.
+After installing <code>brew</code>, open a Terminal window and type in (using an Administrator privileged user):</p>
+<pre><code class="language-bash">brew install gradle git
+</code></pre>
 <p>or if you aready have them installed but need to upgrade the version:</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">brew</span> upgrade gradle git</code></pre></div>
-<h4 id="windows">Windows</h4>
-<p>we suggest using the <strong>Chocolatey</strong> package manager. See install instructions at <a href="https://chocolatey.org/" class="uri">https://chocolatey.org/</a>, and you will just need</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">choco</span> install gradle
-<span class="ex">choco</span> install git</code></pre></div>
-<p>Alternatively, you could install a real <code>bash</code> shell and install both <code>gradle</code> and <code>git</code> through <code>apt-get</code>. See <a href="https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/" class="uri">https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/</a> for how to install the ubuntu bash shell in Windows 10.</p>
-<p>Another alternative would be to install them separately. For <code>gradle</code> follow the instructions at <a href="https://gradle.org/install/" class="uri">https://gradle.org/install/</a>, and for <code>git</code> here are a couple of suggestions: Git for Windows <a href="https://gitforwindows.org/" class="uri">https://gitforwindows.org/</a>. Getting the individual installs working together on the command line will be trickier so we recommend using Chocolatey or bash.</p>
-<h4 id="linux">Linux</h4>
+<pre><code class="language-bash">brew upgrade gradle git
+</code></pre>
+<h4 id="windows"><a href="#windows" name="windows" class="anchor"><span class="octicon octicon-link"></span>Windows</a></h4>
+<p>we suggest using the <strong>Chocolatey</strong> package manager.  See install instructions at <a href="https://chocolatey.org/">https://chocolatey.org/</a>, and you will just need</p>
+<pre><code class="language-bash">choco install gradle
+choco install git
+</code></pre>
+<p>Alternatively, you could install a real <code>bash</code> shell and install both <code>gradle</code> and <code>git</code> through <code>apt-get</code>.
+See <a href="https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/">https://devblogs.microsoft.com/commandline/bash-on-ubuntu-on-windows-download-now-3/</a>
+for how to install the ubuntu bash shell in Windows 10.</p>
+<p>Another alternative would be to install them separately. For <code>gradle</code> follow the instructions at <a href="https://gradle.org/install/">https://gradle.org/install/</a>, and for <code>git</code> here are a couple of suggestions: Git for Windows <a href="https://gitforwindows.org/">https://gitforwindows.org/</a>.
+Getting the individual installs working together on the command line will be trickier
+so we recommend using Chocolatey or bash.</p>
+<h4 id="linux"><a href="#linux" name="linux" class="anchor"><span class="octicon octicon-link"></span>Linux</a></h4>
 <p>this will depend on which distribution you're using.</p>
-<h5 id="for-debian-based-distributions-e.g.-mint-ubuntu-debian">For <em>Debian</em> based distributions (e.g. Mint, Ubuntu, Debian)</h5>
+<h5 id="for-debian-based-distributions-eg-mint-ubuntu-debian"><a href="#for-debian-based-distributions-eg-mint-ubuntu-debian" name="for-debian-based-distributions-eg-mint-ubuntu-debian" class="anchor"><span class="octicon octicon-link"></span>For <em>Debian</em> based distributions (e.g. Mint, Ubuntu, Debian)</a></h5>
 <p>run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"> <span class="fu">sudo</span> apt-get install gradle git</code></pre></div>
-<h5 id="for-rpm-based-distributions-e.g.-fedora-centos-redhat">for RPM-based distributions (e.g. Fedora, CentOS, RedHat)</h5>
+<pre><code class="language-bash"> sudo apt-get install gradle git
+</code></pre>
+<h5 id="for-rpm-based-distributions-eg-fedora-centos-redhat"><a href="#for-rpm-based-distributions-eg-fedora-centos-redhat" name="for-rpm-based-distributions-eg-fedora-centos-redhat" class="anchor"><span class="octicon octicon-link"></span>for RPM-based distributions (e.g. Fedora, CentOS, RedHat)</a></h5>
 <p>run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">sudo</span> yum install gradle git</code></pre></div>
+<pre><code class="language-bash">sudo yum install gradle git
+</code></pre>
 <p>If you have some other version of linux you'll probably be able to work it out!</p>
-<h2 id="downloading-the-jalview-source-tree">Downloading the Jalview source tree</h2>
-<p>This can be done with <code>git</code>. On the command line, change directory to where you want to download Jalview's build-tree top level directory. Then run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> clone http://source.jalview.org/git/jalview.git</code></pre></div>
-<p>You'll get some progress output and after a minute or two you should have the full Jalview build-tree in the folder <code>jalview</code>.</p>
-<h3 id="whats-in-the-source-tree">What's in the source tree?</h3>
-<p>Jalview is a mature product with its codebase going back many years. As such it doesn't have a folder structure that most new gradle projects would have, so you might not find everything in the place you might expect. Here's a brief description of what you might find in the main folders under the <code>jalview</code> tree.</p>
+<h2 id="downloading-the-jalview-source-tree"><a href="#downloading-the-jalview-source-tree" name="downloading-the-jalview-source-tree" class="anchor"><span class="octicon octicon-link"></span>Downloading the Jalview source tree</a></h2>
+<p>This can be done with <code>git</code>.
+On the command line, change directory to where you want to download Jalview's build-tree
+top level directory.  Then run</p>
+<pre><code class="language-bash">git clone http://source.jalview.org/git/jalview.git
+</code></pre>
+<p>You'll get some progress output and after a minute or two you should have the full
+Jalview build-tree in the folder <code>jalview</code>.</p>
+<h3 id="whats-in-the-source-tree"><a href="#whats-in-the-source-tree" name="whats-in-the-source-tree" class="anchor"><span class="octicon octicon-link"></span>What's in the source tree?</a></h3>
+<p>Jalview is a mature product with its codebase going back many years.  As such it doesn't
+have a folder structure that most new gradle projects would have, so you might not
+find everything in the place you might expect.  Here's a brief description of what
+you might find in the main folders under the <code>jalview</code> tree.</p>
 <p>Within the <code>jalview</code> folder you will find (of possible interest):</p>
 <table>
-<colgroup>
-<col width="16%" />
-<col width="83%" />
-</colgroup>
 <thead>
-<tr class="header">
-<th>dir/ or file</th>
-<th>contains</th>
-</tr>
+<tr><th>dir/ or file</th><th>contains</th></tr>
 </thead>
 <tbody>
-<tr class="odd">
-<td><code>bin/</code></td>
-<td>used by eclipse for compiled classes -- no need to touch this</td>
-</tr>
-<tr class="even">
-<td><code>build/</code></td>
-<td>the gradle build dir</td>
-</tr>
-<tr class="odd">
-<td><code>classes/</code></td>
-<td>contains the compiled Java classes for the Jalview application</td>
-</tr>
-<tr class="even">
-<td><code>dist/</code></td>
-<td>assembled <code>.jar</code> files needed to run Jalview application</td>
-</tr>
-<tr class="odd">
-<td><code>examples/</code></td>
-<td>example input files usable by Jalview</td>
-</tr>
-<tr class="even">
-<td><code>getdown/</code></td>
-<td>the libraries used by the Javliew launcher (getdown)</td>
-</tr>
-<tr class="odd">
-<td><code>getdown/src/</code></td>
-<td>our modified source for <code>getdown</code></td>
-</tr>
-<tr class="even">
-<td><code>getdown/website/</code></td>
-<td>the assembled &quot;download&quot; folder used by getdown for downloads/upgrades</td>
-</tr>
-<tr class="odd">
-<td><code>getdown/files/</code></td>
-<td>the minimal fileset to launch the Jalview launcher, which can then download the rest of the Jalview application</td>
-</tr>
-<tr class="even">
-<td><code>help/</code></td>
-<td>the help documents</td>
-</tr>
-<tr class="odd">
-<td><code>j8lib/</code></td>
-<td>libraries needed to run Jalview under Java 1.8</td>
-</tr>
-<tr class="even">
-<td><code>j11lib/</code></td>
-<td>libraries needed to run Jalivew under Java 11</td>
-</tr>
-<tr class="odd">
-<td><code>resource/</code></td>
-<td>non-java resources used in the Jalview application</td>
-</tr>
-<tr class="even">
-<td><code>src/</code></td>
-<td>the Jalview application source <code>.java</code> files</td>
-</tr>
-<tr class="odd">
-<td><code>test/</code></td>
-<td>Test class source files</td>
-</tr>
-<tr class="even">
-<td><code>utils/</code></td>
-<td>helper applications used in the build process</td>
-</tr>
-<tr class="odd">
-<td><code>utils/install4j/</code></td>
-<td>files used by the packaging tool, install4j</td>
-</tr>
-<tr class="even">
-<td><code>build.gradle</code></td>
-<td>the build file used by gradle</td>
-</tr>
-<tr class="odd">
-<td><code>gradle.properties</code></td>
-<td>configurable properties for the build process</td>
-</tr>
+<tr><td><code>bin/</code></td><td>used by eclipse for compiled classes -- no need to touch this</td></tr>
+<tr><td><code>build/</code></td><td>the gradle build dir</td></tr>
+<tr><td><code>classes/</code></td><td>contains the compiled Java classes for the Jalview application</td></tr>
+<tr><td><code>dist/</code></td><td>assembled <code>.jar</code> files needed to run Jalview application</td></tr>
+<tr><td><code>examples/</code></td><td>example input files usable by Jalview</td></tr>
+<tr><td><code>getdown/</code></td><td>the libraries used by the Javliew launcher (getdown)</td></tr>
+<tr><td><code>getdown/src/</code></td><td>our modified source for <code>getdown</code></td></tr>
+<tr><td><code>getdown/website/</code></td><td>the assembled &quot;download&quot; folder used by getdown for downloads/upgrades</td></tr>
+<tr><td><code>getdown/files/</code></td><td>the minimal fileset to launch the Jalview launcher, which can then download the rest of the Jalview application</td></tr>
+<tr><td><code>help/</code></td><td>the help documents</td></tr>
+<tr><td><code>j8lib/</code></td><td>libraries needed to run Jalview under Java 1.8</td></tr>
+<tr><td><code>j11lib/</code></td><td>libraries needed to run Jalivew under Java 11</td></tr>
+<tr><td><code>resource/</code></td><td>non-java resources used in the Jalview application</td></tr>
+<tr><td><code>src/</code></td><td>the Jalview application source <code>.java</code> files</td></tr>
+<tr><td><code>test/</code></td><td>Test class source files</td></tr>
+<tr><td><code>utils/</code></td><td>helper applications used in the build process</td></tr>
+<tr><td><code>utils/install4j/</code></td><td>files used by the packaging tool, install4j</td></tr>
+<tr><td><code>build.gradle</code></td><td>the build file used by gradle</td></tr>
+<tr><td><code>gradle.properties</code></td><td>configurable properties for the build process</td></tr>
+<tr><td><code>RELEASE</code></td><td>propertyfile configuring JALVIEW_VERSION (from jalview.version) and the release branch (from jalview.release). An alternative file can be specified via JALVIEW_RELEASE_FILE property</td></tr>
 </tbody>
 </table>
 <p>Note that you need a Java 11 JDK to compile Jalview whether your target build is Java 1.8 or Java 11.</p>
-<h2 id="building-jalview">Building Jalview</h2>
-<p>You will need to have the Java 11 <code>javac</code> in your path, or alternatively you can configure gradle to know where this is by putting</p>
-<pre><code>org.gradle.java.home=/path_to_jdk_directory</code></pre>
+<h2 id="building-jalview"><a href="#building-jalview" name="building-jalview" class="anchor"><span class="octicon octicon-link"></span>Building Jalview</a></h2>
+<p>You will need to have the Java 11 <code>javac</code> in your path, or alternatively you can configure
+gradle to know where this is by putting</p>
+<pre><code>org.gradle.java.home=/path_to_jdk_directory
+</code></pre>
 <p>in the <code>gradle.properties</code> file.</p>
 <blockquote>
 <p><em>You may want to see some of the other properties you can change at the end of this document.</em></p>
 </blockquote>
-<h3 id="minimal-jalview-build">Minimal Jalview Build</h3>
+<h3 id="minimal-jalview-build"><a href="#minimal-jalview-build" name="minimal-jalview-build" class="anchor"><span class="octicon octicon-link"></span>Minimal Jalview Build</a></h3>
 <p>To compile the necessary class files, just run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> compileJava</code></pre></div>
-<p>to compile the classes into the <code>classes</code> folder. You should now be able to run the Jalview application directly with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;classes:resources:help:j11lib/*&quot;</span> jalview.bin.Jalview</code></pre></div>
-<p>You can also run with an automatic large memory setting (which will set the maximum memory heap of the Jalview JVM to 90% of your local physical memory) and docked icon setting (if possible in your OS) with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;classes:resources:help:j11lib/*&quot;</span> jalview.bin.Launcher</code></pre></div>
+<pre><code class="language-bash">gradle compileJava
+</code></pre>
+<p>to compile the classes into the <code>classes</code> folder.
+You should now be able to run the Jalview application directly with</p>
+<pre><code class="language-bash">java -cp &quot;classes:resources:help:j11lib/*&quot; jalview.bin.Jalview
+</code></pre>
+<p>You can also run with an automatic large memory setting (which will set the maximum
+memory heap of the Jalview JVM to 90% of your local physical memory) and docked icon setting
+(if possible in your OS) with</p>
+<pre><code class="language-bash">java -cp &quot;classes:resources:help:j11lib/*&quot; jalview.bin.Launcher
+</code></pre>
 <blockquote>
-<p><em>You must use just &quot;<code>j11lib/*</code>&quot; and not &quot;<code>j11lib/*.jar</code>&quot; as this is a special Java classpath argument wildcard interpreted by <code>java</code>, <strong>not</strong> a shell expansion wildcard interpreted by the shell.</em></p>
+<p><em>You must use just &quot;<code>j11lib/*</code>&quot; and not &quot;<code>j11lib/*.jar</code>&quot; as this is a special Java
+classpath argument wildcard interpreted by <code>java</code>, <strong>not</strong> a shell expansion wildcard interpreted
+by the shell.</em></p>
 </blockquote>
-<p>Note that <code>jalview.bin.Launcher</code> is a simplified launcher class that re-launches <code>jalview.bin.Jalview</code> with the same JRE (<em>not</em> the same JVM instance), classpath and arguments, but with an automatically determined <code>-Xmx...</code> memory setting if one hasn't been provided.</p>
-<h3 id="jalview-in-a-jar-file">Jalview in a Jar File</h3>
+<p>Note that <code>jalview.bin.Launcher</code> is a simplified launcher class that re-launches <code>jalview.bin.Jalview</code>
+with the same JRE (<em>not</em> the same JVM instance), classpath and arguments, but with an automatically determined <code>-Xmx...</code>
+memory setting if one hasn't been provided.</p>
+<h3 id="jalview-in-a-jar-file"><a href="#jalview-in-a-jar-file" name="jalview-in-a-jar-file" class="anchor"><span class="octicon octicon-link"></span>Jalview in a Jar File</a></h3>
 <p>To package the <code>classes</code>, <code>resources</code>, and <code>help</code> into one jar, you can run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> jar</code></pre></div>
+<pre><code class="language-bash">gradle jar
+</code></pre>
 <p>which assembles the Jalview classes and resources into <code>dist/jalview.jar</code></p>
 <p>To run this, use</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;dist/jalview.jar:j11lib/*&quot;</span> jalview.bin.Jalview</code></pre></div>
-<h3 id="distributed-jar-files">Distributed Jar Files</h3>
-<p>To simplify this, all required <code>.jar</code> files can be assembled into the <code>dist</code> folder using</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> makeDist</code></pre></div>
+<pre><code class="language-bash">java -cp &quot;dist/jalview.jar:j11lib/*&quot; jalview.bin.Jalview
+</code></pre>
+<h3 id="distributed-jar-files"><a href="#distributed-jar-files" name="distributed-jar-files" class="anchor"><span class="octicon octicon-link"></span>Distributed Jar Files</a></h3>
+<p>To simplify this, all required <code>.jar</code> files can be assembled into the <code>dist</code> folder
+using</p>
+<pre><code class="language-bash">gradle makeDist
+</code></pre>
 <p>which puts all required jar files into <code>dist</code> so you can run with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -cp <span class="st">&quot;dist/*&quot;</span> jalview.bin.Jalview</code></pre></div>
-<h3 id="single-shadow-jar-file">Single <em>shadow</em> Jar File</h3>
-<p>The shadow jar file is a single <code>.jar</code> that contains all required classes and resources from <code>jalview.jar</code> and all of the supporting libraries in <code>j11lib/*.jar</code> merged into one <code>.jar</code> archive file. A default launching class (<code>MAIN-CLASS: jalview.bin.Launcher</code>) is specified in the <code>.jar</code> manifest file (<code>META/MANIFEST.MF</code>) so a start class doesn't need to be specified.</p>
+<pre><code class="language-bash">java -cp &quot;dist/*&quot; jalview.bin.Jalview
+</code></pre>
+<h3 id="single-shadow-jar-file"><a href="#single-shadow-jar-file" name="single-shadow-jar-file" class="anchor"><span class="octicon octicon-link"></span>Single <em>shadow</em> Jar File</a></h3>
+<p>The shadow jar file is a single <code>.jar</code> that contains all required classes and resources from <code>jalview.jar</code>
+and all of the supporting libraries in <code>j11lib/*.jar</code> merged into one <code>.jar</code> archive
+file.  A default launching class (<code>MAIN-CLASS: jalview.bin.Launcher</code>) is specified in the <code>.jar</code>
+manifest file (<code>META/MANIFEST.MF</code>) so a start class doesn't need to be specified.</p>
 <p>Build the shadow jar file in <code>build/lib/jalview-all-11.jar</code> with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> shadowJar</code></pre></div>
+<pre><code class="language-bash">gradle shadowJar
+</code></pre>
 <p>and run it with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar build/lib/jalview-all-11.jar</code></pre></div>
-<p>Because no arguments are required, most OSes will associate a <code>.jar</code> file with the <code>java</code> application (if this has been installed through the OS and not just a local unzip) as a <code>-jar</code> argument so you may find you can launch <code>jalview-all-11.jar</code> just by double-clicking on it)!</p>
+<pre><code class="language-bash">java -jar build/lib/jalview-all-11.jar
+</code></pre>
+<p>Because no arguments are required, most OSes will associate a <code>.jar</code> file with the
+<code>java</code> application (if this has been installed through the OS and not just a local
+unzip) as a <code>-jar</code> argument so you may find you can launch <code>jalview-all-11.jar</code>
+just by double-clicking on it)!</p>
 <blockquote>
-<p>The <code>shadowJar</code> task is not a requirement for any other task, so to build the shadow jar file you must specify the <code>shadowJar</code> task.</p>
+<p>The <code>shadowJar</code> task is not a requirement for any other task, so to build the shadow
+jar file you must specify the <code>shadowJar</code> task.</p>
 </blockquote>
 <blockquote>
-<p>The shadow jar file represents probably the simplest way to distribute the Jalview application to machines that already have a Java 11 installed, although without the many and compelling benefits of the <code>getdown</code> launcher.</p>
+<p>The shadow jar file represents probably the simplest way to distribute the Jalview application to machines that already have a Java 11 installed,
+although without the many and compelling benefits of the <code>getdown</code> launcher.</p>
 </blockquote>
-<h3 id="building-the-getdown-launcher">Building the <code>getdown</code> launcher</h3>
-<p>We have made significant customisations to the <code>getdown</code> launcher which you can find in <code>getdown/src/getdown</code>.</p>
+<h3 id="building-the-getdown-launcher"><a href="#building-the-getdown-launcher" name="building-the-getdown-launcher" class="anchor"><span class="octicon octicon-link"></span>Building the <code>getdown</code> launcher</a></h3>
+<p>We have made significant customisations to the <code>getdown</code> launcher which you can find
+in <code>getdown/src/getdown</code>.</p>
 <blockquote>
-<p>You don't need to build this afresh as the required <code>gradle-core.jar</code> and <code>gradle-launcher.jar</code> files are already distributed in <code>j11lib</code> and <code>getdown/lib</code> but if you want to, then you'll need a working Maven and also a Java 8 JDK. Ensure the Java 8 <code>javac</code> is forefront in your path and do</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="bu">cd</span> getdown/src/getdown
-<span class="ex">mvn</span> clean package -Dgetdown.host.whitelist=<span class="st">&quot;jalview.org,*.jalview.org&quot;</span></code></pre></div>
-<p>and you will find the required <code>.jar</code> files in <code>core/target/gradle-core-XXX.jar</code> and <code>launcher/target/gradle-launcher-XXX.jar</code>. The <code>gradle-core.jar</code> should then be copied to all three of the <code>j8lib</code>, <code>j11lib</code> and <code>getdown/lib</code> folders, whilst the <code>gradle-launcher.jar</code> only needs to be copied to <code>getdown/lib</code>.</p>
-<p>The <code>mvn</code> command should ideally include the <code>-Dgetdown.host.whitelist=*.jalview.org</code> setting. This, and the necessary file copying commands, can be found in <code>getdown/src/getdown/mvn_cmd</code>.</p>
+<p>You don't need to build this afresh as the required <code>gradle-core.jar</code>
+and <code>gradle-launcher.jar</code> files are already distributed in <code>j11lib</code> and <code>getdown/lib</code> but if you want to, then
+you'll need a working Maven and also a Java 8 JDK.  Ensure the Java 8 <code>javac</code> is forefront
+in your path and do</p>
+<pre><code class="language-bash">cd getdown/src/getdown
+mvn clean package -Dgetdown.host.whitelist=&quot;jalview.org,*.jalview.org&quot;
+</code></pre>
+<p>and you will find the required <code>.jar</code> files in <code>core/target/gradle-core-XXX.jar</code>
+and <code>launcher/target/gradle-launcher-XXX.jar</code>.  The <code>gradle-core.jar</code> should then be copied
+to all three of the <code>j8lib</code>, <code>j11lib</code> and <code>getdown/lib</code> folders, whilst the <code>gradle-launcher.jar</code> only
+needs to be copied to <code>getdown/lib</code>.</p>
+<p>The <code>mvn</code> command should ideally include the <code>-Dgetdown.host.whitelist=*.jalview.org</code> setting.
+This, and the necessary file copying commands, can be found in <code>getdown/src/getdown/mvn_cmd</code>.</p>
 </blockquote>
 <p>To assemble Jalview with <code>getdown</code> use the following gradle task:</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> getdown</code></pre></div>
-<p>This puts all the necessary files to launch Jalview with <code>getdown</code> into <code>getdown/website/11/</code>. This could be treated as the reference folder for <code>getdown</code>, which is where a getdown launcher will check to see if the Jalview application files it has are up to date, and download if they aren't or it simply doesn't have them.</p>
-<p>A minimal getdown-launcher can be found in <code>getdown/files/11/</code> which checks its up-to-date status with (the absolute path to) <code>getdown/website/11/</code>.</p>
+<pre><code class="language-bash">gradle getdown
+</code></pre>
+<p>This puts all the necessary files to launch Jalview with <code>getdown</code>
+into <code>getdown/website/11/</code>.  This could be treated as the reference folder
+for <code>getdown</code>, which is where a getdown launcher will check to see if the Jalview application
+files it has are up to date, and download if they aren't or it simply doesn't have
+them.</p>
+<p>A minimal getdown-launcher can be found in <code>getdown/files/11/</code> which checks its up-to-date
+status with (the absolute path to) <code>getdown/website/11/</code>.</p>
 <p>This can be launched with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar getdown/files/11/getdown-launcher.jar getdown/files/11/ jalview</code></pre></div>
+<pre><code class="language-bash">java -jar getdown/files/11/getdown-launcher.jar getdown/files/11/ jalview
+</code></pre>
 <blockquote>
-<p>We've already met the <code>-jar file.jar</code> arguments. The next argument is the working folder for getdown, and the final argument, &quot;<code>jalview</code>&quot;, is a getdown application id (only &quot;<code>jalview</code>&quot; is defined here).</p>
+<p>We've already met the <code>-jar file.jar</code> arguments.  The next argument is the working folder for
+getdown, and the final argument, &quot;<code>jalview</code>&quot;, is a getdown application id (only &quot;<code>jalview</code>&quot;
+is defined here).</p>
 </blockquote>
-<h3 id="running-tests">Running tests</h3>
+<h3 id="running-tests"><a href="#running-tests" name="running-tests" class="anchor"><span class="octicon octicon-link"></span>Running tests</a></h3>
 <p>There are substantial tests written for Jalview that use TestNG, which you can run with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test</code></pre></div>
-<p>These normally take around 5 - 10 minutes to complete and outputs its full results into the <code>tests/</code> folder. A summary of results should appear in your console.</p>
+<pre><code class="language-bash">gradle test
+</code></pre>
+<p>These normally take around 5 - 10 minutes to complete and outputs its full results into
+the <code>tests/</code> folder.  A summary of results should appear in your console.</p>
 <p>You can run different defined groups of tests with</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test -PtestngGroups=Network</code></pre></div>
+<pre><code class="language-bash">gradle test -PtestngGroups=Network
+</code></pre>
 <p>Available groups include Functional (default), Network, External.</p>
-<h4 id="excluding-some-tests">Excluding some tests</h4>
+<h4 id="excluding-some-tests"><a href="#excluding-some-tests" name="excluding-some-tests" class="anchor"><span class="octicon octicon-link"></span>Excluding some tests</a></h4>
 <p>Some of Jalview's Functional tests don't pass reliably in all environments. We tag these tests with a group like 'Not-bamboo' to mark them for exclusion when we run tests as part of continuous integration.</p>
 <p>To exclude one or more groups of tests, add them as a comma separated list in testngExcludedGroups.</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> test -PtestngExcludedGroups=Not-bamboo</code></pre></div>
-<h3 id="installer-packaging-with-install4j">Installer packaging with <em>install4j</em></h3>
-<p>Jalview is currently using <em>install4j</em> <a href="https://www.ej-technologies.com/products/install4j/overview.html" class="uri">https://www.ej-technologies.com/products/install4j/overview.html</a> as its installer packaging tool.</p>
-<p>If you have a licensed installation of <em>install4j</em> you can build Jalview installers by running</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> installers</code></pre></div>
-<p>though you may need to fiddle with the <code>install4j</code> and <code>copyInstall4jTemplate</code> tasks in <code>build.gradle</code> file to point to your installation of <em>install4j</em> and also to bundled JREs if you want to bundle those into the installers.</p>
-<p>If you want more details, get in touch on our development mailing list <a href="mailto:jalview-dev@jalview.org">jalview-dev@jalview.org</a>. Sign up at <a href="http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev" class="uri">http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev</a>.</p>
-<h2 id="gradle-properties">Gradle properties</h2>
-<p>There are a lot of properties configured in <code>gradle.properties</code> which we strongly recommend being left as they are unless you have a specific problem with the build process.</p>
-<p>There are a few gradle properties you might want to set on the command line with the <code>-P</code> flag when building a version of Jalview with specific requirements:</p>
-<h4 id="java_version"><code>JAVA_VERSION</code></h4>
-<p>This changes the <em>target</em> java bytecode version &gt; NOTE that you will need to use a Java 11 (or greater) JDK Java compiler to build Jalview for any byte-code target version.</p>
+<pre><code class="language-bash">gradle test -PtestngExcludedGroups=Not-bamboo
+</code></pre>
+<h3 id="installer-packaging-with-install4j"><a href="#installer-packaging-with-install4j" name="installer-packaging-with-install4j" class="anchor"><span class="octicon octicon-link"></span>Installer packaging with <em>install4j</em></a></h3>
+<p>Jalview is currently using <em>install4j</em> <a href="https://www.ej-technologies.com/products/install4j/overview.html">https://www.ej-technologies.com/products/install4j/overview.html</a>
+as its installer packaging tool.</p>
+<p>If you have a licensed installation of <em>install4j</em> you can build Jalview installers
+by running</p>
+<pre><code class="language-bash">gradle installers
+</code></pre>
+<p>though you may need to fiddle with the <code>install4j</code> and <code>copyInstall4jTemplate</code> tasks
+in <code>build.gradle</code> file to point to your installation of <em>install4j</em> and also to bundled
+JREs if you want to bundle those into the installers.</p>
+<p>If you want more details, get in touch on our development mailing list <a href="mailto:jalview-dev@jalview.org">jalview-dev@jalview.org</a>.
+Sign up at <a href="http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev">http://www.compbio.dundee.ac.uk/mailman/listinfo/jalview-dev</a>.</p>
+<h2 id="gradle-properties"><a href="#gradle-properties" name="gradle-properties" class="anchor"><span class="octicon octicon-link"></span>Gradle properties</a></h2>
+<p>There are a lot of properties configured in <code>gradle.properties</code> which we strongly recommend
+being left as they are unless you have a specific problem with the build process.</p>
+<p>There are a few gradle properties you might want to set on the command line with the
+<code>-P</code> flag when building a version of Jalview with specific requirements:</p>
+<h4 id="java-version"><a href="#java-version" name="java-version" class="anchor"><span class="octicon octicon-link"></span><code>JAVA_VERSION</code></a></h4>
+<p>This changes the <em>target</em> java bytecode version</p>
+<blockquote>
+<p>NOTE that you will need to use a Java 11 (or greater) JDK Java compiler to build
+Jalview for any byte-code target version.</p>
+</blockquote>
 <p>Valid values are <code>11</code> and <code>1.8</code>.</p>
 <p>e.g.</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> shadowJar -PJAVA_VERSION=1.8</code></pre></div>
+<pre><code class="language-bash">gradle shadowJar -PJAVA_VERSION=1.8
+</code></pre>
 <p>When using <code>-PJAVA_VERSION=1.8</code> the libraries from <code>j8lib</code> (instead of <code>j11lib</code>) will be used in the compile<br />
-and runtime classpath and also used in the <code>makeDist</code> build step. Where a Java version of <code>11</code> is used in folder and file names, it will instead use <code>1.8</code>. Also if you are building installer packages with <em>install4j</em> the package builder will look for JRE 1.8 bundles to package in the installers.</p>
+and runtime classpath and also used in the <code>makeDist</code> build step.  Where a Java version of <code>11</code> is used in folder and file names, it will
+instead use <code>1.8</code>.  Also if you are building installer packages with <em>install4j</em> the
+package builder will look for JRE 1.8 bundles to package in the installers.</p>
 <blockquote>
-<p>Note that continued development of Jalview will assume a Java 11+ runtime environment, the 2.11.0 release will run under a Java 1.8 JRE with a few minor features disabled.</p>
+<p>Note that continued development of Jalview will assume a Java 11+ runtime environment,
+the 2.11.0 release will run under a Java 1.8 JRE with a few minor features disabled.</p>
 </blockquote>
-<h4 id="channel"><code>CHANNEL</code></h4>
-<p>This changes the <code>appbase</code> setting in <code>getdown.txt</code> (<code>appbase</code> is where the getdown launcher looks to see if there's an updated file) to point to a particular Jalview channel or some other appropriate place to look for required files. If the selected channel type requires the getdown <code>appbase</code> to be a local directory on the filesystem (instead of a website URL) then a modified version of the <code>getdown-launcher.jar</code> will be used to allow this. The two versions of the <code>getdown-launcher.jar</code> can be found in <code>getdown/lib</code>. Some other variables used in the build process might also be set differently depending on the value of <code>CHANNEL</code> to allow smooth operation of getdown in the given context.</p>
-<p>There are several values of <code>CHANNEL</code> that can be chosen, with a default of <code>LOCAL</code>. Here's what they're for and what they do:</p>
+<h4 id="channel"><a href="#channel" name="channel" class="anchor"><span class="octicon octicon-link"></span><code>CHANNEL</code></a></h4>
+<p>This changes the <code>appbase</code> setting in <code>getdown.txt</code> (<code>appbase</code> is where the getdown launcher
+looks to see if there's an updated file) to point to a particular Jalview channel or some other appropriate
+place to look for required files.  If the selected channel type requires the getdown <code>appbase</code> to be a local
+directory on the filesystem (instead of a website URL) then a modified version of the <code>getdown-launcher.jar</code> will
+be used to allow this.  The two versions of the <code>getdown-launcher.jar</code> can be found in <code>getdown/lib</code>.
+Some other variables used in the build process might also be set differently depending on the value of <code>CHANNEL</code>
+to allow smooth operation of getdown in the given context.</p>
+<p>There are several values of <code>CHANNEL</code> that can be chosen, with a default of <code>LOCAL</code>.  Here's what they're for and what they do:</p>
 <ul>
-<li><code>LOCAL</code>: This is for running the compiled application from the development directory. It will set
+<li><code>LOCAL</code>: This is for running the compiled application from the development directory.
+It will set
 <ul>
-<li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/files/JAVA_VERSION</code> (e.g. <code>file://home/user/git/jalview/getdown/files/11</code>)</li>
+<li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/files/JAVA_VERSION</code>
+(e.g. <code>file://home/user/git/jalview/getdown/files/11</code>)</li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>BUILD</code>: This is for creating an appbase channel on the build server by an automatic or manually started build. It will set
+</ul>
+</li>
+<li><code>BUILD</code>: This is for creating an appbase channel on the build server by an automatic or manually started build.
+It will set
 <ul>
-<li><code>appbase</code> as <code>https://builds.jalview.org/browse/${bamboo_planKey}/latest/artifact/shared/getdown-channel/JAVA_VERSION</code> Note that bamboo_planKey should be set by the build plan with <code>-Pbamboo_planKey=${bamboo.planKey}</code></li>
+<li><code>appbase</code> as <code>https://builds.jalview.org/browse/${bamboo_planKey}/latest/artifact/shared/getdown-channel/JAVA_VERSION</code>
+Note that bamboo_planKey should be set by the build plan with <code>-Pbamboo_planKey=${bamboo.planKey}</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>DEVELOP</code>: This is for creating a <code>develop</code> appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server. It will set
+</ul>
+</li>
+<li><code>DEVELOP</code>: This is for creating a <code>develop</code> appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server.
+It will set
 <ul>
 <li><code>appbase</code> as <code>http://www.jalview.org/getdown/develop/JAVA_VERSION</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>SCRATCH-NAME</code>: This is for creating a temporary scratch appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server. This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels. The value of <code>NAME</code> can be any &quot;word-character&quot; [A-Za-z0-9_] It will set
+</ul>
+</li>
+<li><code>SCRATCH-NAME</code>: This is for creating a temporary scratch appbase channel on the main web server.  This won't become live until the actual getdown artefact is synced to the web server.  This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels.  The value of <code>NAME</code> can be any &quot;word-character&quot; [A-Za-z0-9_]
+It will set
 <ul>
 <li><code>appbase</code> as <code>http://www.jalview.org/getdown/SCRATCH-NAME/JAVA_VERSION</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>TEST-LOCAL</code>: Like <code>SCRATCH</code> but with a specific <code>test-local</code> channel name and a local filesystem appbase. This is meant for testing an over-the-air update on the local filesystem. An extra property <code>LOCALDIR</code> must be given (e.g. <code>-PLOCALDIR=/home/user/tmp/test</code>) It will set
+</ul>
+</li>
+<li><code>TEST-LOCAL</code>:  Like <code>SCRATCH</code> but with a specific <code>test-local</code> channel name and a local filesystem appbase.  This is meant for testing an over-the-air update on the local filesystem.  An extra property <code>LOCALDIR</code> must be given (e.g. <code>-PLOCALDIR=/home/user/tmp/test</code>)
+It will set
 <ul>
 <li><code>appbase</code> as <code>file://${LOCALDIR}</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>TEST-RELEASE</code>: Like <code>SCRATCH</code> but with a specific <code>test-release</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels. It will set
+</ul>
+</li>
+<li><code>TEST-RELEASE</code>:  Like <code>SCRATCH</code> but with a specific <code>test-release</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.  This is meant for testing an over-the-air update without interfering with the live <code>release</code> or <code>develop</code> channels.
+It will set
 <ul>
 <li><code>appbase</code> as <code>http://www.jalview.org/getdown/test-release/JAVA_VERSION</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>RELEASE</code>: This is for an actual release build, and will use an appbase on the main web server with the main <code>release</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. It will set
+</ul>
+</li>
+<li><code>RELEASE</code>:  This is for an actual release build, and will use an appbase on the main web server with the main <code>release</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.
+It will set
 <ul>
 <li><code>appbase</code> as <code>http://www.jalview.org/getdown/release/JAVA_VERSION</code></li>
 <li>application subdir as <code>release</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>ARCHIVE</code>: This is a helper to create a channel for a specific release version, and will use an appbase on the main web server with a specific <code>archive/JALVIEW_VERSION</code> channel name. This won't become live until the actual getdown artefact is synced to the web server. You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files. This should create a getdown structure and digest with the older jar files. It will set
+</ul>
+</li>
+<li><code>ARCHIVE</code>:  This is a helper to create a channel for a specific release version, and will use an appbase on the main web server with a specific <code>archive/JALVIEW_VERSION</code> channel name.  This won't become live until the actual getdown artefact is synced to the web server.
+You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files.  This should create a getdown structure and digest with the older jar files.
+It will set
 <ul>
 <li><code>appbase</code> as <code>http://www.jalview.org/getdown/archive/JALVIEW_VERSION/JAVA_VERSION</code></li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher cannot use a <code>file://</code> scheme appbase.</li>
-</ul></li>
-<li><code>ARCHIVELOCAL</code>: Like <code>ARCHIVE</code> but with a local filesystem appbase for local testing. You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files. This should create a getdown structure and digest with the older jar files. It will set
+</ul>
+</li>
+<li><code>ARCHIVELOCAL</code>:  Like <code>ARCHIVE</code> but with a local filesystem appbase for local testing.
+You must also specify an <code>ARCHIVEDIR</code> property that points to an earlier version of Jalview with a <code>dist</code> directory containing the required jar files.  This should create a getdown structure and digest with the older jar files.
+It will set
 <ul>
 <li><code>appbase</code> as <code>file://PATH/TO/YOUR/DEVELOPMENT/getdown/website/JAVA_VERSION</code> (where the old jars will have been copied and digested)</li>
 <li>application subdir as <code>alt</code></li>
 <li>Getdown launcher can use a <code>file://</code> scheme appbase.</li>
-</ul></li>
+</ul>
+</li>
 </ul>
 <p>e.g.</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> getdown -PCHANNEL=SCRATCH-my_test_version</code></pre></div>
-<h4 id="install4jmediatypes"><code>install4jMediaTypes</code></h4>
-<p>If you are building <em>install4j</em> installers (requires <em>install4j</em> to be installed) then this property specifies a comma-separated list of media types (i.e. platform specific installers) <em>install4j</em> should actually build.</p>
-<p>Currently the valid values are <code>linuxDeb</code>, <code>linuxRPM</code>, <code>macosArchive</code>, <code>unixArchive</code>, <code>unixInstaller</code>, <code>windows</code></p>
+<pre><code class="language-bash">gradle getdown -PCHANNEL=SCRATCH-my_test_version
+</code></pre>
+<h4 id="jalview-version-and-the-release-file"><a href="#jalview-version-and-the-release-file" name="jalview-version-and-the-release-file" class="anchor"><span class="octicon octicon-link"></span>JALVIEW_VERSION and the RELEASE file</a></h4>
+<p>Any Jalview build will include the value of JALVIEW_VERSION in various places, including the 'About' and Jalview Desktop window title, and in filenames for the stand-alone executable jar. You can specify a custom version for a build via the JALVIEW_VERSION property, but for most situations, JALVIEW_VERSION will be automatically configured according to the value of the CHANNEL property, using the <code>jalview.version</code> property specified in the RELEASE file:</p>
+<ul>
+<li><code>CHANNEL=RELEASE</code> will set version to jalview.version</li>
+<li><code>CHANNEL=TEST or DEVELOP</code> will append '-test' or '-develop' to jalview.version</li>
+</ul>
+<p>It is also possible to specify a custom location for the RELEASE file via an optional JALVIEW_RELEASE_FILE property.</p>
+<h4 id="install4jmediatypes"><a href="#install4jmediatypes" name="install4jmediatypes" class="anchor"><span class="octicon octicon-link"></span><code>install4jMediaTypes</code></a></h4>
+<p>If you are building <em>install4j</em> installers (requires <em>install4j</em> to be installed) then this property specifies a comma-separated
+list of media types (i.e. platform specific installers) <em>install4j</em> should actually build.</p>
+<p>Currently the valid values are
+<code>linuxDeb</code>,
+<code>linuxRPM</code>,
+<code>macosArchive</code>,
+<code>unixArchive</code>,
+<code>unixInstaller</code>,
+<code>windows</code></p>
 <p>The default value is all of them.</p>
 <p>e.g.</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">gradle</span> installers -PJAVA_VERSION=1.8 -Pinstall4jMediaTypes=macosArchive</code></pre></div>
+<pre><code class="language-bash">gradle installers -PJAVA_VERSION=1.8 -Pinstall4jMediaTypes=macosArchive
+</code></pre>
 <p>To get an up-to-date list of possible values, you can run</p>
-<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">perl</span> -n -e <span class="st">&#39;m/^\s*&lt;(\w+)[^&gt;]*\bmediaFileName=/ &amp;&amp; print &quot;$1\n&quot;;&#39;</span> utils/install4j/install4j_template.install4j  <span class="kw">|</span> <span class="fu">sort</span> -u</code></pre></div>
+<pre><code class="language-bash">perl -n -e 'm/^\s*&lt;(\w+)[^&gt;]*\bmediaFileName=/ &amp;&amp; print &quot;$1\n&quot;;' utils/install4j/install4j_template.install4j  | sort -u
+</code></pre>
 <p>in the <code>jalview</code> root folder.</p>
-<h2 id="enabling-code-coverage-with-openclover">Enabling Code Coverage with OpenClover</h2>
+<h2 id="enabling-code-coverage-with-openclover"><a href="#enabling-code-coverage-with-openclover" name="enabling-code-coverage-with-openclover" class="anchor"><span class="octicon octicon-link"></span>Enabling Code Coverage with OpenClover</a></h2>
 <p>Bytecode instrumentation tasks are enabled by specifying 'true' (or just a non-whitespace non-numeric word) in the 'clover' property. This adds the 'openclover' plugin to the build script's classpath, making it possible to track code execution during test which can be viewed as an HTML report published at build/reports/clover/index.html.</p>
 <p><code>gradle -Pclover=true test cloverReport</code></p>
-<h4 id="troubleshooting-report-generation">Troubleshooting report generation</h4>
+<h4 id="troubleshooting-report-generation"><a href="#troubleshooting-report-generation" name="troubleshooting-report-generation" class="anchor"><span class="octicon octicon-link"></span>Troubleshooting report generation</a></h4>
 <p>The build forks a new JVM process to run the clover report generation tools (both XML and HTML reports are generated by default). The following properties can be used to specify additional options or adjust JVM memory settings. Default values for these options are:</p>
-<h5 id="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported">JVM Memory settings - increase if out of memory errors are reported</h5>
+<h5 id="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported"><a href="#jvm-memory-settings---increase-if-out-of-memory-errors-are-reported" name="jvm-memory-settings---increase-if-out-of-memory-errors-are-reported" class="anchor"><span class="octicon octicon-link"></span>JVM Memory settings - increase if out of memory errors are reported</a></h5>
 <p><code>cloverReportJVMHeap = 2g</code></p>
-<h5 id="dfile.encodingutf-8-is-an-essential-parameters-for-report-generation.-add-additional-ones-separated-by-a-space.">-Dfile.encoding=UTF-8 is an essential parameters for report generation. Add additional ones separated by a space.</h5>
+<h5 id="-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space"><a href="#-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space" name="-dfileencodingutf-8-is-an-essential-parameters-for-report-generation-add-additional-ones-separated-by-a-space" class="anchor"><span class="octicon octicon-link"></span>-Dfile.encoding=UTF-8 is an essential parameters for report generation. Add additional ones separated by a space.</a></h5>
 <p><code>cloverReportJVMArgs = -Dfile.encoding=UTF-8</code></p>
-<h5 id="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database">Add -v to debug velocity html generation errors, or -d to track more detailed issues with the coverage database</h5>
+<h5 id="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database"><a href="#add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database" name="add--v-to-debug-velocity-html-generation-errors-or--d-to-track-more-detailed-issues-with-the-coverage-database" class="anchor"><span class="octicon octicon-link"></span>Add -v to debug velocity html generation errors, or -d to track more detailed issues with the coverage database</a></h5>
 <p><code>cloverReportHTMLOptions =</code></p>
-<h5 id="v-for-verbose--d-for-debug-level-messages-as-above">-v for verbose, -d for debug level messages (as above)</h5>
+<h5 id="-v-for-verbose--d-for-debug-level-messages-as-above"><a href="#-v-for-verbose--d-for-debug-level-messages-as-above" name="-v-for-verbose--d-for-debug-level-messages-as-above" class="anchor"><span class="octicon octicon-link"></span>-v for verbose, -d for debug level messages (as above)</a></h5>
 <p><code>cloverReportXMLOptions =</code></p>
 <p><em>Note</em> do not forget to include the -Dfile.encoding=UTF-8 option: this is essential for some platforms in order for Clover to correctly parse some Jalview source files that contain characters that are UTF-8 encoded.</p>
-<h2 id="setting-up-in-eclipse-ide">Setting up in Eclipse IDE</h2>
-<h3 id="installing-eclipse-ide">Installing Eclipse IDE</h3>
-<p>We develop in Eclipse, and support settings to develop and save Jalview source code in our preferred style. We also support running the Jalview application, debugging and running tests with TestNG from within Eclipse.</p>
-<p>To get Jalview set up as a project in Eclipse, we recommend using at least the 2019-12 version of Eclipse IDE for Java Developers which you can download from the Eclipse website: <a href="https://www.eclipse.org/downloads/" class="uri">https://www.eclipse.org/downloads/</a>. Since Eclipse 2020-03 you are encouraged to use the Eclipse Installer (see the Eclipse Downloads page). In the installer, when given a choice of packages for Eclipse you should choose the &quot;Eclipse IDE for Enterprise Java Developers&quot; package.</p>
-<div class="figure">
-<img src="./images/eclipse_installer.png" title="Eclipse Installer screenshot" />
-
-</div>
+<h2 id="setting-up-in-eclipse-ide"><a href="#setting-up-in-eclipse-ide" name="setting-up-in-eclipse-ide" class="anchor"><span class="octicon octicon-link"></span>Setting up in Eclipse IDE</a></h2>
+<h3 id="installing-eclipse-ide"><a href="#installing-eclipse-ide" name="installing-eclipse-ide" class="anchor"><span class="octicon octicon-link"></span>Installing Eclipse IDE</a></h3>
+<p>We develop in Eclipse, and support settings to develop and save Jalview source code
+in our preferred style.  We also support running the Jalview application, debugging
+and running tests with TestNG from within Eclipse.</p>
+<p>To get Jalview set up as a project in Eclipse, we recommend using at least the 2019-12
+version of Eclipse IDE for Java Developers which you can download from the Eclipse
+website: <a href="https://www.eclipse.org/downloads/">https://www.eclipse.org/downloads/</a>.  Since Eclipse 2020-03 you are encouraged to use the Eclipse Installer (see the Eclipse Downloads page).
+In the installer, when given a choice of packages for Eclipse you should choose the &quot;Eclipse IDE for Enterprise Java Developers&quot; package.</p>
+<p><img src="./images/eclipse_installer.png" alt="" title="Eclipse Installer screenshot" /></p>
 <p>Once Eclipse is installed, we also recommend installing several plugins from the Eclipse Marketplace.</p>
 <p>Some of these should already be installed with the Enterprise Java Developer package:</p>
-<ol style="list-style-type: decimal">
+<ol>
 <li>Buildship Gradle Integration 3.0 (or greater)</li>
 <li>EclEmma Java Code Coverage</li>
 <li>Egit - Git Integration for Eclipse</li>
 </ol>
 <p>To install the others, launch Eclipse, and go to Help-&gt;Eclipse Marketplace...</p>
 <p>Search for and install:</p>
-<ol style="list-style-type: decimal">
+<ol>
 <li>Groovy Development Tools 3.4.0 (or greater)</li>
 <li>Checkstyle Plug-in (optional)</li>
 <li>TestNG for Eclipse (optional -- only needed if you want to run tests from Eclipse)</li>
 </ol>
 <blockquote>
-<p>At time of writing, TestNG for Eclipse does not show up in the Eclipse Marketplace as the latest released version does not install in Eclipse 2019-03. However, you can install a working release of TestNG for Eclipse by going to</p>
+<p>At time of writing, TestNG for Eclipse does not show up in the Eclipse Marketplace
+as the latest released version does not install in Eclipse 2019-03.
+However, you can install a working release of TestNG for Eclipse by going to</p>
 <p>Help-&gt;Install New Software...</p>
 <p>and entering</p>
 <p><code>TestNG Release - https://dl.bintray.com/testng-team/testng-eclipse-release</code></p>
 <p>into the <em>Work with</em> box and click on the <em>Add...</em> button.</p>
-<p>Eclipse might pause for a bit with the word <em>Pending</em> in the table below at this point, but it will eventually list TestNG with a selection box under the <em>Name</em> column.</p>
-<p>Select <em>TestNG</em> and carry on through the install process to install the TestNG plugin.</p>
+<p>Eclipse might pause for a bit with the word <em>Pending</em> in the table below at this point, but it will eventually list TestNG with
+a selection box under the <em>Name</em> column.</p>
+<p>Select <em>TestNG</em> and carry on through the
+install process to install the TestNG plugin.</p>
 </blockquote>
 <p>After installing the plugins, check that Java 11 is set up in Eclipse as the default JRE (see section <a href="#java-11-compliant-jdk">Java 11 compliant JDK</a>).</p>
-<p>To do this go to Preferences (Eclipse-&gt;Preferences in macOS, File-&gt;Preferences on Windows or Window-&gt;Preferences on Linux) and find</p>
+<p>To do this go to Preferences (Eclipse-&gt;Preferences in macOS, File-&gt;Preferences
+on Windows or Window-&gt;Preferences on Linux) and find</p>
 <p>Java -&gt; Installed JREs</p>
 <p>If your Java 11 installation is not listed, click on</p>
 <p><em>Add</em> -&gt; Standard VM -&gt; <em>Next</em></p>
-<p>and enter the JRE home. You can browse to where it is installed. Give it a name (like &quot;AdoptOpenJDK 11&quot;). Select this JDK as the default JRE and click on <em>Apply and Close</em>.</p>
+<p>and enter the JRE home.  You can browse to where it is installed. Give it a name (like &quot;AdoptOpenJDK 11&quot;).  Select this JDK
+as the default JRE and click on <em>Apply and Close</em>.</p>
 <p>You can now import Jalview.</p>
-<h3 id="importing-jalview-as-an-eclipse-project">Importing Jalview as an Eclipse project</h3>
-<h4 id="importing-an-already-downloaded-git-repo">Importing an already downloaded git repo</h4>
+<h3 id="importing-jalview-as-an-eclipse-project"><a href="#importing-jalview-as-an-eclipse-project" name="importing-jalview-as-an-eclipse-project" class="anchor"><span class="octicon octicon-link"></span>Importing Jalview as an Eclipse project</a></h3>
+<h4 id="importing-an-already-downloaded-git-repo"><a href="#importing-an-already-downloaded-git-repo" name="importing-an-already-downloaded-git-repo" class="anchor"><span class="octicon octicon-link"></span>Importing an already downloaded git repo</a></h4>
 <p>If you have already downloaded Jalview using <code>git clone</code> then you can import this folder into Eclipse directly.</p>
-<p>It is important to import Jalview as a Gradle project (not as a Java project), so go to</p>
+<p>It is important to import
+Jalview as a Gradle project (not as a Java project), so go to</p>
 <p>File-&gt;Import...</p>
 <p>find and select</p>
 <p>Gradle-&gt;Existing Gradle Project</p>
 <p>and then click on the <em>Next</em> button.</p>
-<p>In the following options, it is the <strong>Project Root Directory</strong> you should set to be the <code>jalview</code> folder that git downloaded. Then you can click on the <em>Finish</em> button.</p>
-<h4 id="using-eclipse-ide-to-download-the-git-repo">Using Eclipse IDE to download the git repo</h4>
-<p>If you don't have git as a command line tool or would prefer to work entirely within Eclipse IDE then Eclipse's eGit plugin can set up a git repo of the jalview source. Go to</p>
+<p>In the following options, it is the <strong>Project Root Directory</strong> you should set to be the
+<code>jalview</code> folder that git downloaded.  Then you can click on the <em>Finish</em> button.</p>
+<h4 id="using-eclipse-ide-to-download-the-git-repo"><a href="#using-eclipse-ide-to-download-the-git-repo" name="using-eclipse-ide-to-download-the-git-repo" class="anchor"><span class="octicon octicon-link"></span>Using Eclipse IDE to download the git repo</a></h4>
+<p>If you don't have git as a command line tool or would prefer to work entirely within Eclipse IDE then
+Eclipse's eGit plugin can set up a git repo of the jalview source.  Go to</p>
 <p>File-&gt;Import...</p>
 <p>Find and select</p>
 <p>Git-&gt;Projects from Git</p>
 <p>and then click on the <em>Next</em> button.</p>
 <p>Then select Clone URI and click on <em>Next</em>.</p>
-<p>In the next window (Source Git Repository) you should put the <code>git clone</code> URL in the text box labelled <code>URI</code>. If you have a Jalview developer account (with a username and password for the Jalview git repository) then you should enter <code>https://source.jalview.org/git/jalview.git</code>. If you do not have a Jalview developer account then you should enter <code>http://source.jalview.org/git/jalview.git</code>. You will not be able to push any of your changes back to the Jalview git repository. However you can still pull all branches of the Jalview source code to your computer and develop the code there. &gt; You can sign up for a Jalview developer account at <a href="https://source.jalview.org/crucible/" class="uri">https://source.jalview.org/crucible/</a></p>
-<p>If you have a Jalview developer account, enter the username and password and decide if you want to use Eclipse's secure storage. If you don't have an account you can leave the Authentication section blank.</p>
-<div class="figure">
-<img src="./images/eclipse_egit_connection.png" alt="Eclipse eGit connection configuration" />
-<p class="caption">Eclipse eGit connection configuration</p>
-</div>
+<p>In the next window (Source Git Repository) you should put the <code>git clone</code> URL in the text box labelled <code>URI</code>.  If you have a Jalview developer account (with a username and password for the Jalview git repository) then you should enter
+<code>https://source.jalview.org/git/jalview.git</code>.
+If you do not have a Jalview developer account then you should enter
+<code>http://source.jalview.org/git/jalview.git</code>.
+You will not be able to push any of your changes back to the Jalview git repository. However you can still pull all branches of the Jalview source code to your computer and develop the code there.</p>
+<blockquote>
+<p>You can sign up for a Jalview developer account at <a href="https://source.jalview.org/crucible/">https://source.jalview.org/crucible/</a></p>
+</blockquote>
+<p>If you have a Jalview developer account, enter the username and password and decide if you want to use Eclipse's secure storage.  If you don't have an account you can leave the Authentication section blank.</p>
+<p><img src="./images/eclipse_egit_connection.png" alt="Eclipse eGit connection configuration" /></p>
 <p>Click on the <em>Next</em> button.</p>
-<p>The next window (Branch Selection) gives a list of the many Jalview branches, which by default will be all checked. You probably only want to download one branch (you can always download others at a later time). This is likely to be the <code>develop</code> branch so you can click on the <em>Deselect All</em> button, find the <code>develop</code> branch (the filter text helps), select that, and then click on the <em>Next</em> button.</p>
-<p>Choose a directory to your copy of the git repo in, and leave the other options as they are and click on the <em>Next</em> button. The next stage may take a minute or two as it checks out the selected branch(es) from the Jalview git repository.</p>
-<p>When it has finished it is important to select <strong>Import as general project</strong> and then click on <em>Next</em>. &gt; Ideally there would be an <em>Import as gradle project</em> here but there isn't -- we'll sort that out later.</p>
-<div class="figure">
-<img src="./images/eclipse_egit_import.png" alt="Eclipse eGit import choice" />
-<p class="caption">Eclipse eGit import choice</p>
-</div>
+<p>The next window (Branch Selection) gives a list of the many Jalview branches, which by default will be all checked.  You probably only want to download one branch (you can always download others at a later time).  This is likely to be the <code>develop</code> branch so you can click on the <em>Deselect All</em> button, find the <code>develop</code> branch (the filter text helps), select that, and then click on the <em>Next</em> button.</p>
+<p>Choose a directory to your copy of the git repo in, and leave the other options as they are and click on the <em>Next</em> button.  The next stage may take a minute or two as it checks out the selected branch(es) from the Jalview git repository.</p>
+<p>When it has finished it is important to select <strong>Import as general project</strong> and then click on <em>Next</em>.</p>
+<blockquote>
+<p>Ideally there would be an <em>Import as gradle project</em> here but there isn't -- we'll sort that out later.</p>
+</blockquote>
+<p><img src="./images/eclipse_egit_import.png" alt="Eclipse eGit import choice" /></p>
 <p>Click on the <em>Next</em> button.</p>
-<p>You can change the project name here. By default it will show as <strong>jalview</strong> which is fine unless you have another instance of the a Jalview project also called jalview, in which case you could change this project's name now to avoid a conflict within Eclipse.</p>
+<p>You can change the project name here.  By default it will show as <strong>jalview</strong> which is fine unless you have another instance of the a Jalview project also called jalview, in which case you could change this project's name now to avoid a conflict within Eclipse.</p>
 <p>Click on <em>Finish</em>!</p>
 <p>However, we haven't finished...</p>
-<p>You should now see, and be able to expand, the jalview project in the Project Explorer. We need to tell eclipse that this is a Gradle project, which will then allow the Eclipse Buildship plugin to automatically configure almost everything else!</p>
+<p>You should now see, and be able to expand, the jalview project in the Project Explorer.  We need to tell eclipse that this is a Gradle project, which will then allow the Eclipse Buildship plugin to automatically configure almost everything else!</p>
 <p>Right click on the project name (jalview) in the Project Explorer and find Configure towards the bottom of this long context menu, then choose Add Gradle Nature.</p>
-<div class="figure">
-<img src="./images/eclipse_add_gradle_nature.png" alt="Eclipse Add Gradle Nature" />
-<p class="caption">Eclipse Add Gradle Nature</p>
-</div>
+<p><img src="./images/eclipse_add_gradle_nature.png" alt="Eclipse Add Gradle Nature" /></p>
 <p>The project should now reconfigure itself using the <code>build.gradle</code> file to dynamically set various aspects of the project including classpath.</p>
-<h4 id="additional-views">Additional views</h4>
-<p>Some views that are automatically added when Importing a Gradle Project are not added when simply Adding a Gradle Nature, but we can add these manually by clicking on Window-&gt;Show View-&gt;Console and Window-&gt;Show View-&gt;Other... Filter with the word &quot;gradle&quot; and choose both <strong>Gradle Executions</strong> and <strong>Gradle Tasks</strong> and then click on the <em>Open</em> button.</p>
-<p>Okay, ready to code! Use of Eclipse is beyond the scope of this document, but you can find more information about developing jalview and our developer workflow in the google doc <a href="https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing" class="uri">https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing</a></p>
+<h4 id="additional-views"><a href="#additional-views" name="additional-views" class="anchor"><span class="octicon octicon-link"></span>Additional views</a></h4>
+<p>Some views that are automatically added when Importing a Gradle Project are not added when simply Adding a Gradle Nature, but we can add these manually by clicking on
+Window-&gt;Show View-&gt;Console
+and
+Window-&gt;Show View-&gt;Other...
+Filter with the word &quot;gradle&quot; and choose both <strong>Gradle Executions</strong> and <strong>Gradle Tasks</strong> and then click on the <em>Open</em> button.</p>
+<p>Okay, ready to code!  Use of Eclipse is beyond the scope of this document, but you can find more information about developing jalview and our developer workflow in the google doc <a href="https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing">https://docs.google.com/document/d/1lZo_pZRkazDBJGNachXr6qCVlw8ByuMYG6e9SZlPUlQ/edit?usp=sharing</a></p>
 <hr />
 <p><a href="mailto:help@jalview.org">Jalview Development Team</a></p>
-</body>
+
+  </body>
 </html>
index 398181a..06e796d 100644 (file)
@@ -165,6 +165,7 @@ Within the `jalview` folder you will find (of possible interest):
  `utils/install4j/`  | files used by the packaging tool, install4j
  `build.gradle`      | the build file used by gradle
  `gradle.properties` | configurable properties for the build process
+ `RELEASE`          | propertyfile configuring JALVIEW_VERSION (from jalview.version) and the release branch (from jalview.release). An alternative file can be specified via JALVIEW_RELEASE_FILE property
 
 Note that you need a Java 11 JDK to compile Jalview whether your target build is Java 1.8 or Java 11.
 
@@ -422,7 +423,7 @@ There are several values of `CHANNEL` that can be chosen, with a default of `LOC
       Note that bamboo_planKey should be set by the build plan with `-Pbamboo_planKey=${bamboo.planKey}`
     - application subdir as `alt`
     - Getdown launcher cannot use a `file://` scheme appbase. 
-* `DEVELOP`: This is for creating a `develop` appbase channel on the main web server.  This won't become live until the actual getdown artefact is synced to the web server.
+* `DEVELOP`: This is for creating a `develop` appbase channel on the main web server. This won't become live until the actual getdown artefact is synced to the web server.
   It will set
     - `appbase` as `http://www.jalview.org/getdown/develop/JAVA_VERSION`
     - application subdir as `alt`
@@ -465,6 +466,13 @@ e.g.
 gradle getdown -PCHANNEL=SCRATCH-my_test_version
 ```
 
+#### JALVIEW_VERSION and the RELEASE file
+Any Jalview build will include the value of JALVIEW_VERSION in various places, including the 'About' and Jalview Desktop window title, and in filenames for the stand-alone executable jar. You can specify a custom version for a build via the JALVIEW_VERSION property, but for most situations, JALVIEW_VERSION will be automatically configured according to the value of the CHANNEL property, using the `jalview.version` property specified in the RELEASE file:
+  - `CHANNEL=RELEASE` will set version to jalview.version
+  - `CHANNEL=TEST or DEVELOP` will append '-test' or '-develop' to jalview.version
+  
+It is also possible to specify a custom location for the RELEASE file via an optional JALVIEW_RELEASE_FILE property.
+
 #### `install4jMediaTypes`
 If you are building *install4j* installers (requires *install4j* to be installed) then this property specifies a comma-separated 
 list of media types (i.e. platform specific installers) *install4j* should actually build.
@@ -555,18 +563,18 @@ Search for and install:
 as the latest released version does not install in Eclipse 2019-03.
 However, you can install a working release of TestNG for Eclipse by going to
 >
-Help->Install New Software...
+> Help->Install New Software...
 >
-and entering
+> and entering
 >
-`TestNG Release - https://dl.bintray.com/testng-team/testng-eclipse-release`
+> `TestNG Release - https://dl.bintray.com/testng-team/testng-eclipse-release`
 >
-into the *Work with* box and click on the *Add...* button.
+> into the *Work with* box and click on the *Add...* button.
 >
-Eclipse might pause for a bit with the word *Pending* in the table below at this point, but it will eventually list TestNG with 
+> Eclipse might pause for a bit with the word *Pending* in the table below at this point, but it will eventually list TestNG with 
 a selection box under the *Name* column.
 >
-Select *TestNG* and carry on through the 
+> Select *TestNG* and carry on through the 
 install process to install the TestNG plugin.
 
 After installing the plugins, check that Java 11 is set up in Eclipse as the default JRE (see section [Java 11 compliant JDK](#java-11-compliant-jdk)).
diff --git a/doc/releaseprocess.html b/doc/releaseprocess.html
new file mode 100644 (file)
index 0000000..7205420
--- /dev/null
@@ -0,0 +1,692 @@
+<html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta http-equiv="Content-Style-Type" content="text/css" />
+    <meta name="generator" content="flexmark" />
+  <title>Making a Jalview release</title>
+    <style type="text/css">code{white-space: pre;}</style>
+<style>
+@font-face {
+  font-family: octicons-link;
+  src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
+}
+
+body {
+  -webkit-text-size-adjust: 100%;
+  text-size-adjust: 100%;
+  color: #333;
+  font-family: "Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+  font-size: 16px;
+  line-height: 1.6;
+  word-wrap: break-word;
+  width: 728px;
+  max-width: 99%;
+  box-sizing: border-box;
+  padding: 30px 30px 8rem 30px;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+body a {
+  background-color: transparent;
+}
+
+body a:active,
+body a:hover {
+  outline: 0;
+}
+
+body strong {
+  font-weight: bold;
+}
+
+body h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+body img {
+  border: 0;
+}
+
+body hr {
+  box-sizing: content-box;
+  height: 0;
+}
+
+body pre {
+  overflow: auto;
+}
+
+body code,
+body kbd,
+body pre {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+
+body input {
+  color: inherit;
+  font: inherit;
+  margin: 0;
+}
+
+body html input[disabled] {
+  cursor: default;
+}
+
+body input {
+  line-height: normal;
+}
+
+body input[type="checkbox"] {
+  box-sizing: border-box;
+  padding: 0;
+}
+
+body table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+body td,
+body th {
+  padding: 0;
+}
+
+body * {
+  box-sizing: border-box;
+}
+
+body input {
+  font: 13px / 1.4 Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+}
+
+body a {
+  color: #4078c0;
+  text-decoration: none;
+}
+
+body a:hover,
+body a:active {
+  text-decoration: underline;
+}
+
+body hr {
+  height: 0;
+  margin: 15px 0;
+  overflow: hidden;
+  background: transparent;
+  border: 0;
+  border-bottom: 1px solid #ddd;
+}
+
+body hr:before {
+  display: table;
+  content: "";
+}
+
+body hr:after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+body h1,
+body h2,
+body h3,
+body h4,
+body h5,
+body h6 {
+  margin-top: 15px;
+  margin-bottom: 15px;
+  line-height: 1.1;
+}
+
+body h1 {
+  font-size: 30px;
+}
+
+body h2 {
+  font-size: 21px;
+}
+
+body h3 {
+  font-size: 16px;
+}
+
+body h4 {
+  font-size: 14px;
+}
+
+body h5 {
+  font-size: 12px;
+}
+
+body h6 {
+  font-size: 11px;
+}
+
+body blockquote {
+  margin: 0;
+}
+
+body ul,
+body ol {
+  padding: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+body ol ol,
+body ul ol {
+  list-style-type: lower-roman;
+}
+
+body ul ul ol,
+body ul ol ol,
+body ol ul ol,
+body ol ol ol {
+  list-style-type: lower-alpha;
+}
+
+body dd {
+  margin-left: 0;
+}
+
+body code {
+  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font-size: 12px;
+}
+
+body pre {
+  margin-top: 0;
+  margin-bottom: 0;
+  font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+}
+
+body .select::-ms-expand {
+  opacity: 0;
+}
+
+body .octicon {
+  font: normal normal normal 16px/1 octicons-link;
+  display: inline-block;
+  text-decoration: none;
+  text-rendering: auto;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+
+body .octicon-link:before {
+  content: '\f05c';
+}
+
+body:before {
+  display: table;
+  content: "";
+}
+
+body:after {
+  display: table;
+  clear: both;
+  content: "";
+}
+
+body>*:first-child {
+  margin-top: 0 !important;
+}
+
+body>*:last-child {
+  margin-bottom: 0 !important;
+}
+
+body a:not([href]) {
+  color: inherit;
+  text-decoration: none;
+}
+
+body .anchor {
+  display: inline-block;
+  padding-right: 2px;
+  margin-left: -18px;
+}
+
+body .anchor:focus {
+  outline: none;
+}
+
+body h1,
+body h2,
+body h3,
+body h4,
+body h5,
+body h6 {
+  margin-top: 1em;
+  margin-bottom: 16px;
+  font-weight: bold;
+  line-height: 1.4;
+}
+
+body h1 .octicon-link,
+body h2 .octicon-link,
+body h3 .octicon-link,
+body h4 .octicon-link,
+body h5 .octicon-link,
+body h6 .octicon-link {
+  color: #000;
+  vertical-align: middle;
+  visibility: hidden;
+}
+
+body h1:hover .anchor,
+body h2:hover .anchor,
+body h3:hover .anchor,
+body h4:hover .anchor,
+body h5:hover .anchor,
+body h6:hover .anchor {
+  text-decoration: none;
+}
+
+body h1:hover .anchor .octicon-link,
+body h2:hover .anchor .octicon-link,
+body h3:hover .anchor .octicon-link,
+body h4:hover .anchor .octicon-link,
+body h5:hover .anchor .octicon-link,
+body h6:hover .anchor .octicon-link {
+  visibility: visible;
+}
+
+body h1 {
+  padding-bottom: 0.3em;
+  font-size: 1.75em;
+  line-height: 1.2;
+}
+
+body h1 .anchor {
+  line-height: 1;
+}
+
+body h2 {
+  padding-bottom: 0.3em;
+  font-size: 1.5em;
+  line-height: 1.225;
+}
+
+body h2 .anchor {
+  line-height: 1;
+}
+
+body h3 {
+  font-size: 1.25em;
+  line-height: 1.43;
+}
+
+body h3 .anchor {
+  line-height: 1.2;
+}
+
+body h4 {
+  font-size: 1em;
+}
+
+body h4 .anchor {
+  line-height: 1.2;
+}
+
+body h5 {
+  font-size: 1em;
+}
+
+body h5 .anchor {
+  line-height: 1.1;
+}
+
+body h6 {
+  font-size: 1em;
+  color: #777;
+}
+
+body h6 .anchor {
+  line-height: 1.1;
+}
+
+body p,
+body blockquote,
+body ul,
+body ol,
+body dl,
+body table,
+body pre {
+  margin-top: 0;
+  margin-bottom: 16px;
+}
+
+body hr {
+  height: 4px;
+  padding: 0;
+  margin: 16px 0;
+  background-color: #e7e7e7;
+  border: 0 none;
+}
+
+body ul,
+body ol {
+  padding-left: 2em;
+}
+
+body ul ul,
+body ul ol,
+body ol ol,
+body ol ul {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+body li>p {
+  margin-top: 16px;
+}
+
+body dl {
+  padding: 0;
+}
+
+body dl dt {
+  padding: 0;
+  margin-top: 16px;
+  font-size: 1em;
+  font-style: italic;
+  font-weight: bold;
+}
+
+body dl dd {
+  padding: 0 16px;
+  margin-bottom: 16px;
+}
+
+body blockquote {
+  padding: 0 15px;
+  color: #777;
+  border-left: 4px solid #ddd;
+}
+
+body blockquote>:first-child {
+  margin-top: 0;
+}
+
+body blockquote>:last-child {
+  margin-bottom: 0;
+}
+
+body table {
+  display: block;
+  width: 100%;
+  overflow: auto;
+  word-break: normal;
+  word-break: keep-all;
+}
+
+body table th {
+  font-weight: bold;
+}
+
+body table th,
+body table td {
+  padding: 6px 13px;
+  border: 1px solid #ddd;
+}
+
+body table tr {
+  background-color: #fff;
+  border-top: 1px solid #ccc;
+}
+
+body table tr:nth-child(2n) {
+  background-color: #f8f8f8;
+}
+
+body img {
+  max-width: 100%;
+  box-sizing: content-box;
+  background-color: #fff;
+}
+
+body code {
+  padding: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+  margin: 0;
+  font-size: 85%;
+  background-color: rgba(0,0,0,0.04);
+  border-radius: 3px;
+}
+
+body code:before,
+body code:after {
+  letter-spacing: -0.2em;
+  content: "\00a0";
+}
+
+body pre>code {
+  padding: 0;
+  margin: 0;
+  font-size: 100%;
+  word-break: normal;
+  white-space: pre;
+  background: transparent;
+  border: 0;
+}
+
+body .highlight {
+  margin-bottom: 16px;
+}
+
+body .highlight pre,
+body pre {
+  padding: 16px;
+  overflow: auto;
+  font-size: 85%;
+  line-height: 1.45;
+  background-color: #f7f7f7;
+  border-radius: 3px;
+}
+
+body .highlight pre {
+  margin-bottom: 0;
+  word-break: normal;
+}
+
+body pre {
+  word-wrap: normal;
+}
+
+body pre code {
+  display: inline;
+  max-width: initial;
+  padding: 0;
+  margin: 0;
+  overflow: initial;
+  line-height: inherit;
+  word-wrap: normal;
+  background-color: transparent;
+  border: 0;
+}
+
+body pre code:before,
+body pre code:after {
+  content: normal;
+}
+
+body kbd {
+  display: inline-block;
+  padding: 3px 5px;
+  font-size: 11px;
+  line-height: 10px;
+  color: #555;
+  vertical-align: middle;
+  background-color: #fcfcfc;
+  border: solid 1px #ccc;
+  border-bottom-color: #bbb;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 #bbb;
+}
+
+body .pl-c {
+  color: #969896;
+}
+
+body .pl-c1,
+body .pl-s .pl-v {
+  color: #0086b3;
+}
+
+body .pl-e,
+body .pl-en {
+  color: #795da3;
+}
+
+body .pl-s .pl-s1,
+body .pl-smi {
+  color: #333;
+}
+
+body .pl-ent {
+  color: #63a35c;
+}
+
+body .pl-k {
+  color: #a71d5d;
+}
+
+body .pl-pds,
+body .pl-s,
+body .pl-s .pl-pse .pl-s1,
+body .pl-sr,
+body .pl-sr .pl-cce,
+body .pl-sr .pl-sra,
+body .pl-sr .pl-sre {
+  color: #183691;
+}
+
+body .pl-v {
+  color: #ed6a43;
+}
+
+body .pl-id {
+  color: #b52a1d;
+}
+
+body .pl-ii {
+  background-color: #b52a1d;
+  color: #f8f8f8;
+}
+
+body .pl-sr .pl-cce {
+  color: #63a35c;
+  font-weight: bold;
+}
+
+body .pl-ml {
+  color: #693a17;
+}
+
+body .pl-mh,
+body .pl-mh .pl-en,
+body .pl-ms {
+  color: #1d3e81;
+  font-weight: bold;
+}
+
+body .pl-mq {
+  color: #008080;
+}
+
+body .pl-mi {
+  color: #333;
+  font-style: italic;
+}
+
+body .pl-mb {
+  color: #333;
+  font-weight: bold;
+}
+
+body .pl-md {
+  background-color: #ffecec;
+  color: #bd2c00;
+}
+
+body .pl-mi1 {
+  background-color: #eaffea;
+  color: #55a532;
+}
+
+body .pl-mdr {
+  color: #795da3;
+  font-weight: bold;
+}
+
+body .pl-mo {
+  color: #1d3e81;
+}
+
+body kbd {
+  display: inline-block;
+  padding: 3px 5px;
+  font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  line-height: 10px;
+  color: #555;
+  vertical-align: middle;
+  background-color: #fcfcfc;
+  border: solid 1px #ccc;
+  border-bottom-color: #bbb;
+  border-radius: 3px;
+  box-shadow: inset 0 -1px 0 #bbb;
+}
+
+body .task-list-item {
+  list-style-type: none;
+}
+
+body .task-list-item+.task-list-item {
+  margin-top: 3px;
+}
+
+body .task-list-item input {
+  margin: 0 0.35em 0.25em -1.6em;
+  vertical-align: middle;
+}
+
+body :checked+.radio-label {
+  z-index: 1;
+  position: relative;
+  border-color: #4078c0;
+}
+</style>
+</head>
+  <body>
+<ul>
+<li><a href="#objectives">Objectives</a></li>
+<li><a href="#tldr">tl;dr</a></li>
+</ul>
+<h1 id="making-a-jalview-release"><a href="#making-a-jalview-release" name="making-a-jalview-release" class="anchor"><span class="octicon octicon-link"></span>Making a Jalview release</a></h1>
+<h2 id="objectives"><a href="#objectives" name="objectives" class="anchor"><span class="octicon octicon-link"></span>Objectives</a></h2>
+<p>1 Update the release getdown channel
+2 Update single executable jar
+3 Update installer
+4 Update web site with release notes
+5 Update Conda recipe
+6 Update Homebrew
+7 Update JalviewJS</p>
+<h2 id="tldr"><a href="#tldr" name="tldr" class="anchor"><span class="octicon octicon-link"></span>tl;dr</a></h2>
+<p><code>OSX_SIGNING=yes KEYPASS=MYSECRETKEY gradle -POSX_KEYPASS=true -POSX_KEYSTORE=/Users/jbb/buildtools/private/sourceofpain.p12 -PJSIGN_SH=&quot;/Users/jbb/buildtools/jsign.sh&quot; -Pinstall4j_verbose=true -PJAVA_VERSION=1.8 -PCHANNEL=RELEASE -PVERSION=W.X.Y.Z getdown installers shadowJar</code>
+<code>codesign -s &quot;MY Apple Dev Name&quot; build/install4j/1.8/Jalview-DEVELOPMENT-macos-java_8.dmg</code></p>
+
+  </body>
+</html>
diff --git a/examples/groovy/ComputePeptideVariants.groovy b/examples/groovy/ComputePeptideVariants.groovy
new file mode 100644 (file)
index 0000000..6caa69c
--- /dev/null
@@ -0,0 +1,32 @@
+import jalview.datamodel.SequenceFeature
+import jalview.gui.Desktop
+def af = jalview.bin.Jalview.currentAlignFrame
+def av = af.viewport
+def fr = Desktop.getAlignFrameFor(av.codingComplement).getFeatureRenderer()
+def counts = 0
+def countm = 0
+for (seq in av.alignment.sequences) 
+{
+   ds = seq.datasetSequence
+   for (res = ds.start ; res <= ds.end; res++) 
+   {
+     mf = fr.findComplementFeaturesAtResidue(seq, res)
+     if (mf != null)
+     {
+         for (feature in mf.features)
+         {
+           variant = mf.findProteinVariants(feature)
+           if (!"".equals(variant))
+           {
+               type = variant.contains("=") ? "synonymous_variant" : "missense_variant"
+               if (type.equals("synonymous_variant")) counts++ else countm++;
+               sf = new SequenceFeature(type, variant, res, res, null)
+               seq.addSequenceFeature(sf)
+           }
+         }
+     }
+   }
+}
+af.getFeatureRenderer().featuresAdded()
+af.alignPanel.paintAlignment(true, true)
+println "Added " + countm + " missense and " + counts + " synonymous variants"
\ No newline at end of file
index 59699e3..b99cdca 100644 (file)
@@ -1,5 +1,6 @@
-org.gradle.jvmargs=-Xmx1536m -Xms512m -Dfile.encoding=UTF-8
-#systemProp.file.encoding=UTF-8
+# Convention for properties.  Read from gradle.properties, use lower_case_underlines for property names.
+# For properties set within build.gradle, use camelCaseNoSpace.
+#
 
 jalviewDir = .
 
@@ -24,38 +25,35 @@ proxyHost = sqid
 jalview_keyalg = SHA1withRSA
 jalview_keydig = SHA1
 
-testngGroups = Functional
-testngExcludedGroups = 
+testng_groups = Functional
+testng_excluded_groups = 
 
 j8libDir = j8lib
 j11libDir = j11lib
 resource_dir = resources
 help_parent_dir = help
 help_dir = help
-docDir = doc
-schemaDir = schemas
+doc_dir = doc
 classes_dir = classes
-examplesDir = examples
 clover = false
-use_clover = false
-cloverReportJVMHeap = 2g
-cloverReportJVMArgs = -Dfile.encoding=UTF-8
-cloverReportHTMLOptions = 
-cloverReportXMLOptions =
-cloverClassesDir = clover-classes
-cloverSourcesInstrDir = sources-instr
-packageDir = dist
+clover_classes_dir = clover-classes
+clover_sources_instr_dir = clover-instr
+clover_report_dir = clover-report
+clover_lib_dir = utils/clover/lib
+cloverreport_mem = 2g
+cloverreport_jvmargs = -Dfile.encoding=UTF-8
+cloverreport_html_options = 
+cloverreport_xml_options =
+package_dir = dist
 ARCHIVEDIR =
-outputJar = jalview.jar
 
-testOutputDir = tests
-utilsDir = utils
+test_output_dir = tests
+utils_dir = utils
 
 build_properties_file = .build_properties
 application_codebase = *.jalview.org
-mainClass = jalview.bin.Jalview
-shadowJarMainClass = jalview.bin.Launcher
-launcherClass = jalview.bin.Jalview
+main_class = jalview.bin.Jalview
+shadow_jar_main_class = jalview.bin.Launcher
 
 jalview_name = Jalview
 
@@ -112,6 +110,8 @@ j11libDir = j11lib
 j11modDir = j11mod
 j11modules = com.sun.istack.runtime,com.sun.xml.bind,com.sun.xml.fastinfoset,com.sun.xml.streambuffer,com.sun.xml.txw2,com.sun.xml.ws.policy,java.activation,java.annotation,java.base,java.compiler,java.datatransfer,java.desktop,java.logging,java.management,java.management.rmi,java.naming,java.prefs,java.rmi,java.scripting,java.security.sasl,java.sql,java.xml,java.xml.bind,java.xml.soap,java.xml.ws,javax.jws,jdk.httpserver,jdk.jsobject,jdk.unsupported,jdk.xml.dom,org.jvnet.mimepull,org.jvnet.staxex,javax.servlet.api,java.ws.rs
 
+flexmark_css = utils/doc/github.css
+
 install4j_home_dir = ~/buildtools/install4j8
 install4j_copyright_message = ...
 install4j_bundle_id = org.jalview.jalview-desktop
@@ -130,9 +130,6 @@ OSX_KEYSTORE =
 OSX_KEYPASS =
 JSIGN_SH = echo
 
-pandoc_exec = /usr/local/bin/pandoc,/usr/bin/pandoc
-dev = false
-
 CHANNEL=LOCAL
 getdown_channel_base = https://www.jalview.org/getdown
 getdown_app_dir_release = release
@@ -187,7 +184,6 @@ jalviewjs_core_key = core
 jalviewjs_ignore_transpile_errors = true
 
 j2s.compiler.status = enable
-j2s.compiler.java.version = 11
 #j2s.site.directory = null ## site defined from buildDir+'/jalviewjs/'+jalviewjs_site_dir
 #j2s.log.methods.declared = j2s_methods_declared.log
 #j2s.log.methods.called = j2s_methods_called.log
index aecd1a4..99d010d 100755 (executable)
@@ -47,6 +47,7 @@
    <mapID target="seqfeatures" url="html/features/seqfeatures.html"/>
    <mapID target="seqfeatedit" url="html/features/editingFeatures.html"/>
    <mapID target="seqfeatcreat" url="html/features/creatinFeatures.html"/>
+   <mapID target="seqfeatures.report" url="html/features/seqfeaturereport.html"/>
    <mapID target="seqfeatures.settings" url="html/features/featuresettings.html"/>
    <mapID target="seqfeatures.settings.selcols" url="html/features/featuresettings.html#selectbyfeature"/>
    <mapID target="viewingpdbs" url="html/features/viewingpdbs.html"/>
index 5802ddb..a0c7fe6 100755 (executable)
@@ -63,6 +63,7 @@
                        <tocitem text="Feature Colourschemes" target="features.featureschemes" />
                        <tocitem text="User Defined Sequence Features" target="seqfeatcreat" />
                        <tocitem text="Editing Sequence Features" target="seqfeatedit" />
+                       <tocitem text="HTML Feature Attributes report" target="seqfeatures.report" />
                        <tocitem text="HTML annotation report" target="io.seqreport" />
                </tocitem>
                
index 60db069..0d800cf 100644 (file)
           </div>
       </td>
     </tr>
+    <tr>
+      <td><div align="center">-jvmmempc=PERCENT</div></td>
+      <td><div align="left"><em>Only available with standalone executable jar or jalview.bin.Launcher.</em>
+          Limit maximum heap size (memory) to PERCENT% of total physical memory detected.
+         This defaults to 90 if total physical memory can be detected.
+         See <a href="../memory.html">Memory usage settings for Jalview</a> for more details.
+          </div>
+      </td>
+    </tr>
+    <tr>
+      <td><div align="center">-jvmmemmax=MAXMEMORY</div></td>
+      <td><div align="left"><em>Only available with standalone executable jar or jalview.bin.Launcher.</em>
+          Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m),
+         gigabytes(g) or if you're lucky enough, terabytes(t).
+         This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected.
+         See <a href="../memory.html">Memory usage settings for Jalview</a> for more details.
+          </div>
+      </td>
+    </tr>
   </table>
 </body>
 </html>
index 511ff2e..7b699eb 100755 (executable)
     for more information.
   </p>
   <p>
+    <strong>Working with variants without CSQ fields</strong>
+  </p>
+  <p>
+    <a name="computepepvariants">Jalview 2.11.1's new virtual
+      features</a> mean that peptide sequences are no longer annotated
+    directly with protein missense variants. This makes it harder to
+    filter variants when they do not already include the CSQ field. You
+    can rescue the pre-2.11.1 functionality by:
+  </p>
+  <ol>
+    <li>Download the script at
+      https://www.jalview.org/examples/groovy/ComputePeptideVariants.groovy</li>
+    <li>Executing the script via the <a href="groovy.html">Groovy
+        Console</a> on a linked CDS/Protein view to create missense and
+      synonymous peptide variant features.
+    </li>
+  </ol>
+  <p>
     <strong>Working with variants from organisms other than
       H.sapiens.</strong>
   </p>
diff --git a/help/help/html/features/seqfeaturereport.html b/help/help/html/features/seqfeaturereport.html
new file mode 100644 (file)
index 0000000..340df5c
--- /dev/null
@@ -0,0 +1,54 @@
+<html>
+<!--
+ * 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.
+ -->
+<head>
+<title>Sequence Feature Reports</title>
+</head>
+<body>
+  <p>
+    <strong>Sequence Feature Reports</strong> <br /> Sequence features
+    can carry a number of attributes. To view all the attributes for a
+    particular sequence feature, mouse over the feature and <em>right-click</em>
+    to open the <a href="../menus/popupMenu.html#featuredetails">Popup Menu</a> and
+    select the feature's entry from the <em>Feature Details</em>
+    submenu.
+  </p>
+  <img src="seqfeaturesrep.png" width="460"
+    alt="Full details for a particular Sequence Feature can be displayed as HTML in a report window" />
+  <p>
+    <em>Virtual Feature Reports</em><br /> When a sequence feature
+    report is shown for features mapped between CDS and Protein
+    sequences, the report will include both the original and mapped
+    feature's location.
+  </p>
+  <p>
+    <strong>Copying and pasting annotation to other programs</strong><br>
+    The <strong>File&rarr;Save</strong> option in the sequence
+    annotation report window allows the report to be saved as HTML,
+    which will preserve links and any other metadata. It is also
+    possible to copy and paste the text to other programs, but in some
+    cases, the HTML will not be preserved. In that case, you can toggle
+    the display of HTML source code with the <strong>Edit&rarr;Show
+      HTML Source</strong> drop down menu entry.
+  </p>
+  <em>Feature Reports were added in Jalview 2.11.</em>
+</body>
+</html>
diff --git a/help/help/html/features/seqfeaturesrep.png b/help/help/html/features/seqfeaturesrep.png
new file mode 100644 (file)
index 0000000..f5995a1
Binary files /dev/null and b/help/help/html/features/seqfeaturesrep.png differ
index 03b9ced..bb379ed 100644 (file)
       settings tabs and corresponding views showing 'Virtual Features'
       from each view overlaid on the other (created with Jalview 2.11.1.0).</em>
   </p>
-  <p>When virtual features are enabled, they are also shown on any
-    linked 3D structure views when 'Colour by Sequence' is enabled, and
+  <p>
+    When virtual features are enabled, they are also shown on any linked
+    3D structure views when 'Colour by Sequence' is enabled, and
     exported as GFF and Jalview Features files (mapped to their
-    associated virtual coordinates).</p>
+    associated virtual coordinates). Both the original and the mapped
+    locations are also included in <a href="seqfeaturereport.html">Sequence
+      Feature Reports</a>.
+  </p>
   <p>
     <strong>Operations supported in Split Frame Mode</strong>
   </p>
index 36ed33a..442c508 100755 (executable)
@@ -45,7 +45,7 @@
   <p>
     For more information, you might also want to take a look at the
     documentation section of the Jalview website (<a
-      href="http://www-test.jalview.org/about/documentation">http://www.jalview.org/about/documentation</a>).
+      href="http://www.jalview.org/about/documentation">http://www.jalview.org/about/documentation</a>).
   </p>
   <p>
     If you are using the Jalview Desktop application and are looking for
index 81190a9..0374dcb 100755 (executable)
     <li><em><font size="3">Maximum memory limit</em><br/>
       Since 2.11.1.0, Jalview's configuration includes a 'maximum memory limit':
       <pre>jalview.jvmmemmax = 32G</pre>
-      Adjusting this default (via a JVL file, above) will allow larger amounts of memory to be allocated to Jalview in connjunction with the jalview.jvmmempc setting. 
+      Adjusting this default (via a JVL file, above) will allow larger amounts (or can limit the amount) of memory to be allocated to Jalview in conjunction with the jalview.jvmmempc setting. 
+      <br/><br/>
+    </li>
+    <li><em><font size="3"><a name="jar">Command line arguments when using the executable jar (jalview-all.jar) or jalview.bin.Launcher</a></em><br/>
+      If you are using the Jalview standalone executable jar (usually named <em>jalview-all-....jar</em> with a Jalview and Java version designation) or using <em>jalview.bin.Launcher</em> to start Jalview,
+      then you can set the <em>jvmmempc</em> and <em>jvmmemmax</em> values using application command line arguments <em>-jvmmempc=PERCENT</em>
+      and <em>-jvmmemmax=MAXMEMORY</em> respectively.  <em>PERCENT</em> should be an integer between 1 and 100, and MAXMEMORY should be an amount of memory in bytes, or you can append a "k", "m", "g", or "t" to use units of kilobytes, megabytes, gigabytes or terabytes, e.g.
+      <pre>java -jar jalview-all-2.11.1.0-j1.8.jar -jvmmempc=50 -jvmmemmax=20g</pre>
+      (this example will launch Jalview with a maximum heap size of the smaller of 20GB or 50% of physical memory detected).
+      <br/>The default value for jvmmempc is 90, whilst the default value for jvmmemmax is 32g if Jalview can determine a total physical memory size of the host system, and a more cautious 8g if Jalview is unable to determine a total physical memory size.
+      <br/><br/>
     </li>
     <li><em><font size="3"><a name="jvm"/>Directly opening Jalview
           with a JVM</font></em> <br /> Launching Jalview directly with a JVM is
index 7625606..9c65e1a 100755 (executable)
   <p>
     <strong>Popup Menu</strong><br> <em>This menu is visible
       when right clicking either within a selected region on the
-      alignment or on a selected sequence name. It may not be accessible
+      alignment, on a sequence name, and also when right-clicking a sequence feature. It may not be accessible
       when in 'Cursor Mode' (toggled with the F2 key).</em><br /> <em><strong>Mac
         Users:</strong> pressing CTRL whilst clicking the mouse/track pad is the
       same as a right-click. See your system's settings to configure
       your track-pad's corners to generate right-clicks.</em>
   </p>
   <ul>
-    <li><strong>Selection</strong>
+    <li><strong>Selection<br/></strong>
+    <em>This menu is only visible when right-clicking a selected sequence or region of the alignment. </em>
       <ul>
         <li><a name="sqreport"><strong>Sequence
               Details...<br>
         according to gaps in just the current sequence)</em></li>
     <li><strong>Hide Sequences</strong><br> <em>Hides the
         currently selected sequences in this alignment view.</em><strong><br>
-    </strong></li>
+    </strong><br/>&nbsp;<br/></li>
+    
+    <li><strong><a name="featuredetails"> Feature Details</a><br /></strong>
+    <em>Each entry opens a <a
+      href="../features/seqfeaturereport.html">Sequence Feature
+        Report</a> for visible features under the mouse.<br />Only visible
+      when right-clicking a region where <a
+      href="../features/seqfeatures.html">Sequence Features</a> are
+      shown.
+    </em>
+  </li>
   </ul>
+  
 </body>
 </html>
index 0097655..c83741a 100755 (executable)
@@ -57,14 +57,31 @@ li:before {
     </tr>
     <tr>
       <td width="60" align="center" nowrap><strong><a
+          id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.1">.1</a><br />
+          <em>13/07/2020</em></strong></td>
+      <td align="left" valign="top">
+        <ul>
+        <!-- -->
+       </ul>
+      </td>
+      <td align="left" valign="top">
+        <ul>
+         <li><!-- JAL-3493 -->Escape does not clear highlights on the alignment (Since Jalview 2.10.3)</li>
+       </ul>
+      </td>
+    </tr>
+    <tr>
+      <td width="60" align="center" nowrap><strong><a
           id="Jalview.2.11.1">2.11.1</a><a id="Jalview.2.11.1.0">.0</a><br />
-          <em>9/04/2020</em></strong></td>
+          <em>22/04/2020</em></strong></td>
       <td align="left" valign="top">
         <ul>
           <li>
-            <!-- JAL-3187,JAL-3305,JAL-3304,JAL-3302 -->Map 'virtual'
-            codon features shown on protein (or vice versa) for display
-            in alignments, on structure views and for export.
+            <!-- JAL-3187,JAL-3305,JAL-3304,JAL-3302,JAL-3567 -->Map
+            'virtual' codon features shown on protein (or vice versa)
+            for display in alignments, on structure views (including
+            transfer to UCSF chimera), in feature reports and for
+            export.
           </li>
           <li>
             <!-- JAL-3121 -->Feature attributes from VCF files can be
@@ -98,6 +115,9 @@ li:before {
             <!-- JAL-3549 -->Warn if Sort by Score or Density attempted
             with no feature types visible
           </li>
+          <li>
+          <!-- JAL-3574 -->Improved support for filtering feature attributes with large integer values
+          </li>
         </ul><em>Jalview Installer</em>
            <ul>
           <li>
@@ -139,6 +159,11 @@ li:before {
             to stdout containing the consensus sequence for each
             alignment in a Jalview session
           </li>
+          <li>
+            <!-- JAL-3578 -->ComputePeptideVariants.groovy to translate
+            genomic sequence_variant annotation from CDS as
+            missense_variant or synonymous_variant on protein products.
+          </li>
         </ul>
       </td>
       <td align="left" valign="top">
@@ -148,6 +173,11 @@ li:before {
             'Show hidden markers' option is not ticked
           </li>
           <li>
+            <!-- JAL-247 -->Hidden sequence markers not shown in EPS and
+            PNG output when 'Automatically set ID width' is set in
+            jalview preferences or properties file
+          </li>
+          <li>
             <!-- JAL-3571 -->Feature Editor dialog can be opened when
             'Show Sequence Features' option is not ticked
           </li>
@@ -201,6 +231,11 @@ li:before {
             <!-- JAL-3406 -->Credits missing some authors in Jalview
             help documentation for 2.11.0 release
           </li>
+          <li>
+            <!-- JAL-3529 -->Export of Pfam alignment as Stockholm
+            includes Pfam ID as sequence's accession rather than its
+            Uniprot Accession
+          </li>
         </ul> <em>Java 11 Compatibility issues</em>
         <ul>
           <li>
index 5cec67d..52cddb2 100644 (file)
@@ -32,7 +32,7 @@
       Function, and Genetics</em> 43(2): 227-241. <a
       href="http://www.ncbi.nlm.nih.gov/pubmed/12112692">PubMed</a>
     or available on the <a
-      href="http://valdarlab.unc.edu/publications.html">Valdar
+      href="http://valdarlab.unc.edu/publications">Valdar
       Group publications page</a>), but the SMERFs score was developed later
     and described by Manning et al. in 2008 (<a
       href="http://www.biomedcentral.com/1471-2105/9/51">BMC
index d627c66..472bda0 100644 (file)
@@ -58,7 +58,7 @@
     <ul>
       <li><a href="http://www.clustal.org/omega">Clustal Omega</a> (version 1.2.4)</li>
       <li><a href="http://www.clustal.org/clustal2">ClustalW</a> (version 2.1)</li>
-      <li><a href="http://align.bmr.kyushu-u.ac.jp/mafft/software/">Mafft</a> (version 7.310)</li>
+      <li><a href="https://mafft.cbrc.jp/alignment/software/index.html">Mafft</a> (version 7.310)</li>
       <li><a href="http://www.drive5.com/muscle">Muscle</a> (version 3.8.31)</li>
       <li><a href="http://www.tcoffee.org/Projects_home_page/t_coffee_home_page.html">T-coffee</a> (version 11.00.8cbe486)</li>
       <li><a href="http://probcons.stanford.edu/">Probcons</a> (version 1.12)</li>
index 00f9466..0f0c7f1 100755 (executable)
       <a href="features/splitView.html#virtualfeats">Sequence
         Features dialog</a>. This allows more analyses of nucleotide and
       peptide sequence features on alignments in a more flexible and
-      memory efficient way than in earlier versions.</li>
+      memory efficient way than in earlier versions.<br />
+    <em>Note: Virtual features work best when variants are
+        annotated with CSQ fields. Please <a
+        href="features/importvcf.html#computepepvariants">see this
+          Groovy script workaround</a> if you are working with VCF files
+        without CSQ fields.
+    </em></li>
     <li><strong>Improved VCF data import</strong><br /> <a
       href="features/importvcf.html#attribs">Standard attributes for
         filtering variants</a> (e.g. position, QUAL field etc) are now
diff --git a/j11lib/Jmol-14.29.17-no_netscape.jar b/j11lib/Jmol-14.29.17-no_netscape.jar
deleted file mode 100644 (file)
index 5e7d573..0000000
Binary files a/j11lib/Jmol-14.29.17-no_netscape.jar and /dev/null differ
diff --git a/j11lib/Jmol-15.1.3.jar b/j11lib/Jmol-15.1.3.jar
new file mode 100644 (file)
index 0000000..1672e67
Binary files /dev/null and b/j11lib/Jmol-15.1.3.jar differ
diff --git a/j11lib/htsjdk-2.12.0.jar b/j11lib/htsjdk-2.12.0.jar
deleted file mode 100644 (file)
index 1df12b2..0000000
Binary files a/j11lib/htsjdk-2.12.0.jar and /dev/null differ
diff --git a/j11lib/htsjdk-2.23.0.jar b/j11lib/htsjdk-2.23.0.jar
new file mode 100644 (file)
index 0000000..bcf201f
Binary files /dev/null and b/j11lib/htsjdk-2.23.0.jar differ
diff --git a/j11lib/intervalstore-v1.0.jar b/j11lib/intervalstore-v1.0.jar
deleted file mode 100644 (file)
index 4f6101c..0000000
Binary files a/j11lib/intervalstore-v1.0.jar and /dev/null differ
diff --git a/j11lib/intervalstore-v1.1.jar b/j11lib/intervalstore-v1.1.jar
new file mode 100644 (file)
index 0000000..668b543
Binary files /dev/null and b/j11lib/intervalstore-v1.1.jar differ
diff --git a/j8lib/htsjdk-2.12.0.jar b/j8lib/htsjdk-2.12.0.jar
deleted file mode 100644 (file)
index 1df12b2..0000000
Binary files a/j8lib/htsjdk-2.12.0.jar and /dev/null differ
diff --git a/j8lib/htsjdk-2.23.0.jar b/j8lib/htsjdk-2.23.0.jar
new file mode 100644 (file)
index 0000000..bcf201f
Binary files /dev/null and b/j8lib/htsjdk-2.23.0.jar differ
diff --git a/j8lib/intervalstore-v1.0.jar b/j8lib/intervalstore-v1.0.jar
deleted file mode 100644 (file)
index 4f6101c..0000000
Binary files a/j8lib/intervalstore-v1.0.jar and /dev/null differ
diff --git a/j8lib/intervalstore-v1.1.jar b/j8lib/intervalstore-v1.1.jar
new file mode 100644 (file)
index 0000000..668b543
Binary files /dev/null and b/j8lib/intervalstore-v1.1.jar differ
index 1762a06..308876b 100644 (file)
@@ -268,11 +268,11 @@ label.autoadd_secstr = Add secondary structure annotation to alignment
 label.autoadd_temp = Add Temperature Factor annotation to alignment
 label.structure_viewer = Default structure viewer
 label.double_click_to_browse = Double-click to browse for file
-label.chimera_path = Path to Chimera program
-label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
-label.invalid_chimera_path = Chimera path not found or not executable
-label.chimera_missing = Chimera structure viewer not found.<br/>Please enter the path to Chimera (if installed),<br/>or download and install UCSF Chimera.
-label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
+label.viewer_path = Path to {0} program
+label.viewer_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
+label.invalid_viewer_path = Path not found or not executable
+label.viewer_missing = Structure viewer not found.<br/>Please enter the path to the executable (if installed),<br/>or download and install the program.
+label.open_viewer_failed = Error opening {0} - is it installed?\nCheck path in Preferences, Structure
 label.min_colour = Minimum Colour
 label.max_colour = Maximum Colour
 label.no_colour = No Colour
@@ -354,12 +354,6 @@ label.status = Status
 label.channels = Channels
 label.channel_title_item_count = {0} ({1})
 label.blog_item_published_on_date = {0} {1} 
-label.session_update = Session Update
-label.new_vamsas_session = New Vamsas Session
-action.load_vamsas_session = Load Vamsas Session...
-action.save_vamsas_session = Save Vamsas Session
-label.select_vamsas_session_opened_as_new_vamsas_session= Select a vamsas session to be opened as a new vamsas session.
-label.open_saved_vamsas_session = Open a saved VAMSAS session
 label.groovy_console = Groovy Console...
 label.lineart = Lineart
 label.dont_ask_me_again = Don't ask me again
@@ -411,13 +405,10 @@ label.couldnt_read_pasted_text = Couldn't read the pasted text {0}
 label.error_parsing_text = Error parsing text
 label.input_alignment_from_url = Input Alignment From URL
 label.input_alignment = Input Alignment
-label.couldnt_import_as_vamsas_session = Couldn't import {0} as a new vamsas session.
 label.vamsas_document_import_failed = Vamsas Document Import Failed
 label.couldnt_locate = Couldn''t locate {0}
 label.url_not_found = URL not found
 label.new_sequence_url_link = New sequence URL link
-label.cannot_edit_annotations_in_wrapped_view = Cannot edit annotations in wrapped view
-label.wrapped_view_no_edit = Wrapped view - no edit
 label.error_retrieving_data = Error Retrieving Data
 label.user_colour_scheme_must_have_name = User colour scheme must have a name
 label.no_name_colour_scheme = No name for colour scheme
@@ -502,10 +493,9 @@ label.insert_gaps = Insert {0} gaps
 label.delete_gap = Delete 1 gap
 label.delete_gaps = Delete {0} gaps
 label.sequence_details = Sequence Details
-label.jmol_help = Jmol Help
-label.chimera_help = Chimera Help
+label.viewer_help = {0} Help
 label.close_viewer = Close Viewer
-label.confirm_close_chimera = This will close Jalview''s connection to {0}.<br>Do you want to close the Chimera window as well?
+label.confirm_close_viewer = This will close Jalview''s connection to {0}.<br>Do you want to close the {1} window as well?
 label.all = All
 label.sort_by = Sort alignment by
 label.sort_by_score = Sort by Score
@@ -523,7 +513,6 @@ label.standard_databases = Standard Databases
 label.fetch_embl_uniprot = Fetch from EMBL/EMBLCDS or Uniprot/PDB and any selected DAS sources
 label.reset_min_max_colours_to_defaults = Reset min and max colours to defaults from user preferences.
 label.align_structures_using_linked_alignment_views = Superpose structures using {0} selected alignment view(s)
-label.connect_to_session = Connect to session {0}
 label.threshold_feature_display_by_score = Threshold the feature display by score.
 label.threshold_feature_no_threshold = No Threshold
 label.threshold_feature_above_threshold = Above Threshold
@@ -557,9 +546,6 @@ label.right_align_sequence_id = Right Align Sequence Id
 label.sequence_id_tooltip = Sequence ID Tooltip
 label.no_services = <No Services>
 label.select_copy_raw_html = Select this if you want to copy raw html
-label.share_data_vamsas_applications = Share data with other vamsas applications
-label.connect_to = Connect to
-label.join_existing_vamsas_session = Join an existing vamsas session
 label.from_url = from URL
 label.any_trees_calculated_or_loaded_alignment_automatically_sort = When selected, any trees calculated or loaded onto the alignment will automatically sort the alignment
 label.sort_with_new_tree = Sort With New Tree
@@ -569,7 +555,6 @@ label.preferences = Preferences
 label.tools = Tools
 label.fetch_sequences = Fetch Sequences
 action.fetch_sequences = Fetch Sequences...
-label.stop_vamsas_session = Stop Vamsas Session
 label.collect_garbage = Collect Garbage
 label.show_memory_usage = Show Memory Usage
 label.show_java_console = Show Java Console
@@ -624,7 +609,6 @@ label.editing = Editing
 label.web_services = Web Services
 label.right_click_to_edit_currently_selected_parameter = Right click to edit currently selected parameter.
 label.let_jmol_manage_structure_colours = Let Jmol manage structure colours
-label.let_chimera_manage_structure_colours = Let Chimera manage structure colours
 label.fetch_chimera_attributes = Fetch Chimera attributes
 label.fetch_chimera_attributes_tip = Copy Chimera attribute to Jalview feature
 label.marks_leaves_tree_not_associated_with_sequence = Marks leaves of tree not associated with a sequence
@@ -711,14 +695,13 @@ label.associate_nodes_with = Associate Nodes With
 label.link_name = Link Name
 label.pdb_file = PDB file
 label.colour_with_jmol = Colour with Jmol
-label.colour_with_chimera = Colour with Chimera
+label.let_viewer_manage_structure_colours = Let viewer manage structure colours
+label.colour_with_viewer = Colour in structure viewer
 label.superpose_structures = Superpose Structures
 error.superposition_failed = Superposition failed: {0}
 label.insufficient_residues = Not enough aligned residues ({0}) to perform superposition
-label.jmol = Jmol
-label.chimera = Chimera
-label.create_chimera_attributes = Write Jalview features
-label.create_chimera_attributes_tip = Set Chimera residue attributes for visible features
+label.create_viewer_attributes = Write Jalview features
+label.create_viewer_attributes_tip = Set structure residue attributes for Jalview features
 label.attributes_set = {0} attribute values set on Chimera
 label.sort_alignment_by_tree = Sort Alignment By Tree
 label.mark_unlinked_leaves = Mark Unlinked Leaves
@@ -877,9 +860,6 @@ label.save_text_to_file = Save Text to File
 label.save_state = Save State
 label.restore_state = Restore State
 label.saving_jalview_project = Saving jalview project {0}
-label.loading_jalview_project = Loading jalview project {0}
-label.save_vamsas_document_archive = Save Vamsas Document Archive
-label.saving_vamsas_doc = Saving VAMSAS Document to {0}
 label.load_feature_colours = Load Feature Colours
 label.save_feature_colours = Save Feature Colour Scheme
 label.select_startup_file = Select startup file
@@ -899,7 +879,6 @@ label.visible = Visible
 label.select_unselect_visible_regions_from = select and unselected {0} regions from {1}
 label.visible_region_of = visible region of
 label.webservice_job_title_on = {0} using {1} on {2}
-label.updating_vamsas_session = Updating vamsas session
 label.loading_file = Loading File: {0}
 label.edit_params = Edit {0}
 label.as_percentage = As Percentage
@@ -940,8 +919,6 @@ error.call_setprogressbar_before_registering_handler = call setProgressBar befor
 label.cancelled_params = Cancelled {0}
 error.implementation_error_cannot_show_view_alignment_frame = Implementation error: cannot show a view from another alignment in an AlignFrame.
 error.implementation_error_dont_know_about_threshold_setting = Implementation error: don't know about threshold setting for current AnnotationColourGradient.
-error.try_join_vamsas_session_another = Trying to join a vamsas session when another is already connected
-error.invalid_vamsas_session_id = Invalid vamsas session id
 label.groovy_support_failed = Jalview Groovy Support Failed
 label.couldnt_create_groovy_shell = Couldn't create the groovy Shell. Check the error log for the details of what went wrong.
 error.unsupported_version_calcIdparam = Unsupported Version for calcIdparam {0}
@@ -949,18 +926,13 @@ error.implementation_error_cant_reorder_tree = Implementation Error: Can't reord
 error.invalid_value_for_option = Invalid value {0} for option {1}
 error.implementation_error_cannot_import_vamsas_doc = Implementation Error - cannot import existing vamsas document into an existing session, Yet!
 label.vamsas_doc_couldnt_be_opened_as_new_session = VAMSAS Document could not be opened as a new session - please choose another
-error.implementation_error_vamsas_operation_not_init = Impementation error! Vamsas Operations when client not initialised and connected
-error.jalview_no_connected_vamsas_session = Jalview not connected to Vamsas session
-error.implementation_error_cannot_recover_vamsas_object_mappings = IMPLEMENTATION ERROR: Cannot recover vamsas object mappings - no backup was made
 error.setstatus_called_non_existent_job_pane = setStatus called for non-existent job pane {0}
 error.implementation_error_cannot_find_marshaller_for_param_set =Implementation error: Can't find a marshaller for the parameter set
 error.implementation_error_old_jalview_object_not_bound =IMPLEMENTATION ERROR: old jalview object is not bound ! ({0})
 error.implementation_error_vamsas_doc_class_should_bind_to_type = Implementation Error: Vamsas Document Class {0} should bind to a {1} (found a {2})
 error.invalid_vamsas_rangetype_cannot_resolve_lists = Invalid vamsas RangeType - cannot resolve both lists of Pos and Seg from choice!
-error.implementation_error_maplist_is_null = Implementation error. MapList is null for initMapType.
 error.implementation_error_cannot_have_null_alignment = Implementation error: Cannot have null alignment property key
 error.implementation_error_null_fileparse = Implementation error. Null FileParse in copy constructor
-error.implementation_error_cannot_map_alignment_sequences = IMPLEMENTATION ERROR: Cannot map an alignment of sequences from different datasets into a single alignment in the vamsas document.
 error.implementation_error_structure_selection_manager_null = Implementation error. Structure selection manager's context is 'null'
 exception.ssm_context_is_null = SSM context is null
 error.idstring_seqstrings_only_one_per_sequence = idstrings and seqstrings contain one string each per sequence
@@ -997,6 +969,7 @@ error.dbrefsource_implementation_exception =DBRefSource Implementation Exception
 error.implementation_error_dbinstance_must_implement_interface = Implmentation Error - getDbInstances must be given a class that implements jalview.ws.seqfetcher.DbSourceProxy (was given{0})
 error.implementation_error_must_init_dbsources =Implementation error. Must initialise dbSources
 label.view_controller_toggled_marked = {0} {1} columns {2} features of type {3}  across {4} sequence(s)
+label.no_highlighted_regions_marked = No highlighted regions marked
 label.toggled = Toggled
 label.marked = Marked
 label.containing = containing
@@ -1110,10 +1083,7 @@ label.png_image = PNG image
 status.export_complete = {0} Export completed
 status.fetching_pdb = Fetching PDB {0}
 status.refreshing_news = Refreshing news
-status.importing_vamsas_session_from = Importing VAMSAS session from {0}
 status.opening_params = Opening {0}
-status.waiting_sequence_database_fetchers_init = Waiting for Sequence Database Fetchers to initialise
-status.init_sequence_database_fetchers = Initialising Sequence Database Fetchers
 status.fetching_sequence_queries_from = Fetching {0} sequence queries from {1}
 status.finshed_querying = Finished querying
 status.parsing_results = Parsing results.
@@ -1124,18 +1094,14 @@ status.fetching_db_refs = Fetching db refs
 status.loading_cached_pdb_entries = Loading Cached PDB Entries
 status.searching_for_pdb_structures = Searching for PDB Structures
 status.opening_file_for = opening file for
-status.colouring_chimera = Colouring Chimera
+status.colouring_structures = Colouring structures
 label.font_doesnt_have_letters_defined = Font doesn't have letters defined\nso cannot be used\nwith alignment data
 label.font_too_small = Font size is too small
-label.error_loading_file_params = Error loading file {0}
-label.error_loading_jalview_file = Error loading Jalview file
 warn.out_of_memory_when_action = Out of memory when {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
 warn.out_of_memory_loading_file = Out of memory loading file {0}\!\!\nSee help files for increasing Java Virtual Machine memory.
 label.out_of_memory = Out of memory
 label.invalid_id_column_width = Invalid ID Column width
 warn.user_defined_width_requirements = The user defined width for the\nannotation and sequence ID columns\nin exported figures must be\nat least 12 pixels wide.
-label.couldnt_create_sequence_fetcher = Couldn't create SequenceFetcher
-warn.couldnt_create_sequence_fetcher_client = Could not create the sequence fetcher client. Check error logs for details.
 warn.server_didnt_pass_validation = Service did not pass validation.\nCheck the Jalview Console for more details.
 warn.url_must_contain = Sequence URL must contain $SEQUENCE_ID$, $DB_ACCESSION$, or a regex
 warn.urls_not_contacted = URLs that could not be contacted
@@ -1318,7 +1284,6 @@ option.enable_disable_autosearch = When ticked, search is performed automaticall
 option.autosearch = Autosearch
 label.retrieve_ids = Retrieve IDs
 label.display_settings_for = Display settings for {0} features
-label.simple = Simple
 label.simple_colour = Simple Colour
 label.colour_by_text = Colour by text
 label.graduated_colour = Graduated Colour
@@ -1355,7 +1320,6 @@ label.backup_filename_strategy = Backup filename strategy
 label.append_to_filename = Append to filename (%n is replaced by the backup number)
 label.append_to_filename_tooltip = %n in the text will be replaced by the backup number. The text will appear after the filename. See the summary box above.
 label.index_digits = Number of digits to use for the backup number (%n)
-label.summary_of_backups_scheme = Summary of backup scheme
 label.scheme_examples = Scheme examples
 label.increment_index = Increase appended text numbers - newest file has largest number.
 label.reverse_roll = "Roll" appended text numbers - newest backup file is always number 1.
@@ -1384,13 +1348,11 @@ label.single_file_description = Keep the last version of the file
 label.keep_all_versions_description = Keep all previous versions of the file
 label.rolled_backups_description = Keep the last nine versions of the file from _bak.1 (newest) to _bak.9 (oldest)
 label.cancel_changes_description = Cancel changes made to your last saved Custom scheme
-label.previously_saved_scheme = Previously saved scheme
 label.no_backup_files = NO BACKUP FILES
 label.include_backup_files = Include backup files
 label.cancel_changes = Cancel changes
 label.warning_confirm_change_reverse = Warning!\nIf you change the increment/decrement of the backup filename number, without changing the suffix or number of digits,\nthis may cause loss of backup files created with the previous backup filename scheme.\nAre you sure you wish to do this?
 label.change_increment_decrement = Change increment/decrement?
-label.was_previous = was {0}
 label.newerdelete_replacement_line = Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted and replaced by apparently older file\n''{1}''\t(modified {3}, size {5}).
 label.confirm_deletion_or_rename = Confirm deletion of ''{0}'' or rename to ''{1}''?
 label.newerdelete_line = Backup file\n''{0}''\t(modified {2}, size {4})\nis to be deleted but is newer than the oldest remaining backup file\n''{1}''\t(modified {3}, size {5}).
index d3c3355..ef40e97 100644 (file)
@@ -211,7 +211,7 @@ label.show_non_conserved = Mostrar no conservadas
 label.overview_window = Ventana resumen
 label.none = Ninguno
 label.above_identity_threshold = Por encima del umbral de identidad
-label.show_sequence_features = Mostrar las características de las secuencias
+label.show_sequence_features = Mostrar las características de secuencia
 label.nucleotide = Nucleótido
 label.to_new_alignment = A nuevo alineamiento
 label.to_this_alignment = Añadir a este alineamiento
@@ -320,11 +320,6 @@ label.status =  [Estado]
 label.channels = Canales
 label.channel_title_item_count = {0} ({1})
 label.blog_item_published_on_date = {0} {1} 
-label.session_update = Actualizar sesión
-label.new_vamsas_session = Nueva sesión Vamsas
-action.save_vamsas_session = Guardar Sesión Vamsas
-label.select_vamsas_session_opened_as_new_vamsas_session= Selecciones una sesión vamsas para abrirla como una nueva sesión.
-label.open_saved_vamsas_session = Abrir una sesión VAMSAS guardada
 label.groovy_console = Consola Groovy 
 label.lineart = Lineart
 label.dont_ask_me_again = No volver a preguntar
@@ -375,13 +370,10 @@ label.couldnt_read_pasted_text = No se pudo leer el texto pegado {0}
 label.error_parsing_text = Error analizando el texto
 label.input_alignment_from_url = Alineamiento de entrada desde URL
 label.input_alignment = Alineamiento de entrada
-label.couldnt_import_as_vamsas_session = No se pudo importar {0} como una nueva sesión Vamsas.
 label.vamsas_document_import_failed =  Fallo en la importación del documento Vamsas
 label.couldnt_locate = No se pudo localizar {0}
 label.url_not_found = URL no encontrada
 label.new_sequence_url_link = Enlace a una nueva secuencia URL
-label.cannot_edit_annotations_in_wrapped_view = No se pueden editar anotaciones en vista envolvente
-label.wrapped_view_no_edit = Vista envolvente - no editar
 label.error_retrieving_data = Error en la recuperación de datos
 label.user_colour_scheme_must_have_name = El esquema de colores del usuario debe tener un nombre
 label.no_name_colour_scheme = No hay nombre para el esquema de colores 
@@ -465,7 +457,7 @@ label.insert_gaps = Insertar {0} huecos
 label.delete_gap = Borrar 1 hueco
 label.delete_gaps = Borrar {0} huecos
 label.sequence_details = Detalles de la secuencia
-label.jmol_help = Ayuda de Jmol
+label.viewer_help = Ayuda sobre {0}
 # Todos/Todas is gender-sensitive, but currently only used for feminine (cadena / anotación)! 
 label.all = Todas
 label.sort_by = Ordenar por
@@ -481,7 +473,6 @@ label.standard_databases = Bases de datos est
 label.fetch_embl_uniprot = Recuperar de EMBL/EMBLCDS o Uniprot/PDB y de cualquier fuente DAS seleccionada
 label.reset_min_max_colours_to_defaults = Reiniciar los colores min y max colours a los valores por defecto establecidos en las preferencias de usuario
 label.align_structures_using_linked_alignment_views = Alinear las estructuras utilizando las {0} vista(s) de alineamiento enlazada(s)
-label.connect_to_session = Conectar a la sesión {0}
 label.threshold_feature_display_by_score = Filtrar la característica mostrada por puntuación.
 label.threshold_feature_no_threshold = Sin umbral
 label.threshold_feature_above_threshold = Por encima del umbral
@@ -512,9 +503,6 @@ label.right_align_sequence_id = Alinear a la derecha el ID de la secuencia
 label.sequence_id_tooltip = Ayuda del ID de la secuencia
 label.no_services = <Sin Servicios>
 label.select_copy_raw_html = Seleccione esta opción si desea copiar el html en bruto
-label.share_data_vamsas_applications = Compartir datos con otras aplicaciones vamsas
-label.connect_to = Conectar a
-label.join_existing_vamsas_session = Unirse a una sesión vamsas existente
 label.from_url = desde una URL
 label.any_trees_calculated_or_loaded_alignment_automatically_sort = Cuando está habilitado, cualquier Ã¡rbol calculado o cargado en el alineamiento lo ordenará
 label.sort_with_new_tree = Ordenar con el nuevo Ã¡rbol
@@ -523,7 +511,6 @@ label.window = Ventana
 label.preferences = Preferencias
 label.tools = Herramientas
 label.fetch_sequences = Recuperar secuencia(s)
-label.stop_vamsas_session = Parar sesión vamsas
 label.collect_garbage = Recolector de basura
 label.show_memory_usage = Mostrar uso de memoria
 label.show_java_console = Mostrar consola de Java
@@ -653,7 +640,6 @@ label.associate_nodes_with = Asociar nodos con
 label.link_name = Nombre del enalce
 label.pdb_file = Fichero PDB
 label.colour_with_jmol = Colorear con Jmol
-label.jmol = Jmol
 label.sort_alignment_by_tree = Ordenar alineamiento por Ã¡rbol
 label.mark_unlinked_leaves = Marcar las hojas como no enlazadas
 label.associate_leaves_with = Asociar hojas con
@@ -694,7 +680,12 @@ label.annotations_for_params = Anotaciones de - {0}
 label.generating_features_for_params = Generando características de - {0}
 label.generating_annotations_for_params = Generando anotaciones de - {0}
 label.varna_params = VARNA - {0}
-label.sequence_feature_settings = Configuración de las características de la secuencia
+label.sequence_feature_settings = Configuración de las características de secuencia
+label.sequence_feature_settings_for = Configuración de las características de secuencia para {0}
+label.sequence_feature_settings_for_view = Configuración de las características de secuencia para vista "{0}"
+label.sequence_feature_settings_for_CDS_and_Protein = Configuración de las características de secuencia para CDS y Proteína 
+action.undo_changes_to_feature_settings = Deshacer todos los cambios no aplicados
+action.undo_changes_to_feature_settings_and_close_the_dialog = Deshacer cambios pendientes, cerrar diálogo
 label.pairwise_aligned_sequences = Secuencias alineadas a pares
 label.original_data_for_params = Datos originales de {0}
 label.points_for_params = Puntos de {0}
@@ -796,9 +787,6 @@ label.save_text_to_file = Guardar Texto en un fichero
 label.save_state = Guardar estado
 label.restore_state = Restaurar estado
 label.saving_jalview_project = Guardando el proyecto de Jalview {0}
-label.loading_jalview_project = Cargando el proyecto de Jalview {0}
-label.save_vamsas_document_archive = Guardar el archivo de documento Vamsas
-label.saving_vamsas_doc = Guardando el documento VAMSAS en {0}
 label.load_feature_colours = Cargar colores de características
 label.save_feature_colours = Guardar esquema cromático de características
 label.select_startup_file = Seleccionar fichero de arranque
@@ -818,7 +806,6 @@ label.visible = Visible
 label.select_unselect_visible_regions_from = seleccionada y deseleccionadas {0} regiones de {1}
 label.visible_region_of = región visible de
 label.webservice_job_title_on = {0} usando {1} de {2}
-label.updating_vamsas_session = Actualizando sesión VAMSAS
 label.loading_file = Cargando fichero: {0}
 label.edit_params = Editar {0}
 label.as_percentage = Como Porcentaje
@@ -859,8 +846,6 @@ error.call_setprogressbar_before_registering_handler = llamada a setProgressBar
 label.cancelled_params = {0} cancelado
 error.implementation_error_cannot_show_view_alignment_frame = Error de implementación: no es posible mostrar una vista de otro alineamiento en un AlignFrame.
 error.implementation_error_dont_know_about_threshold_setting = Error de implementación: no se conoce la configuración del umbral para el AnnotationColourGradient actual.
-error.try_join_vamsas_session_another = Tratando de establecer una sesión VAMSAS cuando ya había otra conectada
-error.invalid_vamsas_session_id = Identificador de sesión VAMSAS no válido
 label.groovy_support_failed = El soporte Groovy de Jalview ha fallado
 label.couldnt_create_groovy_shell = No es posible crear el shell de Groovy. Compruebe el fichero de log para conocer los detalles.
 error.unsupported_version_calcIdparam = Versión no soportada de {0}
@@ -868,18 +853,13 @@ error.implementation_error_cant_reorder_tree = Error de implementaci
 error.invalid_value_for_option = Valor no válido de {0} para la opción {1}
 error.implementation_error_cannot_import_vamsas_doc = Error de implementación - todavía no es posible importar el documento VAMSAS existente en una sesión existente.
 label.vamsas_doc_couldnt_be_opened_as_new_session = El documento VAMSAS no ha podido abrirse como una nueva sesión. Por favor, escoja otra.
-error.implementation_error_vamsas_operation_not_init = Â¡Error de implementación! Operaciones VAMSAS cuando el cliente no estaba inicializado ni conectado
-error.jalview_no_connected_vamsas_session = Jalview está conectado a una sesión VAMSAS
-error.implementation_error_cannot_recover_vamsas_object_mappings = Error de implementación: no es posible recuperar los mapeos del objeto VAMSAS - no se ha hecho ningún backup 
 error.setstatus_called_non_existent_job_pane = se lllamado a setStatus para el panel de trabajo {0} no existente
 error.implementation_error_cannot_find_marshaller_for_param_set =Error de implementación: no puede encontrar un marshaller para el conjunto de parámetros
 error.implementation_error_old_jalview_object_not_bound =Error de implementación: Â¡el objeto Jalview antiguo no está enlazado! ({0})
 error.implementation_error_vamsas_doc_class_should_bind_to_type = Error de implementación: la clase de documento VAMSAS {0} debe enlazar a {1} (pero se ha encontrado que lo está a {2})
 error.invalid_vamsas_rangetype_cannot_resolve_lists = RangeType VAMSAS no válido - Â¡no es posible resolver ambas listas de Pos y Seg con los valores elegidos!
-error.implementation_error_maplist_is_null = Error de implementación. MapList es nulo en initMapType.
 error.implementation_error_cannot_have_null_alignment = Error de implementación: no es posible tener una clave nula en el alineamiento
 error.implementation_error_null_fileparse = Error de implementación. FileParse nulo en el construictor de copia
-error.implementation_error_cannot_map_alignment_sequences = Error de implementación: no es posible maper un alineamiento de secuencias desde distintos conjuntos de datos en un Ãºnico alineamiento en el documento VAMSAS.
 error.implementation_error_structure_selection_manager_null = Error de implementación. El contexto structure selection manager's es nulo
 exception.ssm_context_is_null = El contexto SSM es nulo
 error.idstring_seqstrings_only_one_per_sequence = idstrings y seqstrings contienen una cadena por cada secuencia
@@ -916,6 +896,7 @@ error.dbrefsource_implementation_exception = Excepci
 error.implementation_error_dbinstance_must_implement_interface = Error de Implementación- getDbInstances debe recibir una clase que implemente jalview.ws.seqfetcher.DbSourceProxy (recibió {0})
 error.implementation_error_must_init_dbsources =Error de implementación. Debe inicializar dbSources
 label.view_controller_toggled_marked = {0} {1} columnas {2} características del tipo {3} en {4} secuencia(s)
+label.no_highlighted_regions_marked = No hay regiones resaltadas marcadas
 label.toggled = Invertida
 label.marked = Marcada
 label.containing = conteniendo
@@ -1026,10 +1007,7 @@ label.png_image = Imagen PNG
 status.export_complete = Exportación completada
 status.fetching_pdb = Recuperando PDB {0}
 status.refreshing_news = Refrescando noticias
-status.importing_vamsas_session_from = Importando sesión VAMSAS de {0}
 status.opening_params = Abriendo {0}
-status.waiting_sequence_database_fetchers_init = Esperando inicialización de los recuperadores de bases de datos de secuencias
-status.init_sequence_database_fetchers = Inicializando recuperadores de bases de datos de secuencias
 status.fetching_sequence_queries_from = Recuperando {0} consultas de secuencias de {1}
 status.finshed_querying = Consulta finalizada
 status.parsing_results = Parseando resultados.
@@ -1039,15 +1017,11 @@ status.collecting_job_results = Recolectando los resultados de los trabajos.
 status.fetching_db_refs = Recuperando db refs
 label.font_doesnt_have_letters_defined = La fuente no tiene letras definidas\npor lo que no puede emplease\ncon datos de alineamientos
 label.font_too_small = Tamaño de la letra es demasiado pequeña
-label.error_loading_file_params = Error cargando el fichero {0}
-label.error_loading_jalview_file = Error cargando el fichero Jalview 
 warn.out_of_memory_when_action = Sin memoria al {0}\!\!\nConsulte los ficheros de ayuda para ajustar la memoria de la m\u00E1quina virtual de Java.
 warn.out_of_memory_loading_file = Sin memoria al cargar el fichero {0}\!\!\nConsulte los ficheros de ayuda para ajustar la memoria de la m\u00E1quina virtual de Java.
 label.out_of_memory = Sin memoria
 label.invalid_id_column_width = Identificador de anchura de columna no válido
 warn.user_defined_width_requirements = La anchura definida por el usuario para la \nlas columnas de anotaci\u00F3n e identificador de secuencias\nen figuras exportadas debe ser\na, al menos, de 12 p\u00EDxels
-label.couldnt_create_sequence_fetcher = No es posible crear SequenceFetcher
-warn.couldnt_create_sequence_fetcher_client = No es posible crear el cliente de recuperador de secuencias. Comprueba el fichero de log para más detalles.
 warn.server_didnt_pass_validation = El servicio no ha pasado la validaci\u00F3n.\nCompruebe la consola de Jalview para m\u00E1s detalles.
 warn.url_must_contain = La URL de la secuencia debe contener $SEQUENCE_ID$, $DB_ACCESSION$ o un regex
 info.validate_jabaws_server = \u00BFValidar el servidor JabaWS?\n(Consulte la consola de salida para obtener los resultados)
@@ -1090,7 +1064,6 @@ label.rnalifold_calculations=Predicci
 label.rnalifold_settings=Cambiar ajustes RNAAliFold...
 label.sort_ann_by=Ordenar anotaciones por
 info.enter_search_text_here=Introducir texto de búsqueda aquí
-action.load_vamsas_session=Cargar Sesión Vamsas...
 label.show_all_al_annotations=Mostrar anotaciones relacionadas con el alineamiento 
 label.hide_all_al_annotations=Ocultar anotaciones relacionadas con el alineamiento
 label.show_all_seq_annotations=Mostrar anotaciones relacionadas con las secuencias
@@ -1118,9 +1091,8 @@ label.autoadd_secstr=A
 action.annotations=Anotaciones
 label.nuc_alignment_colour=Color del Alineamiento Nucleotídico
 label.copy_format_from=Copiar formato de
-label.chimera=Chimera
-label.create_chimera_attributes = Escribir características de Jalview
-label.create_chimera_attributes_tip = Establecer atributos en Chimera para características visibles 
+label.create_viewer_attributes = Escribir características de Jalview
+label.create_viewer_attributes_tip = Establecer atributos de residuos para características visibles 
 label.attributes_set = {0} valores de atributos establecidos en Chimera
 label.open_split_window=Abrir ventana dividida
 label.open_split_window?=¿Quieres abrir ventana dividida, con cDNA y proteína vinculadas?
@@ -1140,7 +1112,7 @@ label.invalid_search=Texto de b
 action.export_annotations=Exportar Anotaciones
 action.set_as_reference=Marcar como Referencia
 action.unmark_as_reference=Desmarcar como Referencia
-label.chimera_failed=Error al abrir Chimera - está instalado?\nCompruebe ruta en Preferencias, Estructura
+label.open_viewer_failed=Error al abrir {0} - está instalado?\nCompruebe ruta en Preferencias, Estructura
 label.find=Buscar
 label.select_pdb_file=Seleccionar Fichero PDB
 label.structures_filter=Filtro de Estructuras
@@ -1153,7 +1125,6 @@ action.export_features=Exportar Caracter
 error.invalid_regex=Expresión regular inválida
 label.autoadd_temp=Añadir anotación factor de temperatura al alineamiento
 label.double_click_to_browse = Haga doble clic para buscar fichero 
-label.chimera_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación
 label.structure_chooser=Selector de Estructuras
 label.structure_chooser_manual_association=Selector de Estructuras - asociación manual
 label.threshold_filter=Filtro de Umbral
@@ -1161,11 +1132,11 @@ label.add_reference_annotations=A
 label.hide_insertions=Ocultar Inserciones
 info.change_threshold_mode_to_enable=Cambiar Modo de Umbral para Habilitar
 label.separate_multiple_query_values=Introducir uno o mas {0}s separados por punto y coma ";"
-label.let_chimera_manage_structure_colours=Deja que Chimera maneje colores de estructuras
 label.fetch_chimera_attributes = Buscar atributos desde Chimera
 label.fetch_chimera_attributes_tip = Copiar atributo de Chimera a característica de Jalview
 label.view_rna_structure=Estructura 2D VARNA
-label.colour_with_chimera=Colorear con Chimera
+label.colour_with_viewer = Colorear con visualizador de estructuras
+label.let_viewer_manage_structure_colours = Deja que el visualizador maneje los colores de estructuras
 label.superpose_structures = Superponer estructuras
 error.superposition_failed = Superposición fallido: {0}
 label.insufficient_residues = Residuos alineados ({0}) insuficentes para superponer
@@ -1176,7 +1147,6 @@ tooltip.aacon_settings=Cambiar ajustes para c
 label.mark_as_representative=Marcar como representativa
 label.include_description=Incluir Descripción
 label.for=para
-label.invalid_chimera_path=Ruta de Chimera no encontrada o no ejecutable
 info.search_in_annotation_label=Buscar en etiqueta de {0}
 info.search_in_annotation_description=Buscar en descripción de {0}
 label.select_many_views=Seleccionar múltiples vistas
@@ -1186,23 +1156,26 @@ warn.urls_not_contacted=URLs que no pudieron ser contactados
 label.prot_alignment_colour=Color del Alineamiento Proteico
 info.associate_wit_sequence=Asociar con secuencia
 label.protein=Proteína
+label.CDS=CDS
 warn.oneseq_msainput_selection=La selección actual sólo contiene una Ãºnica secuencia. Â¿Quieres enviar todas las secuencias para la alineación en su lugar?
 label.use_rnaview=Usar RNAView para estructura secondaria
 label.search_all=Introducir uno o más valores de búsqueda separados por punto y coma ";" (Nota: buscará en toda la base de datos PDB)
-label.confirm_close_chimera=Cerrará la conexión de Jalview a {0}.<br>¿Quieres cerrar la ventana Chimera también?
+label.confirm_close_viewer=Cerrará la conexión de Jalview a {0}.<br>¿Quieres cerrar la ventana {1} también?
 tooltip.rnalifold_calculations=Se calcularán predicciones de estructura secondaria de RNA para el alineaminento, y se actualizarán si se efectuan cambios
 tooltip.rnalifold_settings=Modificar la configuración de la predicción RNAAlifold. Ãšselo para ocultar o mostrar resultados del cálculo de RNA, o cambiar parámetros de el plegado de RNA.
 label.show_selected_annotations=Mostrar anotaciones seleccionadas
-status.colouring_chimera=Coloreando Chimera
+status.colouring_structures=Coloreando estructuras
 label.configure_displayed_columns=Configurar Columnas Mostradas
 label.aacon_calculations=cálculos AACon
 label.pdb_web-service_error=Error de servicio web PDB
 exception.unable_to_detect_internet_connection=Jalview no puede detectar una conexión a Internet
-label.chimera_path=Ruta de acceso a Chimera
+label.viewer_path=Ruta de acceso a {0}
+label.viewer_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación
+label.invalid_viewer_path=Ruta no encontrada o no ejecutable
+label.viewer_missing=Visualizador de estructura no encontrado.<br/>Por favor, introduzca la ruta de la ejecutable,<br/>o descargar e instalar el programa.
 warn.delete_all=<html>Borrar todas las secuencias cerrará la ventana del alineamiento.<br>Confirmar o Cancelar.
 label.select_all=Seleccionar Todos
 label.alpha_helix=Hélice Alfa
-label.chimera_help=Ayuda para Chimera
 label.find_tip=Buscar alineamiento, selección o IDs de secuencia para una subsecuencia (sin huecos)
 label.structure_viewer=Visualizador por defecto
 label.embbed_biojson=Incrustar BioJSON al exportar HTML
@@ -1214,12 +1187,10 @@ label.aacon_settings=Cambiar Ajustes AACon...
 tooltip.aacon_calculations=Actualizar cálculos AACon automáticamente.
 info.select_filter_option=Escoger Opción de Filtro / Entrada Manual
 info.invalid_msa_input_mininfo=Necesita por lo menos dos secuencias con al menos 3 residuos cada una, sin regiones ocultas entre ellas.
-label.chimera_missing=Visualizador de estructura Chimera no encontrado.<br/>Por favor, introduzca la ruta de Chimera,<br/>o descargar e instalar la UCSF Chimera.
 exception.fts_server_unreachable=Jalview no puede conectar con el servidor {0}. \nPor favor asegúrese de que está conectado a Internet y vuelva a intentarlo.
 exception.outofmemory_loading_mmcif_file=Sin memoria al cargar el fichero mmCIF
 label.hide_columns_not_containing=Ocultar las columnas que no contengan
 label.pdb_sequence_fetcher=Recuperador de secuencias PDB
-status.waiting_for_user_to_select_output_file=Esperando que el usuario seleccione el fichero {0}
 action.prev_page=<< 
 status.cancelled_image_export_operation=Operación de exportación {0} cancelada
 label.couldnt_run_groovy_script=No se ha podido ejecutar el script Groovy
@@ -1314,7 +1285,6 @@ option.enable_disable_autosearch = Marcar para buscar autom
 option.autosearch = Auto búsqueda
 label.retrieve_ids = Recuperar IDs
 label.display_settings_for = Visualización de características {0}
-label.simple = Simple
 label.simple_colour = Color simple
 label.colour_by_text = Colorear por texto
 label.graduated_colour = Color graduado
@@ -1351,7 +1321,6 @@ label.backup_filename_strategy = Estrategia de nombres de archivo de respaldos
 label.append_to_filename = Adjuntar texto (%n es reemplazado por el número de respaldo)
 label.append_to_filename_tooltip = %n en el texto será reemplazado por el número de respaldo. El texto será después del nombre del archivo. Vea el cuadro de resumen arriba.
 label.index_digits = Número de dígitos a utilizar para el número de respaldo.
-label.summary_of_backups_scheme = Resumen del esquema de copias de seguridad
 label.scheme_examples = Ejemplos de esquema
 label.increment_index = Aumente los números de texto adjuntos: el archivo más nuevo tiene el número más grande
 label.reverse_roll = Ciclos de texto adjuntos: el respaldo más reciente es siempre el número 1
@@ -1380,13 +1349,11 @@ label.single_file_description = Conserve la 
 label.keep_all_versions_description = Mantener todas las versiones anteriores del archivo
 label.rolled_backups_description = Mantenga las Ãºltimas nueve versiones del archivo desde _bak.1 (más reciente) a _bak.9 (más antigua)
 label.cancel_changes_description = Cancelar los cambios realizados en su Ãºltimo esquema personalizado guardado
-label.previously_saved_scheme = Esquema previamente guardado
 label.no_backup_files = NO ARCHIVOS DE RESPALDOS
 label.include_backup_files = Incluir archivos de respaldos
 label.cancel_changes = Cancelar cambios
 label.warning_confirm_change_reverse = Â¡Advertencia!\nSi cambia el incremento/decremento del número de archivos de respaldos, sin cambiar el sufijo o número de dígitos,\nesto puede causar la pérdida de los archivos de respaldos creados con el esquema anterior de nombre de archivo de respaldos.\n¿Está seguro de que desea hacer esto?
 label.change_increment_decrement = Â¿Cambiar de incremento/decremento?
-label.was_previous = era {0}
 label.newerdelete_replacement_line = El archivo de respaldo\n''{0}''\t(modificado {2}, tamaño {4})\nserá borrado y reemplazarse por un archivo aparentemente más antiguo\n''{1}''\t(modificado {3}, tamaño {5}).
 label.confirm_deletion_or_rename = Confirmar borrar ''{0}'', o cambiar el nombre a ''{1}''?
 label.newerdelete_line = El archivo de respaldo\n''{0}''\t(modificado {2}, tamaño {4})\nserá borrado pero es mas nuevo que el archivo de respaldo restante más antiguo\n''{1}''\t(modified {3}, size {5}).
index a910a5a..d7e7937 100644 (file)
@@ -32,8 +32,6 @@
  */
 package ext.edu.ucsf.rbvi.strucviz2;
 
-import jalview.ws.HttpClientUtils;
-
 import java.awt.Color;
 import java.io.BufferedReader;
 import java.io.File;
@@ -54,6 +52,7 @@ import org.slf4j.LoggerFactory;
 
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
 import ext.edu.ucsf.rbvi.strucviz2.port.ListenerThreads;
+import jalview.ws.HttpClientUtils;
 
 /**
  * This object maintains the Chimera communication information.
@@ -175,10 +174,11 @@ public class ChimeraManager
     return hasChimeraModel(modelNubmer, 0);
   }
 
-  public boolean hasChimeraModel(Integer modelNubmer, Integer subModelNumber)
+  public boolean hasChimeraModel(Integer modelNubmer,
+          Integer subModelNumber)
   {
-    return currentModelsMap.containsKey(ChimUtils.makeModelKey(modelNubmer,
-            subModelNumber));
+    return currentModelsMap.containsKey(
+            ChimUtils.makeModelKey(modelNubmer, subModelNumber));
   }
 
   public void addChimeraModel(Integer modelNumber, Integer subModelNumber,
@@ -188,7 +188,8 @@ public class ChimeraManager
             ChimUtils.makeModelKey(modelNumber, subModelNumber), model);
   }
 
-  public void removeChimeraModel(Integer modelNumber, Integer subModelNumber)
+  public void removeChimeraModel(Integer modelNumber,
+          Integer subModelNumber)
   {
     int modelKey = ChimUtils.makeModelKey(modelNumber, subModelNumber);
     if (currentModelsMap.containsKey(modelKey))
@@ -243,9 +244,8 @@ public class ChimeraManager
       if (!modelList.contains(newModel))
       {
         newModel.setModelName(modelName);
-        sendChimeraCommand(
-                "setattr M name " + modelName + " #"
-                        + newModel.getModelNumber(), false);
+        sendChimeraCommand("setattr M name " + modelName + " #"
+                + newModel.getModelNumber(), false);
         modelList.add(newModel);
       }
     }
@@ -254,7 +254,7 @@ public class ChimeraManager
     for (ChimeraModel chimeraModel : modelList)
     {
       // get model color
-      Color modelColor = getModelColor(chimeraModel);
+      Color modelColor = isChimeraX() ? null : getModelColor(chimeraModel);
       if (modelColor != null)
       {
         chimeraModel.setModelColor(modelColor);
@@ -265,7 +265,7 @@ public class ChimeraManager
       // chimeraSend("repr stick "+newModel.toSpec());
 
       // Create the information we need for the navigator
-      if (type != ModelType.SMILES)
+      if (type != ModelType.SMILES && !isChimeraX())
       {
         addResidues(chimeraModel);
       }
@@ -315,8 +315,8 @@ public class ChimeraManager
     {
       sendChimeraCommand("close " + model.toSpec(), false);
       // currentModelNamesMap.remove(model.getModelName());
-      currentModelsMap.remove(ChimUtils.makeModelKey(
-              model.getModelNumber(), model.getSubModelNumber()));
+      currentModelsMap.remove(ChimUtils.makeModelKey(model.getModelNumber(),
+              model.getSubModelNumber()));
       // selectionList.remove(chimeraModel);
     }
     else
@@ -329,12 +329,14 @@ public class ChimeraManager
 
   public void startListening()
   {
-    sendChimeraCommand("listen start models; listen start selection", false);
+    sendChimeraCommand("listen start models; listen start selection",
+            false);
   }
 
   public void stopListening()
   {
-    sendChimeraCommand("listen stop models ; listen stop selection ", false);
+    String command = "listen stop models ; listen stop selection ";
+    sendChimeraCommand(command, false);
   }
 
   /**
@@ -344,9 +346,17 @@ public class ChimeraManager
    */
   public void startListening(String uri)
   {
-    sendChimeraCommand("listen start models url " + uri
-            + ";listen start select prefix SelectionChanged url " + uri,
-            false);
+    /*
+     * listen for model changes
+     */
+    String command = "listen start models url " + uri;
+    sendChimeraCommand(command, false);
+
+    /*
+     * listen for selection changes
+     */
+    command = "listen start select prefix SelectionChanged url " + uri;
+    sendChimeraCommand(command, false);
   }
 
   /**
@@ -420,19 +430,23 @@ public class ChimeraManager
   public List<String> getSelectedResidueSpecs()
   {
     List<String> selectedResidues = new ArrayList<>();
-    List<String> chimeraReply = sendChimeraCommand(
-            "list selection level residue", true);
+
+    String command = "list selection level residue";
+    List<String> chimeraReply = sendChimeraCommand(command, true);
     if (chimeraReply != null)
     {
       /*
-       * expect 0, 1 or more lines of the format
+       * expect 0, 1 or more lines of the format either
+       * Chimera:
        * residue id #0:43.A type GLY
-       * where we are only interested in the atomspec #0.43.A
+       * ChimeraX:
+       * residue id /A:89 name THR index 88
+       * We are only interested in the atomspec (third token of the reply)
        */
       for (String inputLine : chimeraReply)
       {
         String[] inputLineParts = inputLine.split("\\s+");
-        if (inputLineParts.length == 5)
+        if (inputLineParts.length >= 5)
         {
           selectedResidues.add(inputLineParts[2]);
         }
@@ -473,14 +487,21 @@ public class ChimeraManager
   public List<ChimeraModel> getModelList()
   {
     List<ChimeraModel> modelList = new ArrayList<>();
-    List<String> list = sendChimeraCommand("list models type molecule",
-            true);
+    String command = "list models type "
+            + (isChimeraX() ? "AtomicStructure" : "molecule");
+    List<String> list = sendChimeraCommand(command, true);
     if (list != null)
     {
       for (String modelLine : list)
       {
-        ChimeraModel chimeraModel = new ChimeraModel(modelLine);
-        modelList.add(chimeraModel);
+        try
+        {
+          ChimeraModel chimeraModel = new ChimeraModel(modelLine);
+          modelList.add(chimeraModel);
+        } catch (NullPointerException e)
+        {
+          // hack for now
+        }
       }
     }
     return modelList;
@@ -567,8 +588,7 @@ public class ChimeraManager
         args.add(chimeraPath);
         // shows Chimera output window but suppresses REST responses:
         // args.add("--debug");
-        args.add("--start");
-        args.add("RESTServer");
+        addLaunchArguments(args);
         ProcessBuilder pb = new ProcessBuilder(args);
         chimera = pb.start();
         error = "";
@@ -584,8 +604,8 @@ public class ChimeraManager
     if (error.length() == 0)
     {
       this.chimeraRestPort = getPortNumber();
-      System.out.println("Chimera REST API started on port "
-              + chimeraRestPort);
+      System.out.println(
+              "Chimera REST API started on port " + chimeraRestPort);
       // structureManager.initChimTable();
       structureManager.setChimeraPathProperty(workingPath);
       // TODO: [Optional] Check Chimera version and show a warning if below 1.8
@@ -600,14 +620,27 @@ public class ChimeraManager
   }
 
   /**
+   * Adds command-line arguments to start the REST server
+   * <p>
+   * Method extracted for Jalview to allow override in ChimeraXManager
+   * 
+   * @param args
+   */
+  protected void addLaunchArguments(List<String> args)
+  {
+    args.add("--start");
+    args.add("RESTServer");
+  }
+
+  /**
    * Read and return the port number returned in the reply to --start RESTServer
    */
   private int getPortNumber()
   {
     int port = 0;
     InputStream readChan = chimera.getInputStream();
-    BufferedReader lineReader = new BufferedReader(new InputStreamReader(
-            readChan));
+    BufferedReader lineReader = new BufferedReader(
+            new InputStreamReader(readChan));
     StringBuilder responses = new StringBuilder();
     try
     {
@@ -616,21 +649,29 @@ public class ChimeraManager
       {
         responses.append("\n" + response);
         // expect: REST server on host 127.0.0.1 port port_number
+        // ChimeraX is the same except "REST server started on host..."
         if (response.startsWith("REST server"))
         {
           String[] tokens = response.split(" ");
-          if (tokens.length == 7 && "port".equals(tokens[5]))
+          for (int i = 0; i < tokens.length - 1; i++)
           {
-            port = Integer.parseInt(tokens[6]);
-            break;
+            if ("port".equals(tokens[i]))
+            {
+              port = Integer.parseInt(tokens[i + 1]);
+              break;
+            }
           }
         }
+        if (port > 0)
+        {
+          break; // hack for hanging readLine()
+        }
         response = lineReader.readLine();
       }
     } catch (Exception e)
     {
-      logger.error("Failed to get REST port number from " + responses
-              + ": " + e.getMessage());
+      logger.error("Failed to get REST port number from " + responses + ": "
+              + e.getMessage());
     } finally
     {
       try
@@ -642,11 +683,12 @@ public class ChimeraManager
     }
     if (port == 0)
     {
-      System.err
-              .println("Failed to start Chimera with REST service, response was: "
+      System.err.println(
+              "Failed to start Chimera with REST service, response was: "
                       + responses);
     }
-    logger.info("Chimera REST service listening on port " + chimeraRestPort);
+    logger.info(
+            "Chimera REST service listening on port " + chimeraRestPort);
     return port;
   }
 
@@ -703,7 +745,8 @@ public class ChimeraManager
   public List<String> getAttrList()
   {
     List<String> attributes = new ArrayList<>();
-    final List<String> reply = sendChimeraCommand("list resattr", true);
+    String command = (isChimeraX() ? "info " : "list ") + "resattr";
+    final List<String> reply = sendChimeraCommand(command, true);
     if (reply != null)
     {
       for (String inputLine : reply)
@@ -731,8 +774,8 @@ public class ChimeraManager
         String[] lineParts = inputLine.split("\\s");
         if (lineParts.length == 5)
         {
-          ChimeraResidue residue = ChimUtils
-                  .getResidue(lineParts[2], model);
+          ChimeraResidue residue = ChimUtils.getResidue(lineParts[2],
+                  model);
           String value = lineParts[4];
           if (residue != null)
           {
@@ -775,18 +818,27 @@ public class ChimeraManager
    */
   public List<String> sendChimeraCommand(String command, boolean reply)
   {
-   // System.out.println("chimeradebug>> " + command);
+    if (debug)
+    {
+      System.out.println("chimeradebug>> " + command);
+    }
     if (!isChimeraLaunched() || command == null
             || "".equals(command.trim()))
     {
       return null;
     }
-    // TODO do we need a maximum wait time before aborting?
-    while (busy)
+    /*
+     * set a maximum wait time before trying anyway
+     * to avoid hanging indefinitely
+     */
+    int waited = 0;
+    int pause = 25;
+    while (busy  && waited < 1001)
     {
       try
       {
-        Thread.sleep(25);
+        Thread.sleep(pause);
+        waited += pause;
       } catch (InterruptedException q)
       {
       }
@@ -808,7 +860,6 @@ public class ChimeraManager
                 + (System.currentTimeMillis() - startTime) + "ms: "
                 + command);
       }
-
     }
   }
 
@@ -822,14 +873,23 @@ public class ChimeraManager
   {
     String restUrl = "http://127.0.0.1:" + this.chimeraRestPort + "/run";
     List<NameValuePair> commands = new ArrayList<>(1);
+    String method = getHttpRequestMethod();
+    if ("GET".equals(method))
+    {
+      command = command.replace(" ", "+").replace("#", "%23")
+              .replace("|", "%7C").replace(";", "%3B");
+    }
     commands.add(new BasicNameValuePair("command", command));
 
     List<String> reply = new ArrayList<>();
     BufferedReader response = null;
     try
     {
-      response = HttpClientUtils.doHttpUrlPost(restUrl, commands, CONNECTION_TIMEOUT_MS,
-              REST_REPLY_TIMEOUT_MS);
+      response = "GET".equals(method)
+              ? HttpClientUtils.doHttpGet(restUrl, commands,
+                      CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS)
+              : HttpClientUtils.doHttpUrlPost(restUrl, commands,
+                      CONNECTION_TIMEOUT_MS, REST_REPLY_TIMEOUT_MS);
       String line = "";
       while ((line = response.readLine()) != null)
       {
@@ -854,6 +914,17 @@ public class ChimeraManager
   }
 
   /**
+   * Returns "POST" as the HTTP request method to use for REST service calls to
+   * Chimera
+   * 
+   * @return
+   */
+  protected String getHttpRequestMethod()
+  {
+    return "POST";
+  }
+
+  /**
    * Send a command to stdin of Chimera process, and optionally read any
    * responses.
    * 
@@ -901,4 +972,9 @@ public class ChimeraManager
   {
     return chimera;
   }
+
+  public boolean isChimeraX()
+  {
+    return false;
+  }
 }
index 22c9098..5cf8a73 100644 (file)
@@ -32,9 +32,6 @@
  */
 package ext.edu.ucsf.rbvi.strucviz2;
 
-import jalview.bin.Cache;
-import jalview.gui.Preferences;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -49,6 +46,9 @@ import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import jalview.bin.Cache;
+import jalview.gui.Preferences;
+
 /**
  * This object maintains the relationship between Chimera objects and Cytoscape
  * objects.
@@ -56,6 +56,27 @@ import org.slf4j.LoggerFactory;
 
 public class StructureManager
 {
+  /*
+   * Version numbers to build Windows installation paths for 
+   * Chimera  https://www.cgl.ucsf.edu/chimera/download.html
+   * ChimeraX http://www.rbvi.ucsf.edu/chimerax/download.html#release
+   *          https://www.rbvi.ucsf.edu/trac/ChimeraX/wiki/ChangeLog
+   * These are a fallback for Jalview users who don't save path in Preferences;
+   * these will need to be updated as new versions are released;
+   * deliberately not 'final' (so modifiable using Groovy).
+   * 
+   * May 2020: 1.14 is Chimera latest, anticipating a few more...
+   * 0.93 is ChimeraX latest, 1.0 expected soon
+   */
+  private static String[] CHIMERA_VERSIONS = new String[] { "1.16.2",
+      "1.16.1", "1.16",
+      "1.15.2", "1.15.1", "1.15", "1.14.2", "1.14.1", "1.14",
+      "1.13.1", "1.13", "1.12.2", "1.12.1", "1.12", "1.11.2",
+      "1.11.2", "1.11.1", "1.11" };
+
+  private static String[] CHIMERAX_VERSIONS = new String[] { "1.0", "0.93",
+      "0.92", "0.91", "0.9" };
+
   static final String[] defaultStructureKeys = { "Structure", "pdb",
       "pdbFileName", "PDB ID", "structure", "biopax.xref.PDB", "pdb_ids",
       "ModelName", "ModelNumber" };
@@ -896,7 +917,7 @@ public class StructureManager
   StructureSettings defaultSettings = null;
 
   // TODO: [Optional] Change priority of Chimera paths
-  public static List<String> getChimeraPaths()
+  public static List<String> getChimeraPaths(boolean isChimeraX)
   {
     List<String> pathList = new ArrayList<>();
 
@@ -916,22 +937,35 @@ public class StructureManager
     // }
 
     /*
-     * Jalview addition: check if path set in user preferences.
+     * Jalview addition: check if path set in user preferences
      */
-    String userPath = Cache.getDefault(Preferences.CHIMERA_PATH, null);
+    String userPath = Cache
+            .getDefault(isChimeraX ? Preferences.CHIMERAX_PATH
+                    : Preferences.CHIMERA_PATH, null);
     if (userPath != null)
     {
-      pathList.add(0, userPath);
+      pathList.add(userPath);
     }
 
+    /*
+     * paths are based on getChimeraPaths() in
+     * Chimera:
+     * https://github.com/RBVI/structureViz2/blob/master/src/main/java/edu/ucsf/rbvi/structureViz2/internal/model/StructureManager.java
+     * ChimeraX:
+     * https://github.com/RBVI/structureVizX/blob/master/src/main/java/edu/ucsf/rbvi/structureVizX/internal/model/StructureManager.java
+     */
+    String chimera = isChimeraX ? "ChimeraX" : "Chimera";
+    String chimeraExe = isChimeraX ? "ChimeraX" : "chimera";
+
     // Add default installation paths
     String os = System.getProperty("os.name");
     if (os.startsWith("Linux"))
     {
-      pathList.add("/usr/local/chimera/bin/chimera");
-      pathList.add("/usr/local/bin/chimera");
-      pathList.add("/usr/bin/chimera");
-      pathList.add(System.getProperty("user.home") + "/opt/bin/chimera");
+      // todo should this be /chimeraX/ for ChimeraX? not in structureVizX code
+      pathList.add("/usr/local/chimera/bin/" + chimeraExe);
+      pathList.add("/usr/local/bin/" + chimeraExe);
+      pathList.add("/usr/bin/" + chimeraExe);
+      pathList.add(System.getProperty("user.home") + "/opt/bin/" + chimeraExe);
     }
     else if (os.startsWith("Windows"))
     {
@@ -939,18 +973,22 @@ public class StructureManager
           "C:\\Program Files", "\\Program Files (x86)",
           "C:\\Program Files (x86)" })
       {
-        for (String version : new String[] { "1.11", "1.11.1", "1.11.2",
-            "1.12", "1.12.1", "1.12.2", "1.13" })
+        String[] candidates = isChimeraX ? CHIMERAX_VERSIONS
+                : CHIMERA_VERSIONS;
+        for (String version : candidates)
         {
-          pathList.add(root + "\\Chimera " + version + "\\bin\\chimera");
-          pathList.add(
-                  root + "\\Chimera " + version + "\\bin\\chimera.exe");
+          // TODO original code doesn't include version in path; which is right?
+          String path = String.format("%s\\%s %s\\bin\\%s", root, chimera,
+                  version, chimeraExe);
+          pathList.add(path);
+          pathList.add(path + ".exe");
         }
       }
     }
     else if (os.startsWith("Mac"))
     {
-      pathList.add("/Applications/Chimera.app/Contents/MacOS/chimera");
+      pathList.add(String.format("/Applications/%s.app/Contents/MacOS/%s",
+              chimera, chimeraExe));
     }
     return pathList;
   }
index daed0ac..228ea30 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.analysis;
 
 public interface GeneticCodeI
index 137b7f8..df1dd82 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.analysis;
 
 import jalview.bin.Cache;
index 19f6136..71b35ef 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.api;
 
 import jalview.datamodel.SearchResultsI;
index 8f778f7..d8c8371 100644 (file)
@@ -23,7 +23,6 @@ package jalview.api.structures;
 import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
-import jalview.schemes.ColourSchemeI;
 import jalview.structures.models.AAStructureBindingModel;
 
 public interface JalviewStructureDisplayI
@@ -59,13 +58,6 @@ public interface JalviewStructureDisplayI
   void closeViewer(boolean closeExternalViewer);
 
   /**
-   * apply a colourscheme to the structures in the viewer
-   * 
-   * @param colourScheme
-   */
-  void setJalviewColourScheme(ColourSchemeI colourScheme);
-
-  /**
    * 
    * @return true if all background sequence/structure binding threads have
    *         completed for this viewer instance
@@ -125,4 +117,48 @@ public interface JalviewStructureDisplayI
    */
   void raiseViewer();
 
+  AlignmentViewPanel getAlignmentPanel();
+
+  /**
+   * Answers true if the given alignment view is used to colour structures by
+   * sequence, false if not
+   * 
+   * @param ap
+   * @return
+   */
+  boolean isUsedForColourBy(AlignmentViewPanel ap);
+
+  /**
+   * If implemented, shows a command line console in the structure viewer
+   * 
+   * @param show
+   *          true to show, false to hide
+   */
+  void showConsole(boolean show);
+
+  /**
+   * Remove references to the given alignment view for this structure viewer
+   * 
+   * @param avp
+   */
+  void removeAlignmentPanel(AlignmentViewPanel avp);
+
+  /**
+   * Updates the progress bar if there is one. Call stopProgressBar with the
+   * returned handle to remove the message.
+   * 
+   * @param msg
+   * @return handle
+   */
+  long startProgressBar(String msg);
+
+  /**
+   * Ends the progress bar with the specified handle, leaving a message (if not
+   * null) on the status bar
+   * 
+   * @param msg
+   * @param handle
+   */
+  void stopProgressBar(String msg, long handle);
+
 }
index 3aed7ec..80db11d 100644 (file)
  */
 package jalview.appletgui;
 
+import java.awt.CheckboxMenuItem;
+import java.awt.Frame;
+import java.awt.Menu;
+import java.awt.MenuItem;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.Vector;
+
 import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.analysis.AlignmentUtils;
@@ -57,25 +76,6 @@ import jalview.schemes.ZappoColourScheme;
 import jalview.util.MessageManager;
 import jalview.util.UrlLink;
 
-import java.awt.CheckboxMenuItem;
-import java.awt.Frame;
-import java.awt.Menu;
-import java.awt.MenuItem;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.ItemEvent;
-import java.awt.event.ItemListener;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.Vector;
-
 public class APopupMenu extends java.awt.PopupMenu
         implements ActionListener, ItemListener
 {
@@ -900,7 +900,7 @@ public class APopupMenu extends java.awt.PopupMenu
       contents.append(MessageManager
               .formatMessage("label.annotation_for_displayid", new Object[]
               { seq.getDisplayId(true) }));
-      new SequenceAnnotationReport(null).createSequenceAnnotationReport(
+      new SequenceAnnotationReport(false).createSequenceAnnotationReport(
               contents, seq, true, true, ap.seqPanel.seqCanvas.fr);
       contents.append("</p>");
     }
@@ -1056,7 +1056,7 @@ public class APopupMenu extends java.awt.PopupMenu
     BLOSUM62Colour.setName(JalviewColourScheme.Blosum62.toString());
     BLOSUM62Colour.addItemListener(this);
     PIDColour.setLabel(
-            MessageManager.getString("label.colourScheme_%_identity"));
+            MessageManager.getString("label.colourScheme_%identity"));
     PIDColour.setName(JalviewColourScheme.PID.toString());
     PIDColour.addItemListener(this);
     zappoColour
@@ -1073,19 +1073,19 @@ public class APopupMenu extends java.awt.PopupMenu
             .setName(JalviewColourScheme.Hydrophobic.toString());
     hydrophobicityColour.addItemListener(this);
     helixColour.setLabel(MessageManager
-            .getString("label.colourScheme_helix_propensity"));
+            .getString("label.colourScheme_helixpropensity"));
     helixColour.setName(JalviewColourScheme.Helix.toString());
     helixColour.addItemListener(this);
     strandColour.setLabel(MessageManager
-            .getString("label.colourScheme_strand_propensity"));
+            .getString("label.colourScheme_strandpropensity"));
     strandColour.setName(JalviewColourScheme.Strand.toString());
     strandColour.addItemListener(this);
     turnColour.setLabel(
-            MessageManager.getString("label.colourScheme_turn_propensity"));
+            MessageManager.getString("label.colourScheme_turnpropensity"));
     turnColour.setName(JalviewColourScheme.Turn.toString());
     turnColour.addItemListener(this);
     buriedColour.setLabel(
-            MessageManager.getString("label.colourScheme_buried_index"));
+            MessageManager.getString("label.colourScheme_buriedindex"));
     buriedColour.setName(JalviewColourScheme.Buried.toString());
     buriedColour.addItemListener(this);
     nucleotideColour.setLabel(
index 1a46585..b6f8929 100644 (file)
@@ -3522,16 +3522,16 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
             MessageManager.getString("label.colourScheme_hydrophobic"));
     hydrophobicityColour.addActionListener(this);
     helixColour.setLabel(MessageManager
-            .getString("label.colourScheme_helix_propensity"));
+            .getString("label.colourScheme_helixpropensity"));
     helixColour.addActionListener(this);
     strandColour.setLabel(MessageManager
-            .getString("label.colourScheme_strand_propensity"));
+            .getString("label.colourScheme_strandpropensity"));
     strandColour.addActionListener(this);
     turnColour.setLabel(
-            MessageManager.getString("label.colourScheme_turn_propensity"));
+            MessageManager.getString("label.colourScheme_turnpropensity"));
     turnColour.addActionListener(this);
     buriedColour.setLabel(
-            MessageManager.getString("label.colourScheme_buried_index"));
+            MessageManager.getString("label.colourScheme_buriedindex"));
     buriedColour.addActionListener(this);
     purinePyrimidineColour.setLabel(MessageManager
             .getString("label.colourScheme_purine/pyrimidine"));
@@ -3540,19 +3540,19 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     // .getString("label.rna_interaction"));
     // RNAInteractionColour.addActionListener(this);
     RNAHelixColour.setLabel(
-            MessageManager.getString("label.colourScheme_rna_helices"));
+            MessageManager.getString("label.colourScheme_rnahelices"));
     RNAHelixColour.addActionListener(this);
     userDefinedColour
             .setLabel(MessageManager.getString("action.user_defined"));
     userDefinedColour.addActionListener(this);
     PIDColour.setLabel(
-            MessageManager.getString("label.colourScheme_%_identity"));
+            MessageManager.getString("label.colourScheme_%identity"));
     PIDColour.addActionListener(this);
     BLOSUM62Colour.setLabel(
             MessageManager.getString("label.colourScheme_blosum62"));
     BLOSUM62Colour.addActionListener(this);
     tcoffeeColour.setLabel(
-            MessageManager.getString("label.colourScheme_t-coffee_scores"));
+            MessageManager.getString("label.colourScheme_t-coffeescores"));
     // it will be enabled only if a score file is provided
     tcoffeeColour.setEnabled(false);
     tcoffeeColour.addActionListener(this);
@@ -3964,7 +3964,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
    * without an additional javascript library to exchange messages between the
    * distinct applets. See http://issues.jalview.org/browse/JAL-621
    * 
-   * @param viewer
+   * @param jmolViewer
    *          JmolViewer instance
    * @param sequenceIds
    *          - sequence Ids to search for associations
index 7fda3c4..380ec25 100644 (file)
  */
 package jalview.appletgui;
 
-import jalview.bin.JalviewLite;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.io.DataSourceType;
-import jalview.io.FileParse;
-import jalview.io.StructureFile;
-import jalview.schemes.BuriedColourScheme;
-import jalview.schemes.HelixColourScheme;
-import jalview.schemes.HydrophobicColourScheme;
-import jalview.schemes.PurinePyrimidineColourScheme;
-import jalview.schemes.StrandColourScheme;
-import jalview.schemes.TaylorColourScheme;
-import jalview.schemes.TurnColourScheme;
-import jalview.schemes.UserColourScheme;
-import jalview.schemes.ZappoColourScheme;
-import jalview.structure.StructureSelectionManager;
-import jalview.util.MessageManager;
-
 import java.awt.BorderLayout;
 import java.awt.CheckboxMenuItem;
 import java.awt.Color;
@@ -64,6 +45,25 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Vector;
 
+import jalview.bin.JalviewLite;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileParse;
+import jalview.io.StructureFile;
+import jalview.schemes.BuriedColourScheme;
+import jalview.schemes.HelixColourScheme;
+import jalview.schemes.HydrophobicColourScheme;
+import jalview.schemes.PurinePyrimidineColourScheme;
+import jalview.schemes.StrandColourScheme;
+import jalview.schemes.TaylorColourScheme;
+import jalview.schemes.TurnColourScheme;
+import jalview.schemes.UserColourScheme;
+import jalview.schemes.ZappoColourScheme;
+import jalview.structure.StructureSelectionManager;
+import jalview.util.MessageManager;
+
 public class AppletJmol extends EmbmenuFrame implements
         // StructureListener,
         KeyListener, ActionListener, ItemListener
@@ -104,16 +104,16 @@ public class AppletJmol extends EmbmenuFrame implements
           MessageManager.getString("label.colourScheme_hydrophobic"));
 
   MenuItem helix = new MenuItem(
-          MessageManager.getString("label.colourScheme_helix_propensity"));
+          MessageManager.getString("label.colourScheme_helixpropensity"));
 
   MenuItem strand = new MenuItem(
-          MessageManager.getString("label.colourScheme_strand_propensity"));
+          MessageManager.getString("label.colourScheme_strandpropensity"));
 
   MenuItem turn = new MenuItem(
-          MessageManager.getString("label.colourScheme_turn_propensity"));
+          MessageManager.getString("label.colourScheme_turnpropensity"));
 
   MenuItem buried = new MenuItem(
-          MessageManager.getString("label.colourScheme_buried_index"));
+          MessageManager.getString("label.colourScheme_buriedindex"));
 
   MenuItem purinepyrimidine = new MenuItem(
           MessageManager.getString("label.colourScheme_purine/pyrimidine"));
@@ -307,7 +307,7 @@ public class AppletJmol extends EmbmenuFrame implements
       else if (protocol == DataSourceType.FILE
               || protocol == DataSourceType.URL)
       {
-        jmb.viewer.openFile(pdbentry.getFile());
+        jmb.jmolViewer.openFile(pdbentry.getFile());
       }
       else
       {
@@ -350,7 +350,7 @@ public class AppletJmol extends EmbmenuFrame implements
             throw new Exception(MessageManager.getString(
                     "exception.invalid_datasource_couldnt_obtain_reader"));
           }
-          jmb.viewer.openReader(pdbentry.getFile(), pdbentry.getId(),
+          jmb.jmolViewer.openReader(pdbentry.getFile(), pdbentry.getId(),
                   freader);
         } catch (Exception e)
         {
@@ -406,12 +406,12 @@ public class AppletJmol extends EmbmenuFrame implements
         }
       }
     }
-    jmb.centerViewer(toshow);
+    jmb.showChains(toshow);
   }
 
   void closeViewer()
   {
-    jmb.closeViewer();
+    jmb.closeViewer(true);
     jmb = null;
     this.setVisible(false);
   }
@@ -455,41 +455,41 @@ public class AppletJmol extends EmbmenuFrame implements
     else if (evt.getSource() == zappo)
     {
       setEnabled(zappo);
-      jmb.setJalviewColourScheme(new ZappoColourScheme());
+      jmb.colourByJalviewColourScheme(new ZappoColourScheme());
     }
     else if (evt.getSource() == taylor)
     {
       setEnabled(taylor);
-      jmb.setJalviewColourScheme(new TaylorColourScheme());
+      jmb.colourByJalviewColourScheme(new TaylorColourScheme());
     }
     else if (evt.getSource() == hydro)
     {
       setEnabled(hydro);
-      jmb.setJalviewColourScheme(new HydrophobicColourScheme());
+      jmb.colourByJalviewColourScheme(new HydrophobicColourScheme());
     }
     else if (evt.getSource() == helix)
     {
       setEnabled(helix);
-      jmb.setJalviewColourScheme(new HelixColourScheme());
+      jmb.colourByJalviewColourScheme(new HelixColourScheme());
     }
     else if (evt.getSource() == strand)
     {
       setEnabled(strand);
-      jmb.setJalviewColourScheme(new StrandColourScheme());
+      jmb.colourByJalviewColourScheme(new StrandColourScheme());
     }
     else if (evt.getSource() == turn)
     {
       setEnabled(turn);
-      jmb.setJalviewColourScheme(new TurnColourScheme());
+      jmb.colourByJalviewColourScheme(new TurnColourScheme());
     }
     else if (evt.getSource() == buried)
     {
       setEnabled(buried);
-      jmb.setJalviewColourScheme(new BuriedColourScheme());
+      jmb.colourByJalviewColourScheme(new BuriedColourScheme());
     }
     else if (evt.getSource() == purinepyrimidine)
     {
-      jmb.setJalviewColourScheme(new PurinePyrimidineColourScheme());
+      jmb.colourByJalviewColourScheme(new PurinePyrimidineColourScheme());
     }
     else if (evt.getSource() == user)
     {
@@ -658,7 +658,7 @@ public class AppletJmol extends EmbmenuFrame implements
     {
       currentSize = this.getSize();
 
-      if (jmb.viewer == null)
+      if (jmb.jmolViewer == null)
       {
         g.setColor(Color.black);
         g.fillRect(0, 0, currentSize.width, currentSize.height);
@@ -669,7 +669,7 @@ public class AppletJmol extends EmbmenuFrame implements
       }
       else
       {
-        jmb.viewer.renderScreenImage(g, currentSize.width,
+        jmb.jmolViewer.renderScreenImage(g, currentSize.width,
                 currentSize.height);
       }
     }
@@ -693,9 +693,9 @@ public class AppletJmol extends EmbmenuFrame implements
    * 
    * }
    */
-  public void setJalviewColourScheme(UserColourScheme ucs)
+  public void colourByJalviewColourScheme(UserColourScheme ucs)
   {
-    jmb.setJalviewColourScheme(ucs);
+    jmb.colourByJalviewColourScheme(ucs);
   }
 
   public AlignmentPanel getAlignmentPanelFor(AlignmentI alignment)
index f1c494e..c7ce994 100644 (file)
@@ -51,13 +51,6 @@ class AppletJmolBinding extends JalviewJmolBinding
   }
 
   @Override
-  public jalview.api.FeatureRenderer getFeatureRenderer(
-          AlignmentViewPanel alignment)
-  {
-    return appletJmolBinding.ap.getFeatureRenderer();
-  }
-
-  @Override
   public jalview.api.SequenceRenderer getSequenceRenderer(
           AlignmentViewPanel alignment)
   {
@@ -154,7 +147,7 @@ class AppletJmolBinding extends JalviewJmolBinding
           Container consolePanel, String buttonsToShow)
   {
     JmolAppConsoleInterface appc = new AppletConsole();
-    appc.start(viewer);
+    appc.start(jmolViewer);
     return appc;
   }
 
index 5a6d0d0..47f9df0 100644 (file)
@@ -21,7 +21,6 @@
 package jalview.appletgui;
 
 import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
@@ -76,21 +75,6 @@ public class ExtJmol extends JalviewJmolBinding
   }
 
   @Override
-  public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment)
-  {
-    AlignmentPanel alignPanel = (AlignmentPanel) alignment;
-    if (alignPanel.av.isShowSequenceFeatures())
-    {
-      return alignPanel.getFeatureRenderer();
-    }
-    else
-    {
-      return null;
-    }
-  }
-
-
-  @Override
   public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
   {
     return ((AlignmentPanel) alignment).getSequenceRenderer();
@@ -191,14 +175,8 @@ public class ExtJmol extends JalviewJmolBinding
   }
 
   @Override
-  public void releaseReferences(Object svl)
-  {
-  }
-
-  @Override
   public Map<String, Object> getJSpecViewProperty(String arg0)
   {
     return null;
   }
-
 }
index febe5f8..83d6fd6 100644 (file)
@@ -524,7 +524,7 @@ public class UserDefinedColours extends Panel
     }
     else if (jmol != null)
     {
-      jmol.setJalviewColourScheme(ucs);
+      jmol.colourByJalviewColourScheme(ucs);
     }
     else if (pdbcanvas != null)
     {
index bba79d1..ff475b6 100755 (executable)
@@ -317,6 +317,9 @@ public class Cache
       // lcastor = Logger.getLogger("org.exolab.castor.xml.Marshaller");
       // lcastor.setLevel(Level.toLevel(Cache.getDefault("logs.Castor.Level",
       // Level.INFO.toString())));
+      // we shouldn't need to do this
+      org.apache.log4j.Logger.getRootLogger().setLevel(org.apache.log4j.Level.INFO); 
+
       jalview.bin.Cache.log.setLevel(Level.toLevel(Cache
               .getDefault("logs.Jalview.level", Level.INFO.toString())));
       // laxis.addAppender(ap);
index e89bffb..b01dfb8 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.bin;
 
 import java.lang.management.ManagementFactory;
index 78d8dde..f5b7009 100755 (executable)
@@ -1030,6 +1030,8 @@ public class Jalview
                     + "-jabaws URL\tSpecify URL for Jabaws services (e.g. for a local installation).\n"
                     + "-fetchfrom nickname\tQuery nickname for features for the alignments and display them.\n"
                     + "-groovy FILE\tExecute groovy script in FILE, after all other arguments have been processed (if FILE is the text 'STDIN' then the file will be read from STDIN)\n"
+                    + "-jvmmempc=PERCENT\tOnly available with standalone executable jar or jalview.bin.Launcher. Limit maximum heap size (memory) to PERCENT% of total physical memory detected. This defaults to 90 if total physical memory can be detected. See https://www.jalview.org/help/html/memory.html for more details.\n"
+                    + "-jvmmemmax=MAXMEMORY\tOnly available with standalone executable jar or jalview.bin.Launcher. Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m), gigabytes(g) or if you're lucky enough, terabytes(t). This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected. See https://www.jalview.org/help/html/memory.html for more details.\n"
                     + "\n~Read documentation in Application or visit http://www.jalview.org for description of Features and Annotations file~\n\n");
   }
 
index 7dd0382..da83a09 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.bin;
 
 import java.awt.Image;
index 1541615..fb1c5cd 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.bin;
 
 import java.io.File;
index 7849ba2..159374a 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.bin;
 
 /**
index ee46a89..7575583 100644 (file)
@@ -459,8 +459,7 @@ public class AlignViewController implements AlignViewControllerI
     }
     else
     {
-      avcg.setStatus(MessageManager
-              .formatMessage("No highlighted regions marked"));
+      avcg.setStatus(MessageManager.getString("label.no_highlighted_regions_marked"));
       if (!extendCurrent)
       {
         cs.clear();
index 09db9d7..732fd1e 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel;
 
 import jalview.util.MapList;
index f81348f..5a98c63 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel;
 
 import jalview.util.MapList;
index 0fa03cf..d652a97 100644 (file)
@@ -1,14 +1,35 @@
+/*
+ * 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.datamodel;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 import jalview.io.gff.Gff3Helper;
 import jalview.schemes.ResidueProperties;
+import jalview.util.MapList;
 import jalview.util.MappingUtils;
 import jalview.util.StringUtils;
 
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
 /**
  * A data bean to hold a list of mapped sequence features (e.g. CDS features
  * mapped from protein), and the mapping between the sequences. It also provides
@@ -18,22 +39,26 @@ import java.util.Set;
  */
 public class MappedFeatures
 {
+  /*
+   * VEP CSQ:HGVSp (if present) is a short-cut to the protein variant consequence
+   */
   private static final String HGV_SP = "HGVSp";
 
   private static final String CSQ = "CSQ";
 
   /*
-   * the mapping from one sequence to another
+   * the sequence the mapped features are on
    */
-  public final Mapping mapping;
+  private final SequenceI featureSequence;
 
-  /**
-   * the sequence mapped from
+  /*
+   * the mapping between sequences;
+   * NB this could be in either sense (from or to featureSequence)
    */
-  public final SequenceI fromSeq;
+  private final Mapping mapping;
 
   /*
-   * features on the sequence mapped to that overlap the mapped positions
+   * features on featureSequence that overlap the mapped positions
    */
   public final List<SequenceFeature> features;
 
@@ -60,21 +85,23 @@ public class MappedFeatures
    * Constructor
    * 
    * @param theMapping
-   * @param from
-   *                      the sequence mapped from (e.g. CDS)
+   *          sequence mapping (which may be either to, or from, the sequence
+   *          holding the linked features)
+   * @param featureSeq
+   *          the sequence hosting the virtual features
    * @param pos
-   *                      the residue position in the sequence mapped to
+   *          the residue position in the sequence mapped to
    * @param res
-   *                      the residue character at position pos
+   *          the residue character at position pos
    * @param theFeatures
-   *                      list of mapped features found in the 'from' sequence at
-   *                      the mapped position(s)
+   *          list of mapped features found in the 'featureSeq' sequence at the
+   *          mapped position(s)
    */
-  public MappedFeatures(Mapping theMapping, SequenceI from, int pos,
+  public MappedFeatures(Mapping theMapping, SequenceI featureSeq, int pos,
           char res, List<SequenceFeature> theFeatures)
   {
     mapping = theMapping;
-    fromSeq = from;
+    featureSequence = featureSeq;
     toPosition = pos;
     toResidue = res;
     features = theFeatures;
@@ -90,13 +117,13 @@ public class MappedFeatures
     {
       codonPos = codonPositions;
       baseCodon = new char[3];
-      int cdsStart = fromSeq.getStart();
+      int cdsStart = featureSequence.getStart();
       baseCodon[0] = Character
-              .toUpperCase(fromSeq.getCharAt(codonPos[0] - cdsStart));
+              .toUpperCase(featureSequence.getCharAt(codonPos[0] - cdsStart));
       baseCodon[1] = Character
-              .toUpperCase(fromSeq.getCharAt(codonPos[1] - cdsStart));
+              .toUpperCase(featureSequence.getCharAt(codonPos[1] - cdsStart));
       baseCodon[2] = Character
-              .toUpperCase(fromSeq.getCharAt(codonPos[2] - cdsStart));
+              .toUpperCase(featureSequence.getCharAt(codonPos[2] - cdsStart));
     }
     else
     {
@@ -108,11 +135,14 @@ public class MappedFeatures
   /**
    * Computes and returns comma-delimited HGVS notation peptide variants derived
    * from codon allele variants. If no variants are found, answers an empty
-   * string.
+   * string. The peptide variant is either simply read from the "CSQ:HGVSp"
+   * attribute if present, else computed based on the "alleles" attribute if
+   * present. If neither attribute is found, no variant (empty string) is
+   * returned.
    * 
    * @param sf
-   *             a sequence feature (which must be one of those held in this
-   *             object)
+   *          a sequence feature (which must be one of those held in this
+   *          object)
    * @return
    */
   public String findProteinVariants(SequenceFeature sf)
@@ -233,4 +263,53 @@ public class MappedFeatures
 
     return vars.toString();
   }
+
+  /**
+   * Answers the name of the linked sequence holding any mapped features
+   * 
+   * @return
+   */
+  public String getLinkedSequenceName()
+  {
+    return featureSequence == null ? null : featureSequence.getName();
+  }
+
+  /**
+   * Answers the mapped ranges (as one or more [start, end] positions) which
+   * correspond to the given [begin, end] range of the linked sequence.
+   * 
+   * <pre>
+   * Example: MappedFeatures with CDS features mapped to peptide 
+   * CDS/200-220 gtc aac TGa acGt att AAC tta
+   * mapped to PEP/6-7 WN by mapping [206, 207, 210, 210, 215, 217] to [6, 7]
+   * getMappedPositions(206, 206) should return [6, 6]
+   * getMappedPositions(200, 214) should return [6, 6]
+   * getMappedPositions(210, 215) should return [6, 7]
+   * </pre>
+   * 
+   * @param begin
+   * @param end
+   * @return
+   */
+  public int[] getMappedPositions(int begin, int end)
+  {
+    MapList map = mapping.getMap();
+    return mapping.to == featureSequence ? map.locateInFrom(begin, end)
+            : map.locateInTo(begin, end);
+  }
+
+  /**
+   * Answers true if the linked features are on coding sequence, false if on
+   * peptide
+   * 
+   * @return
+   */
+  public boolean isFromCds()
+  {
+    if (mapping.getMap().getFromRatio() == 3)
+    {
+      return mapping.to != featureSequence;
+    }
+    return mapping.to == featureSequence;
+  }
 }
index e7c77c0..3e57e4c 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel;
 
 /**
index 31736e5..0074d2a 100755 (executable)
@@ -175,11 +175,15 @@ public class SearchResults implements SearchResultsI
   @Override
   public boolean involvesSequence(SequenceI sequence)
   {
+    final int start = sequence.getStart();
+    final int end = sequence.getEnd();
+
     SequenceI ds = sequence.getDatasetSequence();
-    for (SearchResultMatchI _m : matches)
+    for (SearchResultMatchI m : matches)
     {
-      SequenceI matched = _m.getSequence();
-      if (matched != null && (matched == sequence || matched == ds))
+      SequenceI matched = m.getSequence();
+      if (matched != null && (matched == sequence || matched == ds)
+              && (m.getEnd() >= start) && (m.getStart() <= end))
       {
         return true;
       }
index 2dd9cf0..6eeba2f 100755 (executable)
  */
 package jalview.datamodel;
 
-import jalview.datamodel.features.FeatureAttributeType;
-import jalview.datamodel.features.FeatureAttributes;
-import jalview.datamodel.features.FeatureLocationI;
-import jalview.datamodel.features.FeatureSourceI;
-import jalview.datamodel.features.FeatureSources;
-import jalview.util.StringUtils;
-
 import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -35,6 +28,13 @@ import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.Vector;
 
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.datamodel.features.FeatureLocationI;
+import jalview.datamodel.features.FeatureSourceI;
+import jalview.datamodel.features.FeatureSources;
+import jalview.util.StringUtils;
+
 /**
  * A class that models a single contiguous feature on a sequence. If flag
  * 'contactFeature' is true, the start and end positions are interpreted instead
@@ -586,13 +586,17 @@ public class SequenceFeature implements FeatureLocationI
   }
 
   /**
-   * Answers an html-formatted report of feature details
+   * Answers an html-formatted report of feature details. If parameter
+   * {@code mf} is not null, the feature is a virtual linked feature, and
+   * details included both the original location and the mapped location
+   * (CDS/peptide).
    * 
    * @param seqName
+   * @param mf
    * 
    * @return
    */
-  public String getDetailsReport(String seqName)
+  public String getDetailsReport(String seqName, MappedFeatures mf)
   {
     FeatureSourceI metadata = FeatureSources.getInstance()
             .getSource(source);
@@ -600,9 +604,26 @@ public class SequenceFeature implements FeatureLocationI
     StringBuilder sb = new StringBuilder(128);
     sb.append("<br>");
     sb.append("<table>");
-    sb.append(String.format(ROW_DATA, "Location", seqName,
+    String name = mf == null ? seqName : mf.getLinkedSequenceName();
+    sb.append(String.format(ROW_DATA, "Location", name,
             begin == end ? begin
                     : begin + (isContactFeature() ? ":" : "-") + end));
+
+    String consequence = "";
+    if (mf != null)
+    {
+      int[] beginRange = mf.getMappedPositions(begin, begin);
+      int[] endRange = mf.getMappedPositions(end, end);
+      int from = beginRange[0];
+      int to = endRange[endRange.length - 1];
+      String s = mf.isFromCds() ? "Peptide Location" : "Coding location";
+      sb.append(String.format(ROW_DATA, s, seqName, from == to ? from
+              : from + (isContactFeature() ? ":" : "-") + to));
+      if (mf.isFromCds())
+      {
+        consequence = mf.findProteinVariants(this);
+      }
+    }
     sb.append(String.format(ROW_DATA, "Type", type, ""));
     String desc = StringUtils.stripHtmlTags(description);
     sb.append(String.format(ROW_DATA, "Description", desc, ""));
@@ -615,6 +636,12 @@ public class SequenceFeature implements FeatureLocationI
       sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
     }
 
+    if (!consequence.isEmpty())
+    {
+      sb.append(String.format(ROW_DATA, "Consequence",
+              "<i>Translated by Jalview</i>", consequence));
+    }
+
     if (otherDetails != null)
     {
       TreeMap<String, Object> ordered = new TreeMap<>(
index fd3069d..a05a2fb 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 /**
index e9a5ece..bcf404b 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import java.util.ArrayList;
index f844141..e9fb9b2 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
index f1f8585..9a18808 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
index b51f2f0..3743278 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
index 90c2986..989424c 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
index a1be1dc..176ab02 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import java.util.HashMap;
index c873593..6c5fc99 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 public interface FeatureSourceI
index 1be1b82..b316821 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.datamodel.features;
 
 import java.util.HashMap;
index 54c0d59..7651016 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.datamodel.features;
 
-import jalview.datamodel.SequenceFeature;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -30,7 +28,9 @@ import java.util.Set;
 
 import intervalstore.api.IntervalStoreI;
 import intervalstore.impl.BinarySearcher;
+import intervalstore.impl.BinarySearcher.Compare;
 import intervalstore.impl.IntervalStore;
+import jalview.datamodel.SequenceFeature;
 
 /**
  * A data store for a set of sequence features that supports efficient lookup of
@@ -277,7 +277,7 @@ public class FeatureStore
      * binary search the sorted list to find the insertion point
      */
     int insertPosition = BinarySearcher.findFirst(contactFeatureStarts,
-            f -> f.getBegin() >= feature.getBegin());
+            true, Compare.GE, feature.getBegin());
     contactFeatureStarts.add(insertPosition, feature);
 
 
@@ -286,7 +286,7 @@ public class FeatureStore
      * binary search the sorted list to find the insertion point
      */
     insertPosition = BinarySearcher.findFirst(contactFeatureEnds,
-            f -> f.getEnd() >= feature.getEnd());
+            false, Compare.GE, feature.getEnd());
     contactFeatureEnds.add(insertPosition, feature);
 
     return true;
@@ -314,8 +314,8 @@ public class FeatureStore
      */
     // int pos = binarySearch(features,
     // SearchCriterion.byFeature(feature, RangeComparator.BY_START_POSITION));
-    int pos = BinarySearcher.findFirst(features,
-            val -> val.getBegin() >= feature.getBegin());
+    int pos = BinarySearcher.findFirst(features, true, Compare.GE,
+            feature.getBegin());
     int len = features.size();
     while (pos < len)
     {
@@ -396,7 +396,7 @@ public class FeatureStore
      * whose end point is not before the target range
      */
     int index = BinarySearcher.findFirst(contactFeatureEnds,
-            f -> f.getEnd() >= from);
+            false, Compare.GE, (int) from);
 
     while (index < contactFeatureEnds.size())
     {
@@ -449,7 +449,7 @@ public class FeatureStore
           List<SequenceFeature> result)
   {
     int index = BinarySearcher.findFirst(contactFeatureStarts,
-            f -> f.getBegin() >= from);
+            true, Compare.GE, (int) from);
 
     while (index < contactFeatureStarts.size())
     {
index db2f0e1..8ac4991 100644 (file)
  */
 package jalview.datamodel.features;
 
-import jalview.datamodel.SequenceFeature;
-import jalview.io.gff.SequenceOntologyFactory;
-import jalview.io.gff.SequenceOntologyI;
-
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -33,6 +30,9 @@ import java.util.Set;
 import java.util.TreeMap;
 
 import intervalstore.api.IntervalI;
+import jalview.datamodel.SequenceFeature;
+import jalview.io.gff.SequenceOntologyFactory;
+import jalview.io.gff.SequenceOntologyI;
 
 /**
  * A class that stores sequence features in a way that supports efficient
@@ -44,7 +44,6 @@ import intervalstore.api.IntervalI;
  */
 public class SequenceFeatures implements SequenceFeaturesI
 {
-
   /*
    * map from feature type to structured store of features for that type
    * null types are permitted (but not a good idea!)
@@ -414,7 +413,10 @@ public class SequenceFeatures implements SequenceFeaturesI
   public static void sortFeatures(List<? extends IntervalI> features,
           final boolean forwardStrand)
   {
-    IntervalI.sortIntervals(features, forwardStrand);
+    Collections.sort(features,
+            forwardStrand
+                    ? IntervalI.COMPARE_BEGIN_ASC_END_DESC
+                    : IntervalI.COMPARE_END_DESC);
   }
 
   /**
index 26bd142..d383409 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.android;
 
 /*
index eaf059c..ea6265f 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.android;
 
 /*
index fcd4f1f..d67121b 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.android;
 
 /*
index f961f55..6cd8019 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.android;
 
 /*
index 97a8e74..97ad242 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
index e9cc9e1..cfc679c 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.ensembl;
 
 import jalview.datamodel.AlignmentI;
index 2859e0f..16ff41b 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.ext.htsjdk;
 
 import jalview.bin.Cache;
index 453152e..eee48df 100644 (file)
  */
 package jalview.ext.jmol;
 
-import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureRenderer;
-import jalview.api.SequenceRenderer;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.gui.IProgressIndicator;
-import jalview.io.DataSourceType;
-import jalview.io.StructureFile;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.ResidueProperties;
-import jalview.structure.AtomSpec;
-import jalview.structure.StructureMappingcommandSet;
-import jalview.structure.StructureSelectionManager;
-import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.MessageManager;
-
-import java.awt.Color;
 import java.awt.Container;
 import java.awt.event.ComponentEvent;
 import java.awt.event.ComponentListener;
 import java.io.File;
 import java.net.URL;
 import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
 import java.util.StringTokenizer;
@@ -58,50 +37,53 @@ import org.jmol.api.JmolSelectionListener;
 import org.jmol.api.JmolStatusListener;
 import org.jmol.api.JmolViewer;
 import org.jmol.c.CBK;
-import org.jmol.script.T;
 import org.jmol.viewer.Viewer;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.api.SequenceRenderer;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.gui.IProgressIndicator;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
+import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureSelectionManager;
+import jalview.structures.models.AAStructureBindingModel;
+import javajs.util.BS;
+
 public abstract class JalviewJmolBinding extends AAStructureBindingModel
         implements JmolStatusListener, JmolSelectionListener,
         ComponentListener
 {
   private String lastMessage;
 
-  boolean allChainsSelected = false;
-
   /*
    * when true, try to search the associated datamodel for sequences that are
    * associated with any unknown structures in the Jmol view.
    */
   private boolean associateNewStructs = false;
 
-  Vector<String> atomsPicked = new Vector<>();
-
-  private List<String> chainNames;
-
-  Hashtable<String, String> chainFile;
-
-  /*
-   * the default or current model displayed if the model cannot be identified
-   * from the selection message
-   */
-  int frameNo = 0;
-
-  // protected JmolGenericPopup jmolpopup; // not used - remove?
+  private Vector<String> atomsPicked = new Vector<>();
 
-  String lastCommand;
+  private String lastCommand;
 
-  boolean loadedInline;
+  private boolean loadedInline;
 
-  StringBuffer resetLastRes = new StringBuffer();
+  private StringBuffer resetLastRes = new StringBuffer();
 
-  public Viewer viewer;
+  public Viewer jmolViewer;
 
   public JalviewJmolBinding(StructureSelectionManager ssm,
           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
           DataSourceType protocol)
   {
     super(ssm, pdbentry, sequenceIs, protocol);
+    setStructureCommands(new JmolCommands());
     /*
      * viewer = JmolViewer.allocateViewer(renderPanel, new SmarterJmolAdapter(),
      * "jalviewJmol", ap.av.applet .getDocumentBase(),
@@ -116,9 +98,10 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   {
     super(ssm, seqs);
 
-    viewer = theViewer;
-    viewer.setJmolStatusListener(this);
-    viewer.addSelectionListener(this);
+    jmolViewer = theViewer;
+    jmolViewer.setJmolStatusListener(this);
+    jmolViewer.addSelectionListener(this);
+    setStructureCommands(new JmolCommands());
   }
 
   /**
@@ -132,409 +115,32 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return getViewerTitle("Jmol", true);
   }
 
-  /**
-   * prepare the view for a given set of models/chains. chainList contains
-   * strings of the form 'pdbfilename:Chaincode'
-   * 
-   * @param chainList
-   *          list of chains to make visible
-   */
-  public void centerViewer(Vector<String> chainList)
-  {
-    StringBuilder cmd = new StringBuilder(128);
-    int mlength, p;
-    for (String lbl : chainList)
-    {
-      mlength = 0;
-      do
-      {
-        p = mlength;
-        mlength = lbl.indexOf(":", p);
-      } while (p < mlength && mlength < (lbl.length() - 2));
-      // TODO: lookup each pdb id and recover proper model number for it.
-      cmd.append(":" + lbl.substring(mlength + 1) + " /"
-              + (1 + getModelNum(chainFile.get(lbl))) + " or ");
-    }
-    if (cmd.length() > 0)
-    {
-      cmd.setLength(cmd.length() - 4);
-    }
-    evalStateCommand("select *;restrict " + cmd + ";cartoon;center " + cmd);
-  }
-
-  public void closeViewer()
-  {
-    // remove listeners for all structures in viewer
-    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
-    if (viewer != null)
-    {
-      viewer.dispose();
-    }
-    lastCommand = null;
-    viewer = null;
-    releaseUIResources();
-  }
-
-  @Override
-  public void colourByChain()
-  {
-    colourBySequence = false;
-    // TODO: colour by chain should colour each chain distinctly across all
-    // visible models
-    // TODO: http://issues.jalview.org/browse/JAL-628
-    evalStateCommand("select *;color chain");
-  }
-
-  @Override
-  public void colourByCharge()
-  {
-    colourBySequence = false;
-    evalStateCommand("select *;color white;select ASP,GLU;color red;"
-            + "select LYS,ARG;color blue;select CYS;color yellow");
-  }
-
-  /**
-   * superpose the structures associated with sequences in the alignment
-   * according to their corresponding positions.
-   */
-  public void superposeStructures(AlignmentI alignment)
-  {
-    superposeStructures(alignment, -1, null);
-  }
-
-  /**
-   * superpose the structures associated with sequences in the alignment
-   * according to their corresponding positions. ded)
-   * 
-   * @param refStructure
-   *          - select which pdb file to use as reference (default is -1 - the
-   *          first structure in the alignment)
-   */
-  public void superposeStructures(AlignmentI alignment, int refStructure)
+  private String jmolScript(String script)
   {
-    superposeStructures(alignment, refStructure, null);
-  }
+    Cache.log.debug(">>Jmol>> " + script);
+    String s = jmolViewer.evalStringQuiet(script); // scriptWait(script); BH
+    Cache.log.debug("<<Jmol<< " + s);
 
-  /**
-   * superpose the structures associated with sequences in the alignment
-   * according to their corresponding positions. ded)
-   * 
-   * @param refStructure
-   *          - select which pdb file to use as reference (default is -1 - the
-   *          first structure in the alignment)
-   * @param hiddenCols
-   *          TODO
-   */
-  public void superposeStructures(AlignmentI alignment, int refStructure,
-          HiddenColumns hiddenCols)
-  {
-    superposeStructures(new AlignmentI[] { alignment },
-            new int[]
-            { refStructure }, new HiddenColumns[] { hiddenCols });
+    return s;
   }
 
-  /**
-   * {@inheritDoc}
-   */
   @Override
-  public String superposeStructures(AlignmentI[] _alignment,
-          int[] _refStructure, HiddenColumns[] _hiddenCols)
+  public List<String> executeCommand(StructureCommandI command,
+          boolean getReply)
   {
-    while (viewer.isScriptExecuting())
-    {
-      try
-      {
-        Thread.sleep(10);
-      } catch (InterruptedException i)
-      {
-      }
-    }
-
-    /*
-     * get the distinct structure files modelled
-     * (a file with multiple chains may map to multiple sequences)
-     */
-    String[] files = getStructureFiles();
-    if (!waitForFileLoad(files))
+    if (command == null)
     {
       return null;
     }
-
-    StringBuilder selectioncom = new StringBuilder(256);
-    // In principle - nSeconds specifies the speed of animation for each
-    // superposition - but is seems to behave weirdly, so we don't specify it.
-    String nSeconds = " ";
-    if (files.length > 10)
-    {
-      nSeconds = " 0.005 ";
-    }
-    else
-    {
-      nSeconds = " " + (2.0 / files.length) + " ";
-      // if (nSeconds).substring(0,5)+" ";
-    }
-
-    // see JAL-1345 - should really automatically turn off the animation for
-    // large numbers of structures, but Jmol doesn't seem to allow that.
-    // nSeconds = " ";
-    // union of all aligned positions are collected together.
-    for (int a = 0; a < _alignment.length; a++)
-    {
-      int refStructure = _refStructure[a];
-      AlignmentI alignment = _alignment[a];
-      HiddenColumns hiddenCols = _hiddenCols[a];
-      if (a > 0 && selectioncom.length() > 0 && !selectioncom
-              .substring(selectioncom.length() - 1).equals("|"))
-      {
-        selectioncom.append("|");
-      }
-      // process this alignment
-      if (refStructure >= files.length)
-      {
-        System.err.println(
-                "Invalid reference structure value " + refStructure);
-        refStructure = -1;
-      }
-
-      /*
-       * 'matched' bit j will be set for visible alignment columns j where
-       * all sequences have a residue with a mapping to the PDB structure
-       */
-      BitSet matched = new BitSet();
-      for (int m = 0; m < alignment.getWidth(); m++)
-      {
-        if (hiddenCols == null || hiddenCols.isVisible(m))
-        {
-          matched.set(m);
-        }
-      }
-
-      SuperposeData[] structures = new SuperposeData[files.length];
-      for (int f = 0; f < files.length; f++)
-      {
-        structures[f] = new SuperposeData(alignment.getWidth());
-      }
-
-      /*
-       * Calculate the superposable alignment columns ('matched'), and the
-       * corresponding structure residue positions (structures.pdbResNo)
-       */
-      int candidateRefStructure = findSuperposableResidues(alignment,
-              matched, structures);
-      if (refStructure < 0)
-      {
-        /*
-         * If no reference structure was specified, pick the first one that has
-         * a mapping in the alignment
-         */
-        refStructure = candidateRefStructure;
-      }
-
-      String[] selcom = new String[files.length];
-      int nmatched = matched.cardinality();
-      if (nmatched < 4)
-      {
-        return (MessageManager.formatMessage("label.insufficient_residues",
-                nmatched));
-      }
-
-      /*
-       * generate select statements to select regions to superimpose structures
-       */
-      {
-        // TODO extract method to construct selection statements
-        for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-        {
-          String chainCd = ":" + structures[pdbfnum].chain;
-          int lpos = -1;
-          boolean run = false;
-          StringBuilder molsel = new StringBuilder();
-          molsel.append("{");
-
-          int nextColumnMatch = matched.nextSetBit(0);
-          while (nextColumnMatch != -1)
-          {
-            int pdbResNo = structures[pdbfnum].pdbResNo[nextColumnMatch];
-            if (lpos != pdbResNo - 1)
-            {
-              // discontinuity
-              if (lpos != -1)
-              {
-                molsel.append(lpos);
-                molsel.append(chainCd);
-                molsel.append("|");
-              }
-              run = false;
-            }
-            else
-            {
-              // continuous run - and lpos >-1
-              if (!run)
-              {
-                // at the beginning, so add dash
-                molsel.append(lpos);
-                molsel.append("-");
-              }
-              run = true;
-            }
-            lpos = pdbResNo;
-            nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
-          }
-          /*
-           * add final selection phrase
-           */
-          if (lpos != -1)
-          {
-            molsel.append(lpos);
-            molsel.append(chainCd);
-            molsel.append("}");
-          }
-          if (molsel.length() > 1)
-          {
-            selcom[pdbfnum] = molsel.toString();
-            selectioncom.append("((");
-            selectioncom.append(selcom[pdbfnum].substring(1,
-                    selcom[pdbfnum].length() - 1));
-            selectioncom.append(" )& ");
-            selectioncom.append(pdbfnum + 1);
-            selectioncom.append(".1)");
-            if (pdbfnum < files.length - 1)
-            {
-              selectioncom.append("|");
-            }
-          }
-          else
-          {
-            selcom[pdbfnum] = null;
-          }
-        }
-      }
-      StringBuilder command = new StringBuilder(256);
-      // command.append("set spinFps 10;\n");
-
-      for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-      {
-        if (pdbfnum == refStructure || selcom[pdbfnum] == null
-                || selcom[refStructure] == null)
-        {
-          continue;
-        }
-        command.append("echo ");
-        command.append("\"Superposing (");
-        command.append(structures[pdbfnum].pdbId);
-        command.append(") against reference (");
-        command.append(structures[refStructure].pdbId);
-        command.append(")\";\ncompare " + nSeconds);
-        command.append("{");
-        command.append(Integer.toString(1 + pdbfnum));
-        command.append(".1} {");
-        command.append(Integer.toString(1 + refStructure));
-        // conformation=1 excludes alternate locations for CA (JAL-1757)
-        command.append(
-                ".1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS ");
-
-        // for (int s = 0; s < 2; s++)
-        // {
-        // command.append(selcom[(s == 0 ? pdbfnum : refStructure)]);
-        // }
-        command.append(selcom[pdbfnum]);
-        command.append(selcom[refStructure]);
-        command.append(" ROTATE TRANSLATE;\n");
-      }
-      if (selectioncom.length() > 0)
-      {
-        // TODO is performing selectioncom redundant here? is done later on
-        // System.out.println("Select regions:\n" + selectioncom.toString());
-        evalStateCommand("select *; cartoons off; backbone; select ("
-                + selectioncom.toString() + "); cartoons; ");
-        // selcom.append("; ribbons; ");
-        String cmdString = command.toString();
-        // System.out.println("Superimpose command(s):\n" + cmdString);
-
-        evalStateCommand(cmdString);
-      }
-    }
-    if (selectioncom.length() > 0)
-    {// finally, mark all regions that were superposed.
-      if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
-      {
-        selectioncom.setLength(selectioncom.length() - 1);
-      }
-      // System.out.println("Select regions:\n" + selectioncom.toString());
-      evalStateCommand("select *; cartoons off; backbone; select ("
-              + selectioncom.toString() + "); cartoons; ");
-      // evalStateCommand("select *; backbone; select "+selcom.toString()+";
-      // cartoons; center "+selcom.toString());
-    }
-
-    return null;
-  }
-
-  public void evalStateCommand(String command)
-  {
+    String cmd = command.getCommand();
     jmolHistory(false);
-    if (lastCommand == null || !lastCommand.equals(command))
+    if (lastCommand == null || !lastCommand.equals(cmd))
     {
-      jmolScript(command + "\n");
+      jmolScript(cmd + "\n");
     }
     jmolHistory(true);
-    lastCommand = command;
-  }
-
-  Thread colourby = null;
-
-  /**
-   * Sends a set of colour commands to the structure viewer
-   * 
-   * @param colourBySequenceCommands
-   */
-  @Override
-  protected void colourBySequence(
-          final StructureMappingcommandSet[] colourBySequenceCommands)
-  {
-    if (colourby != null)
-    {
-      colourby.interrupt();
-      colourby = null;
-    }
-    Thread colourby = new Thread(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
-        {
-          for (String cbyseq : cpdbbyseq.commands)
-          {
-            executeWhenReady(cbyseq);
-          }
-        }
-      }
-    });
-    colourby.start();
-    this.colourby = colourby;
-  }
-
-  /**
-   * @param files
-   * @param sr
-   * @param viewPanel
-   * @return
-   */
-  @Override
-  protected StructureMappingcommandSet[] getColourBySequenceCommands(
-          String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
-  {
-    return JmolCommands.getColourBySequenceCommand(getSsm(), files,
-            getSequence(), sr, viewPanel);
-  }
-
-  /**
-   * @param command
-   */
-  protected void executeWhenReady(String command)
-  {
-    evalStateCommand(command);
+    lastCommand = cmd;
+    return null;
   }
 
   public void createImage(String file, String type, int quality)
@@ -575,43 +181,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return null;
   }
 
-  public Color getColour(int atomIndex, int pdbResNum, String chain,
-          String pdbfile)
-  {
-    if (getModelNum(pdbfile) < 0)
-    {
-      return null;
-    }
-    // TODO: verify atomIndex is selecting correct model.
-    // return new Color(viewer.getAtomArgb(atomIndex)); Jmol 12.2.4
-    int colour = viewer.ms.at[atomIndex].atomPropertyInt(T.color);
-    return new Color(colour);
-  }
-
-  /**
-   * instruct the Jalview binding to update the pdbentries vector if necessary
-   * prior to matching the jmol view's contents to the list of structure files
-   * Jalview knows about.
-   */
-  public abstract void refreshPdbEntries();
-
-  private int getModelNum(String modelFileName)
-  {
-    String[] mfn = getStructureFiles();
-    if (mfn == null)
-    {
-      return -1;
-    }
-    for (int i = 0; i < mfn.length; i++)
-    {
-      if (mfn[i].equalsIgnoreCase(modelFileName))
-      {
-        return i;
-      }
-    }
-    return -1;
-  }
-
   /**
    * map between index of model filename returned from getPdbFile and the first
    * index of models from this file in the viewer. Note - this is not trimmed -
@@ -622,25 +191,32 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   @Override
   public synchronized String[] getStructureFiles()
   {
-    List<String> mset = new ArrayList<>();
-    if (viewer == null)
+    if (jmolViewer == null)
     {
       return new String[0];
     }
 
     if (modelFileNames == null)
     {
-      int modelCount = viewer.ms.mc;
+      int modelCount = jmolViewer.ms.mc;
       String filePath = null;
+      List<String> mset = new ArrayList<>();
       for (int i = 0; i < modelCount; ++i)
       {
-        filePath = viewer.ms.getModelFileName(i);
-        if (!mset.contains(filePath))
+        /*
+         * defensive check for null as getModelFileName can return null
+         * even when model count ms.mc is > 0
+         */
+        filePath = jmolViewer.ms.getModelFileName(i);
+        if (filePath != null && !mset.contains(filePath))
         {
           mset.add(filePath);
         }
       }
-      modelFileNames = mset.toArray(new String[mset.size()]);
+      if (!mset.isEmpty())
+      {
+        modelFileNames = mset.toArray(new String[mset.size()]);
+      }
     }
 
     return modelFileNames;
@@ -690,55 +266,35 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   public void highlightAtom(int atomIndex, int pdbResNum, String chain,
           String pdbfile)
   {
-    if (modelFileNames == null)
-    {
-      return;
-    }
-
-    // look up file model number for this pdbfile
-    int mdlNum = 0;
-    // may need to adjust for URLencoding here - we don't worry about that yet.
-    while (mdlNum < modelFileNames.length
-            && !pdbfile.equals(modelFileNames[mdlNum]))
-    {
-      mdlNum++;
-    }
-    if (mdlNum == modelFileNames.length)
+    String modelId = getModelIdForFile(pdbfile);
+    if (modelId.isEmpty())
     {
       return;
     }
 
     jmolHistory(false);
 
+    StringBuilder selection = new StringBuilder(32);
     StringBuilder cmd = new StringBuilder(64);
-    cmd.append("select " + pdbResNum); // +modelNum
-
-    resetLastRes.append("select " + pdbResNum); // +modelNum
-
-    cmd.append(":");
-    resetLastRes.append(":");
+    selection.append("select ").append(String.valueOf(pdbResNum));
+    selection.append(":");
     if (!chain.equals(" "))
     {
-      cmd.append(chain);
-      resetLastRes.append(chain);
-    }
-    {
-      cmd.append(" /" + (mdlNum + 1));
-      resetLastRes.append("/" + (mdlNum + 1));
+      selection.append(chain);
     }
-    cmd.append(";wireframe 100;" + cmd.toString() + " and not hetero;");
+    selection.append(" /").append(modelId);
 
-    resetLastRes.append(";wireframe 0;" + resetLastRes.toString()
-            + " and not hetero; spacefill 0;");
+    cmd.append(selection).append(";wireframe 100;").append(selection)
+            .append(" and not hetero;").append("spacefill 200;select none");
 
-    cmd.append("spacefill 200;select none");
+    resetLastRes.append(selection).append(";wireframe 0;").append(selection)
+            .append(" and not hetero; spacefill 0;");
 
     jmolScript(cmd.toString());
     jmolHistory(true);
-
   }
 
-  boolean debug = true;
+  private boolean debug = true;
 
   private void jmolHistory(boolean enable)
   {
@@ -756,7 +312,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     // Then, construct pass a reader for the string to Jmol.
     // ((org.jmol.Viewer.Viewer) viewer).loadModelFromFile(fullPathName,
     // fileName, null, reader, false, null, null, 0);
-    viewer.openStringInline(string);
+    jmolViewer.openStringInline(string);
   }
 
   protected void mouseOverStructure(int atomIndex, final String strInfo)
@@ -799,8 +355,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       chainId = " ";
     }
 
-    String pdbfilename = modelFileNames[frameNo]; // default is first or current
-    // model
+    String pdbfilename = modelFileNames[0]; // default is first model
     if (mdlSep > -1)
     {
       if (chainSeparator1 == -1)
@@ -833,7 +388,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
           if (pdbfilename == null)
           {
-            pdbfilename = new File(viewer.ms.getModelFileName(mnumber))
+            pdbfilename = new File(jmolViewer.ms.getModelFileName(mnumber))
                     .getAbsolutePath();
           }
         }
@@ -859,7 +414,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       sb.append(";set hoverLabel \"").append(toks.nextToken()).append(" ")
               .append(toks.nextToken());
       sb.append("|").append(label).append("\"");
-      evalStateCommand(sb.toString());
+      executeCommand(new StructureCommand(sb.toString()), false);
     }
   }
 
@@ -925,7 +480,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     }
     else
     {
-      viewer.evalString("select " + picked + ";label off");
+      jmolViewer.evalString("select " + picked + ";label off");
       atomsPicked.removeElement(picked);
     }
     jmolHistory(true);
@@ -1041,10 +596,13 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     fileLoadingError = null;
     String[] oldmodels = modelFileNames;
     modelFileNames = null;
-    chainNames = new ArrayList<>();
-    chainFile = new Hashtable<>();
     boolean notifyLoaded = false;
     String[] modelfilenames = getStructureFiles();
+    if (modelfilenames == null)
+    {
+      // Jmol is still loading files!
+      return;
+    }
     // first check if we've lost any structures
     if (oldmodels != null && oldmodels.length > 0)
     {
@@ -1093,7 +651,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         // calculate essential attributes for the pdb data imported inline.
         // prolly need to resolve modelnumber properly - for now just use our
         // 'best guess'
-        pdbfile = viewer.getData(
+        pdbfile = jmolViewer.getData(
                 "" + (1 + _modelFileNameMap[modelnum]) + ".0", "PDB");
       }
       // search pdbentries and sequences to find correct pdbentry for this
@@ -1147,14 +705,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         }
         if (matches)
         {
-          // add an entry for every chain in the model
-          for (int i = 0; i < pdb.getChains().size(); i++)
-          {
-            String chid = new String(
-                    pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
-            chainFile.put(chid, fileName);
-            chainNames.add(chid);
-          }
+          stashFoundChains(pdb, fileName);
           notifyLoaded = true;
         }
       }
@@ -1164,7 +715,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         // this is a foreign pdb file that jalview doesn't know about - add
         // it to the dataset and try to find a home - either on a matching
         // sequence or as a new sequence.
-        String pdbcontent = viewer.getData("/" + (modelnum + 1) + ".1",
+        String pdbcontent = jmolViewer.getData("/" + (modelnum + 1) + ".1",
                 "PDB");
         // parse pdb file into a chain, etc.
         // locate best match for pdb in associated views and add mapping to
@@ -1202,12 +753,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     setLoadingFromArchive(false);
   }
 
-  @Override
-  public List<String> getChainNames()
-  {
-    return chainNames;
-  }
-
   protected IProgressIndicator getIProgressIndicator()
   {
     return null;
@@ -1252,35 +797,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
 
   }
 
-  @Override
-  public void setJalviewColourScheme(ColourSchemeI cs)
-  {
-    colourBySequence = false;
-
-    if (cs == null)
-    {
-      return;
-    }
-
-    jmolHistory(false);
-    StringBuilder command = new StringBuilder(128);
-    command.append("select *;color white;");
-    List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
-            false);
-    for (String resName : residueSet)
-    {
-      char res = resName.length() == 3
-              ? ResidueProperties.getSingleCharacterCode(resName)
-              : resName.charAt(0);
-      Color col = cs.findColour(res, 0, null, null, 0f);
-      command.append("select " + resName + ";color[" + col.getRed() + ","
-              + col.getGreen() + "," + col.getBlue() + "];");
-    }
-
-    evalStateCommand(command.toString());
-    jmolHistory(true);
-  }
-
   public void showHelp()
   {
     showUrl("http://wiki.jmol.org"
@@ -1296,13 +812,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   public abstract void showUrl(String url, String target);
 
   /**
-   * called when the binding thinks the UI needs to be refreshed after a Jmol
-   * state change. this could be because structures were loaded, or because an
-   * error has occured.
-   */
-  public abstract void refreshGUI();
-
-  /**
    * called to show or hide the associated console window container.
    * 
    * @param show
@@ -1363,12 +872,12 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     {
       commandOptions = "";
     }
-    viewer = (Viewer) JmolViewer.allocateViewer(renderPanel,
+    jmolViewer = (Viewer) JmolViewer.allocateViewer(renderPanel,
             (jmolfileio ? new SmarterJmolAdapter() : null),
             htmlName + ((Object) this).toString(), documentBase, codeBase,
             commandOptions, this);
 
-    viewer.setJmolStatusListener(this); // extends JmolCallbackListener
+    jmolViewer.setJmolStatusListener(this); // extends JmolCallbackListener
 
     try
     {
@@ -1396,27 +905,6 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   protected org.jmol.api.JmolAppConsoleInterface console = null;
 
   @Override
-  public void setBackgroundColour(java.awt.Color col)
-  {
-    jmolHistory(false);
-    jmolScript("background [" + col.getRed() + "," + col.getGreen() + ","
-            + col.getBlue() + "];");
-    jmolHistory(true);
-  }
-
-  private String jmolScript(String script)
-  {
-
-    System.err.println(">>Jmol>> " + script);
-
-    String s = viewer.scriptWait(script);
-
-    System.err.println("<<Jmol<< " + s);
-
-    return s;
-  }
-
-  @Override
   public int[] resizeInnerPanel(String data)
   {
     // Jalview doesn't honour resize panel requests
@@ -1477,4 +965,63 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     showConsole(false);
   }
 
+  @Override
+  protected String getModelIdForFile(String pdbFile)
+  {
+    if (modelFileNames == null)
+    {
+      return "";
+    }
+    for (int i = 0; i < modelFileNames.length; i++)
+    {
+      if (modelFileNames[i].equalsIgnoreCase(pdbFile))
+      {
+        return String.valueOf(i + 1);
+      }
+    }
+    return "";
+  }
+
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.JMOL;
+  }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum + 1);
+  }
+
+  /**
+   * Returns ".spt" - the Jmol session file extension
+   * 
+   * @return
+   * @see https://chemapps.stolaf.edu/jmol/docs/#writemodel
+   */
+  @Override
+  public String getSessionFileExtension()
+  {
+    return ".spt";
+  }
+
+  @Override
+  public void selectionChanged(BS arg0)
+  {
+    // TODO Auto-generated method stub
+
+  }
+
+  @Override
+  public SequenceRenderer getSequenceRenderer(AlignmentViewPanel avp)
+  {
+    return new jalview.gui.SequenceRenderer(avp.getAlignViewport());
+  }
+
+  @Override
+  public String getHelpURL()
+  {
+    return "http://wiki.jmol.org"; // BH 2018
+  }
 }
index 603202a..25f6aec 100644 (file)
  */
 package jalview.ext.jmol;
 
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureRenderer;
@@ -28,49 +34,299 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 import jalview.renderer.seqfeatures.FeatureColourFinder;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsBase;
 import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
-
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.List;
+import jalview.util.Comparison;
+import jalview.util.Platform;
 
 /**
- * Routines for generating Jmol commands for Jalview/Jmol binding another
- * cruisecontrol test.
+ * Routines for generating Jmol commands for Jalview/Jmol binding
  * 
  * @author JimP
  * 
  */
-public class JmolCommands
+public class JmolCommands extends StructureCommandsBase
 {
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "select *; cartoons off; backbone");
+
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand("zoom 0");
+
+  private static final StructureCommand COLOUR_ALL_WHITE = new StructureCommand(
+          "select *;color white;");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "select *;color white;select ASP,GLU;color red;"
+                  + "select LYS,ARG;color blue;select CYS;color yellow");
+
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "select *;color chain");
+
+  private static final String PIPE = "|";
+
+  private static final String HYPHEN = "-";
+
+  private static final String COLON = ":";
+
+  private static final String SLASH = "/";
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
+  }
+
+  /**
+   * Returns a string representation of the given colour suitable for inclusion
+   * in Jmol commands
+   * 
+   * @param c
+   * @return
+   */
+  protected String getColourString(Color c)
+  {
+    return c == null ? null
+            : String.format("[%d,%d,%d]", c.getRed(), c.getGreen(),
+                    c.getBlue());
+  }
+
+  @Override
+  public StructureCommandI colourByChain()
+  {
+    return COLOUR_BY_CHAIN;
+  }
+
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
+  }
+
+  @Override
+  public List<StructureCommandI> colourByResidues(Map<String, Color> colours)
+  {
+    List<StructureCommandI> cmds = super.colourByResidues(colours);
+    cmds.add(0, COLOUR_ALL_WHITE);
+    return cmds;
+  }
+
+  @Override
+  public StructureCommandI setBackgroundColour(Color col)
+  {
+    return new StructureCommand("background " + getColourString(col));
+  }
+
+  @Override
+  public StructureCommandI focusView()
+  {
+    return FOCUS_VIEW;
+  }
+
+  @Override
+  public List<StructureCommandI> showChains(List<String> toShow)
+  {
+    StringBuilder atomSpec = new StringBuilder(128);
+    boolean first = true;
+    for (String chain : toShow)
+    {
+      String[] tokens = chain.split(":");
+      if (tokens.length == 2)
+      {
+        if (!first)
+        {
+          atomSpec.append(" or ");
+        }
+        first = false;
+        atomSpec.append(":").append(tokens[1]).append(" /").append(tokens[0]);
+      }
+    }
+
+    String spec = atomSpec.toString();
+    String command = "select *;restrict " + spec + ";cartoon;center "
+            + spec;
+    return Arrays.asList(new StructureCommand(command));
+  }
 
   /**
-   * Jmol utility which constructs the commands to colour chains by the given
-   * alignment
+   * Returns a command to superpose atoms in {@code atomSpec} to those in
+   * {@code refAtoms}, restricted to alpha carbons only (Phosphorous for rna).
+   * For example
    * 
-   * @returns Object[] { Object[] { <model being coloured>,
+   * <pre>
+   * compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} 
+   *         ATOMS {1-87:A}{2-54:A|61-94:A} ROTATE TRANSLATE 1.0;
+   * </pre>
    * 
+   * where {@code conformation=1} excludes ALTLOC atom locations, and 1.0 is the
+   * time in seconds to animate the action. For this example, atoms in model 2
+   * are moved towards atoms in model 1.
+   * <p>
+   * The two atomspecs should each be for one model only, but may have more than
+   * one chain. The number of atoms specified should be the same for both
+   * models, though if not, Jmol may make a 'best effort' at superposition.
+   * 
+   * @see https://chemapps.stolaf.edu/jmol/docs/#compare
    */
-  public static StructureMappingcommandSet[] getColourBySequenceCommand(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
+  @Override
+  public List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    String refModel = refAtoms.getModels().iterator().next();
+    String model2 = atomSpec.getModels().iterator().next();
+    sb.append(String.format("compare {%s.1} {%s.1}", model2, refModel));
+    sb.append(" SUBSET {(*.CA | *.P) and conformation=1} ATOMS {");
+
+    /*
+     * command examples don't include modelspec with atoms, getAtomSpec does;
+     * it works, so leave it as it is for simplicity
+     */
+    sb.append(getAtomSpec(atomSpec, true)).append("}{");
+    sb.append(getAtomSpec(refAtoms, true)).append("}");
+    sb.append(" ROTATE TRANSLATE ");
+    sb.append(getCommandSeparator());
+
+    /*
+     * show residues used for superposition as ribbon
+     */
+    sb.append("select ").append(getAtomSpec(atomSpec, false)).append("|");
+    sb.append(getAtomSpec(refAtoms, false)).append(getCommandSeparator())
+            .append("cartoons");
+
+    return Arrays.asList(new StructureCommand(sb.toString()));
+  }
+
+  @Override
+  public StructureCommandI openCommandFile(String path)
+  {
+    /*
+     * https://chemapps.stolaf.edu/jmol/docs/#script
+     * not currently used in Jalview
+     */
+    return new StructureCommand("script " + path);
+  }
+
+  @Override
+  public StructureCommandI saveSession(String filepath)
+  {
+    /*
+     * https://chemapps.stolaf.edu/jmol/docs/#writemodel
+     */
+    return new StructureCommand("write STATE \"" + filepath + "\"");
+  }
+
+  @Override
+  protected StructureCommandI colourResidues(String atomSpec, Color colour)
+  {
+    StringBuilder sb = new StringBuilder(atomSpec.length()+20);
+    sb.append("select ").append(atomSpec).append(getCommandSeparator())
+            .append("color").append(getColourString(colour));
+    return new StructureCommand(sb.toString());
+  }
+
+  @Override
+  protected String getResidueSpec(String residue)
+  {
+    return residue;
+  }
+
+  /**
+   * Generates a Jmol atomspec string like
+   * 
+   * <pre>
+   * 2-5:A/1.1,8:A/1.1,5-10:B/2.1
+   * </pre>
+   * 
+   * Parameter {@code alphaOnly} is not used here - this restriction is made by
+   * a separate clause in the {@code compare} (superposition) command.
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel model, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(128);
+
+    boolean first = true;
+    for (String modelNo : model.getModels())
+    {
+      for (String chain : model.getChains(modelNo))
+      {
+        for (int[] range : model.getRanges(modelNo, chain))
+        {
+          if (!first)
+          {
+            sb.append(PIPE);
+          }
+          first = false;
+          if (range[0] == range[1])
+          {
+            sb.append(range[0]);
+          }
+          else
+          {
+            sb.append(range[0]).append(HYPHEN).append(range[1]);
+          }
+          sb.append(COLON).append(chain.trim()).append(SLASH);
+          sb.append(String.valueOf(modelNo)).append(".1");
+        }
+      }
+    }
+
+    return sb.toString();
+  }
+
+  @Override
+  public List<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
+  {
+    // https://chemapps.stolaf.edu/jmol/docs/#loadfiles
+    return new StructureCommand("load FILES \"" + 
+            Platform.escapeBackslashes(file) + "\"");
+  }
+
+  /**
+   * Obsolete method, only referenced from
+   * jalview.javascript.MouseOverStructureListener
+   * 
+   * @param ssm
+   * @param files
+   * @param sequence
+   * @param sr
+   * @param viewPanel
+   * @return
+   */
+  @Deprecated
+  public String[] colourBySequence(StructureSelectionManager ssm,
+          String[] files, SequenceI[][] sequence, SequenceRenderer sr,
           AlignmentViewPanel viewPanel)
   {
+    // TODO delete method
+
     FeatureRenderer fr = viewPanel.getFeatureRenderer();
     FeatureColourFinder finder = new FeatureColourFinder(fr);
     AlignViewportI viewport = viewPanel.getAlignViewport();
     HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
     AlignmentI al = viewport.getAlignment();
-    List<StructureMappingcommandSet> cset = new ArrayList<StructureMappingcommandSet>();
+    List<String> cset = new ArrayList<>();
 
     for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
     {
       StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
-      StringBuilder command = new StringBuilder();
-      StructureMappingcommandSet smc;
-      ArrayList<String> str = new ArrayList<String>();
+      StringBuilder command = new StringBuilder(128);
+      List<String> str = new ArrayList<>();
 
       if (mapping == null || mapping.length < 1)
       {
@@ -89,7 +345,7 @@ public class JmolCommands
             for (int r = 0; r < asp.getLength(); r++)
             {
               // no mapping to gaps in sequence
-              if (jalview.util.Comparison.isGap(asp.getCharAt(r)))
+              if (Comparison.isGap(asp.getCharAt(r)))
               {
                 continue;
               }
@@ -125,14 +381,10 @@ public class JmolCommands
                 col = Color.GRAY;
               }
 
-              // todo JAL-3152 handle 'no chain' case without errors
-              boolean hasChain = true || mapping[m].getChain() != " ";
-                         String chainSpec = hasChain
+              String newSelcom = (mapping[m].getChain() != " "
                       ? ":" + mapping[m].getChain()
-                      : "";
-                         String newSelcom = chainSpec + "/" + (pdbfnum + 1) + ".1" + ";color["
-                      + col.getRed() + "," + col.getGreen() + ","
-                      + col.getBlue() + "]";
+                      : "") + "/" + (pdbfnum + 1) + ".1" + ";color"
+                      + getColourString(col);
               if (command.length() > newSelcom.length() && command
                       .substring(command.length() - newSelcom.length())
                       .equals(newSelcom))
@@ -167,15 +419,23 @@ public class JmolCommands
         str.add(command.toString());
         command.setLength(0);
       }
-      // Finally, add the command set ready to be returned.
-      cset.add(new StructureMappingcommandSet(JmolCommands.class,
-              files[pdbfnum], str.toArray(new String[str.size()])));
+      cset.addAll(str);
 
     }
-    return cset.toArray(new StructureMappingcommandSet[cset.size()]);
+    return cset.toArray(new String[cset.size()]);
   }
 
-  public static StringBuilder condenseCommand(StringBuilder command, int pos)
+  /**
+   * Helper method
+   * 
+   * @param command
+   * @param pos
+   * @return
+   */
+  @Deprecated
+  private static StringBuilder condenseCommand(
+          StringBuilder command,
+          int pos)
   {
 
     // work back to last 'select'
@@ -210,4 +470,15 @@ public class JmolCommands
     return sb;
   }
 
+  @Override
+  public StructureCommandI openSession(String filepath)
+  {
+    return loadFile(filepath);
+  }
+
+  @Override
+  public StructureCommandI closeViewer()
+  {
+    return null; // not an external viewer
+  }
 }
diff --git a/src/jalview/ext/pymol/PymolCommands.java b/src/jalview/ext/pymol/PymolCommands.java
new file mode 100644 (file)
index 0000000..36957f5
--- /dev/null
@@ -0,0 +1,335 @@
+package jalview.ext.pymol;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsBase;
+
+/**
+ * A class that generates commands to send to PyMol over its XML-RPC interface.
+ * <p>
+ * Note that because the xml-rpc interface can only accept one command at a
+ * time, we can't concatenate commands, and must instead form and send them
+ * individually.
+ * 
+ * @see https://pymolwiki.org/index.php/Category:Commands
+ * @see https://pymolwiki.org/index.php/RPC
+ */
+public class PymolCommands extends StructureCommandsBase
+{
+  // https://pymol.org/dokuwiki/doku.php?id=command:zoom
+  // not currently documented on
+  // https://pymolwiki.org/index.php/Category:Commands
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand(
+          "zoom");
+
+  // https://pymolwiki.org/index.php/Quit
+  private static final StructureCommand CLOSE_PYMOL = new StructureCommand(
+          "quit");
+
+  // not currently documented on
+  // https://pymolwiki.org/index.php/Category:Commands
+  private static final StructureCommand COLOUR_BY_CHAIN = new StructureCommand(
+          "spectrum", "chain");
+
+  private static final List<StructureCommandI> COLOR_BY_CHARGE = Arrays
+          .asList(new StructureCommand("color", "white", "*"),
+                  new StructureCommand("color", "red", "resn ASP resn GLU"),
+                  new StructureCommand("color", "blue",
+                          "resn LYS resn ARG"),
+                  new StructureCommand("color", "yellow", "resn CYS"));
+
+  private static final List<StructureCommandI> SHOW_BACKBONE = Arrays
+          .asList(new StructureCommand("hide", "everything"),
+                  new StructureCommand("show", "ribbon"));
+
+  @Override
+  public StructureCommandI colourByChain()
+  {
+    return COLOUR_BY_CHAIN;
+  }
+
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return COLOR_BY_CHARGE;
+  }
+
+  @Override
+  public StructureCommandI setBackgroundColour(Color col)
+  {
+    // https://pymolwiki.org/index.php/Bg_Color
+    return new StructureCommand("bg_color", getColourString(col));
+  }
+
+  /**
+   * Returns a colour formatted suitable for use in viewer command syntax. For
+   * example, red is {@code "0xff0000"}.
+   * 
+   * @param c
+   * @return
+   */
+  protected String getColourString(Color c)
+  {
+    return String.format("0x%02x%02x%02x", c.getRed(), c.getGreen(),
+            c.getBlue());
+  }
+
+  @Override
+  public StructureCommandI focusView()
+  {
+    return FOCUS_VIEW;
+  }
+
+  @Override
+  public List<StructureCommandI> showChains(List<String> toShow)
+  {
+    // https://pymolwiki.org/index.php/Show
+    List<StructureCommandI> commands = new ArrayList<>();
+    commands.add(new StructureCommand("hide", "everything"));
+    commands.add(new StructureCommand("show", "lines"));
+    StringBuilder chains = new StringBuilder();
+    for (String chain : toShow)
+    {
+      chains.append(" chain ").append(chain);
+    }
+    commands.add(
+            new StructureCommand("show", "cartoon", chains.toString()));
+    return commands;
+  }
+
+  @Override
+  public List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec)
+  {
+    // https://pymolwiki.org/index.php/Super
+    List<StructureCommandI> commands = new ArrayList<>();
+    String refAtomsAlphaOnly = getAtomSpec(refAtoms, true);
+    String atomSpec2AlphaOnly = getAtomSpec(atomSpec, true);
+    commands.add(new StructureCommand("super", refAtomsAlphaOnly,
+            atomSpec2AlphaOnly));
+
+    /*
+     * and show superposed residues as cartoon
+     */
+    String refAtomsAll = getAtomSpec(refAtoms, false);
+    String atomSpec2All = getAtomSpec(atomSpec, false);
+    commands.add(new StructureCommand("show", "cartoon",
+            refAtomsAll + " " + atomSpec2All));
+
+    return commands;
+  }
+
+  @Override
+  public StructureCommandI openCommandFile(String path)
+  {
+    // https://pymolwiki.org/index.php/Run
+    return new StructureCommand("run", path); // should be .pml
+  }
+
+  @Override
+  public StructureCommandI saveSession(String filepath)
+  {
+    // https://pymolwiki.org/index.php/Save#EXAMPLES
+    return new StructureCommand("save", filepath); // should be .pse
+  }
+
+  /**
+   * Returns a selection string in PyMOL 'selection macro' format:
+   * 
+   * <pre>
+   * modelId// chain/residues/
+   * </pre>
+   * 
+   * If more than one chain, makes a selection expression for each, and they are
+   * separated by spaces.
+   * 
+   * @see https://pymolwiki.org/index.php/Selection_Macros
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel model, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    boolean first = true;
+    for (String modelId : model.getModels())
+    {
+      for (String chain : model.getChains(modelId))
+      {
+        if (!first)
+        {
+          sb.append(" ");
+        }
+        first = false;
+        List<int[]> rangeList = model.getRanges(modelId, chain);
+        chain = chain.trim();
+        sb.append(modelId).append("//").append(chain).append("/");
+        boolean firstRange = true;
+        for (int[] range : rangeList)
+        {
+          if (!firstRange)
+          {
+            sb.append("+");
+          }
+          firstRange = false;
+          sb.append(String.valueOf(range[0]));
+          if (range[0] != range[1])
+          {
+            sb.append("-").append(String.valueOf(range[1]));
+          }
+        }
+        sb.append("/");
+        if (alphaOnly)
+        {
+          sb.append("CA");
+        }
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public List<StructureCommandI> showBackbone()
+  {
+    return SHOW_BACKBONE;
+  }
+
+  @Override
+  protected StructureCommandI colourResidues(String atomSpec, Color colour)
+  {
+    // https://pymolwiki.org/index.php/Color
+    return new StructureCommand("color", getColourString(colour), atomSpec);
+  }
+
+  @Override
+  protected String getResidueSpec(String residue)
+  {
+    // https://pymolwiki.org/index.php/Selection_Algebra
+    return "resn " + residue;
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
+  {
+    return new StructureCommand("load", file);
+  }
+
+  /**
+   * Overrides the default implementation (which generates concatenated
+   * commands) to generate one per colour (because the XML-RPC interface to
+   * PyMOL only accepts one command at a time)
+   * 
+   * @param colourMap
+   * @return
+   */
+  @Override
+  public List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
+  {
+    List<StructureCommandI> commands = new ArrayList<>();
+    for (Object key : colourMap.keySet())
+    {
+      Color colour = (Color) key;
+      final AtomSpecModel colourData = colourMap.get(colour);
+      commands.add(getColourCommand(colourData, colour));
+    }
+
+    return commands;
+  }
+
+  /**
+   * Returns a viewer command to set the given atom property value on atoms
+   * specified by the AtomSpecModel, for example
+   * 
+   * <pre>
+   * iterate 4zho//B/12-34,48-55/CA,jv_chain='primary'
+   * </pre>
+   * 
+   * @param attributeName
+   * @param attributeValue
+   * @param atomSpecModel
+   * @return
+   */
+  protected StructureCommandI setAttribute(String attributeName,
+          String attributeValue, AtomSpecModel atomSpecModel)
+  {
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("p.").append(attributeName).append("='")
+            .append(attributeValue).append("'");
+    String atomSpec = getAtomSpec(atomSpecModel, false);
+    return new StructureCommand("iterate", atomSpec, sb.toString());
+  }
+
+  /**
+   * Traverse the map of features/values/models/chains/positions to construct a
+   * list of 'set property' commands (one per distinct feature type and value).
+   * The values are stored in the 'p' dictionary of user-defined properties of
+   * each atom.
+   * <p>
+   * The format of each command is
+   * 
+   * <pre>
+   * <blockquote> iterate atomspec, p.featureName='value' 
+   * e.g. iterate 4zho//A/23,28-29/CA, p.jv_Metal='Fe'
+   * </blockquote>
+   * </pre>
+   * 
+   * @param featureMap
+   * @return
+   */
+  @Override
+  public List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureMap)
+  {
+    List<StructureCommandI> commands = new ArrayList<>();
+    for (String featureType : featureMap.keySet())
+    {
+      String attributeName = makeAttributeName(featureType);
+
+      /*
+       * todo: clear down existing attributes for this feature?
+       */
+      // commands.add(new StructureCommand("iterate", "all",
+      // "p."+attributeName+"='None'"); //?
+
+      Map<Object, AtomSpecModel> values = featureMap.get(featureType);
+      for (Object value : values.keySet())
+      {
+        /*
+         * for each distinct value recorded for this feature type,
+         * add a command to set the attribute on the mapped residues
+         * Put values in single quotes, encoding any embedded single quotes
+         */
+        AtomSpecModel atomSpecModel = values.get(value);
+        String featureValue = value.toString();
+        featureValue = featureValue.replaceAll("\\'", "&#39;");
+        StructureCommandI cmd = setAttribute(attributeName, featureValue,
+                atomSpecModel);
+        commands.add(cmd);
+      }
+    }
+
+    return commands;
+  }
+
+  @Override
+  public StructureCommandI openSession(String filepath)
+  {
+    // https://pymolwiki.org/index.php/Load
+    // this version of the command has no dependency on file extension
+    return new StructureCommand("load", filepath, "", "0", "pse");
+  }
+
+  @Override
+  public StructureCommandI closeViewer()
+  {
+    // https://pymolwiki.org/index.php/Quit
+    return CLOSE_PYMOL;
+  }
+
+}
diff --git a/src/jalview/ext/pymol/PymolManager.java b/src/jalview/ext/pymol/PymolManager.java
new file mode 100644 (file)
index 0000000..26c780d
--- /dev/null
@@ -0,0 +1,317 @@
+package jalview.ext.pymol;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.SocketException;
+import java.net.URL;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+import jalview.bin.Cache;
+import jalview.gui.Preferences;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+
+public class PymolManager
+{
+  private static final int RPC_REPLY_TIMEOUT_MS = 15000;
+
+  private static final int CONNECTION_TIMEOUT_MS = 100;
+
+  private static final String POST1 = "<methodCall><methodName>";
+
+  private static final String POST2 = "</methodName><params>";
+
+  private static final String POST3 = "</params></methodCall>";
+
+  private Process pymolProcess;
+
+  private int pymolXmlRpcPort;
+
+  /**
+   * Returns a list of paths to try for the PyMOL executable. Any user
+   * preference is placed first, otherwise 'standard' paths depending on the
+   * operating system.
+   * 
+   * @return
+   */
+  public static List<String> getPymolPaths()
+  {
+    return getPymolPaths(System.getProperty("os.name"));
+  }
+
+  /**
+   * Returns a list of paths to try for the PyMOL executable. Any user
+   * preference is placed first, otherwise 'standard' paths depending on the
+   * operating system.
+   * 
+   * @param os
+   *          operating system as reported by environment variable
+   *          {@code os.name}
+   * @return
+   */
+  protected static List<String> getPymolPaths(String os)
+  {
+    List<String> pathList = new ArrayList<>();
+  
+    String userPath = Cache
+            .getDefault(Preferences.PYMOL_PATH, null);
+    if (userPath != null)
+    {
+      pathList.add(userPath);
+    }
+  
+    /*
+     * add default installation paths
+     */
+    String pymol = "PyMOL";
+    if (os.startsWith("Linux"))
+    {
+      pathList.add("/usr/local/pymol/bin/" + pymol);
+      pathList.add("/usr/local/bin/" + pymol);
+      pathList.add("/usr/bin/" + pymol);
+      pathList.add(System.getProperty("user.home") + "/opt/bin/" + pymol);
+    }
+    else if (os.startsWith("Windows"))
+    {
+      // todo Windows installation path(s)
+    }
+    else if (os.startsWith("Mac"))
+    {
+      pathList.add("/Applications/PyMOL.app/Contents/MacOS/" + pymol);
+    }
+    return pathList;
+  }
+
+  public boolean isPymolLaunched()
+  {
+    // TODO pull up generic methods for external viewer processes
+    boolean launched = false;
+    if (pymolProcess != null)
+    {
+      try
+      {
+        pymolProcess.exitValue();
+        // if we get here, process has ended
+      } catch (IllegalThreadStateException e)
+      {
+        // ok - not yet terminated
+        launched = true;
+      }
+    }
+    return launched;
+  }
+
+  /**
+   * Sends the command to Pymol; if requested, tries to get and return any
+   * replies, else returns null
+   * 
+   * @param command
+   * @param getReply
+   * @return
+   */
+  public List<String> sendCommand(StructureCommandI command,
+          boolean getReply)
+  {
+    String postBody = getPostRequest(command);
+    // System.out.println(postBody);// debug
+    String rpcUrl = "http://127.0.0.1:" + this.pymolXmlRpcPort;
+    PrintWriter out = null;
+    BufferedReader in = null;
+    List<String> result = getReply ? new ArrayList<>() : null;
+
+    try
+    {
+      URL realUrl = new URL(rpcUrl);
+      HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
+      conn.setRequestProperty("accept", "*/*");
+      conn.setRequestProperty("content-type", "text/xml");
+      conn.setDoOutput(true);
+      conn.setDoInput(true);
+      out = new PrintWriter(conn.getOutputStream());
+      out.print(postBody);
+      out.flush();
+      int rc = conn.getResponseCode();
+      if (rc != HttpURLConnection.HTTP_OK)
+      {
+        Cache.log.error(
+                String.format("Error status from %s: %d", rpcUrl, rc));
+        return result;
+      }
+
+      InputStream inputStream = conn.getInputStream();
+      if (getReply)
+      {
+        in = new BufferedReader(new InputStreamReader(inputStream));
+        String line;
+        while ((line = in.readLine()) != null)
+        {
+          result.add(line);
+        }
+      }
+    } catch (SocketException e)
+    {
+      // thrown when 'quit' command is sent to PyMol
+      Cache.log.warn(String.format("Request to %s returned %s", rpcUrl,
+              e.toString()));
+    } catch (Exception e)
+    {
+      e.printStackTrace();
+    } finally
+    {
+      if (out != null)
+      {
+        out.close();
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Builds the body of the XML-RPC format POST request to execute the command
+   * 
+   * @param command
+   * @return
+   */
+  static String getPostRequest(StructureCommandI command)
+  {
+    StringBuilder sb = new StringBuilder(64);
+    sb.append(POST1).append(command.getCommand()).append(POST2);
+    if (command.hasParameters())
+    {
+      for (String p : command.getParameters())
+      {
+        /*
+         * for now assuming all are string - <string> element is optional
+         * refactor in future if other data types needed
+         * https://www.tutorialspoint.com/xml-rpc/xml_rpc_data_model.htm
+         */
+        sb.append("<parameter><value>").append(p)
+                .append("</value></parameter>");
+      }
+    }
+    sb.append(POST3);
+    return sb.toString();
+  }
+
+  public Process launchPymol()
+  {
+    // todo pull up much of this
+    // Do nothing if already launched
+    if (isPymolLaunched())
+    {
+      return pymolProcess;
+    }
+
+    String error = "Error message: ";
+    for (String pymolPath : getPymolPaths())
+    {
+      try
+      {
+        // ensure symbolic links are resolved
+        pymolPath = Paths.get(pymolPath).toRealPath().toString();
+        File path = new File(pymolPath);
+        // uncomment the next line to simulate Pymol not installed
+        // path = new File(pymolPath + "x");
+        if (!path.canExecute())
+        {
+          error += "File '" + path + "' does not exist.\n";
+          continue;
+        }
+        List<String> args = new ArrayList<>();
+        args.add(pymolPath);
+        args.add("-R"); // https://pymolwiki.org/index.php/RPC
+        ProcessBuilder pb = new ProcessBuilder(args);
+        pymolProcess = pb.start();
+        error = "";
+        break;
+      } catch (Exception e)
+      {
+        // Pymol could not be started using this path
+        error += e.getMessage();
+      }
+    }
+
+    if (pymolProcess != null)
+    {
+      this.pymolXmlRpcPort = getPortNumber();
+      if (pymolXmlRpcPort > 0)
+      {
+        Cache.log.info("PyMOL XMLRPC started on port " + pymolXmlRpcPort);
+      }
+      else
+      {
+        error += "Failed to read PyMOL XMLRPC port number";
+        Cache.log.error(error);
+        pymolProcess.destroy();
+        pymolProcess = null;
+      }
+    }
+
+    return pymolProcess;
+  }
+
+  private int getPortNumber()
+  {
+    // TODO pull up most of this!
+    int port = 0;
+    InputStream readChan = pymolProcess.getInputStream();
+    BufferedReader lineReader = new BufferedReader(
+            new InputStreamReader(readChan));
+    StringBuilder responses = new StringBuilder();
+    try
+    {
+      String response = lineReader.readLine();
+      while (response != null)
+      {
+        responses.append("\n" + response);
+        // expect: xml-rpc server running on host localhost, port 9123
+        if (response.contains("xml-rpc"))
+        {
+          String[] tokens = response.split(" ");
+          for (int i = 0; i < tokens.length - 1; i++)
+          {
+            if ("port".equals(tokens[i]))
+            {
+              port = Integer.parseInt(tokens[i + 1]);
+              break;
+            }
+          }
+        }
+        if (port > 0)
+        {
+          break; // hack for hanging readLine()
+        }
+        response = lineReader.readLine();
+      }
+    } catch (Exception e)
+    {
+      Cache.log.error("Failed to get REST port number from " + responses
+              + ": " + e.getMessage());
+      // logger.error("Failed to get REST port number from " + responses + ": "
+      // + e.getMessage());
+    } finally
+    {
+      try
+      {
+        lineReader.close();
+      } catch (IOException e2)
+      {
+      }
+    }
+    if (port == 0)
+    {
+      Cache.log.error("Failed to start PyMOL with XMLRPC, response was: "
+              + responses);
+    }
+    Cache.log.error("PyMOL started with XMLRPC on port " + port);
+    return port;
+  }
+
+}
diff --git a/src/jalview/ext/rbvi/chimera/AtomSpecModel.java b/src/jalview/ext/rbvi/chimera/AtomSpecModel.java
deleted file mode 100644 (file)
index 39d6704..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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.ext.rbvi.chimera;
-
-import jalview.util.IntRangeComparator;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * A class to model a Chimera atomspec pattern, for example
- * 
- * <pre>
- * #0:15.A,28.A,54.A,63.A,70-72.A,83-84.A,97-98.A|#1:2.A,6.A,11.A,13-14.A,70.A,82.A,96-97.A
- * </pre>
- * 
- * where
- * <ul>
- * <li>#0 is a model number</li>
- * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
- * <li>.A is a chain identifier</li>
- * <li>residue ranges are separated by comma</li>
- * <li>atomspecs for distinct models are separated by | (or)</li>
- * </ul>
- * 
- * <pre>
- * &#64;see http://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
- * </pre>
- */
-public class AtomSpecModel
-{
-  private Map<Integer, Map<String, List<int[]>>> atomSpec;
-
-  /**
-   * Constructor
-   */
-  public AtomSpecModel()
-  {
-    atomSpec = new TreeMap<Integer, Map<String, List<int[]>>>();
-  }
-
-  /**
-   * Adds one contiguous range to this atom spec
-   * 
-   * @param model
-   * @param startPos
-   * @param endPos
-   * @param chain
-   */
-  public void addRange(int model, int startPos, int endPos, String chain)
-  {
-    /*
-     * Get/initialize map of data for the colour and model
-     */
-    Map<String, List<int[]>> modelData = atomSpec.get(model);
-    if (modelData == null)
-    {
-      atomSpec.put(model, modelData = new TreeMap<String, List<int[]>>());
-    }
-
-    /*
-     * Get/initialize map of data for colour, model and chain
-     */
-    List<int[]> chainData = modelData.get(chain);
-    if (chainData == null)
-    {
-      chainData = new ArrayList<int[]>();
-      modelData.put(chain, chainData);
-    }
-
-    /*
-     * Add the start/end positions
-     */
-    chainData.add(new int[] { startPos, endPos });
-    // TODO add intelligently, using a RangeList class
-  }
-
-  /**
-   * Returns the range(s) formatted as a Chimera atomspec
-   * 
-   * @return
-   */
-  public String getAtomSpec()
-  {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModel = true;
-    for (Integer model : atomSpec.keySet())
-    {
-      if (!firstModel)
-      {
-        sb.append("|");
-      }
-      firstModel = false;
-      sb.append("#").append(model).append(":");
-
-      boolean firstPositionForModel = true;
-      final Map<String, List<int[]>> modelData = atomSpec.get(model);
-
-      for (String chain : modelData.keySet())
-      {
-        chain = " ".equals(chain) ? chain : chain.trim();
-
-        List<int[]> rangeList = modelData.get(chain);
-
-        /*
-         * sort ranges into ascending start position order
-         */
-        Collections.sort(rangeList, IntRangeComparator.ASCENDING);
-
-        int start = rangeList.isEmpty() ? 0 : rangeList.get(0)[0];
-        int end = rangeList.isEmpty() ? 0 : rangeList.get(0)[1];
-
-        Iterator<int[]> iterator = rangeList.iterator();
-        while (iterator.hasNext())
-        {
-          int[] range = iterator.next();
-          if (range[0] <= end + 1)
-          {
-            /*
-             * range overlaps or is contiguous with the last one
-             * - so just extend the end position, and carry on
-             * (unless this is the last in the list)
-             */
-            end = Math.max(end, range[1]);
-          }
-          else
-          {
-            /*
-             * we have a break so append the last range
-             */
-            appendRange(sb, start, end, chain, firstPositionForModel);
-            firstPositionForModel = false;
-            start = range[0];
-            end = range[1];
-          }
-        }
-
-        /*
-         * and append the last range
-         */
-        if (!rangeList.isEmpty())
-        {
-          appendRange(sb, start, end, chain, firstPositionForModel);
-          firstPositionForModel = false;
-        }
-      }
-    }
-    return sb.toString();
-  }
-
-  /**
-   * @param sb
-   * @param start
-   * @param end
-   * @param chain
-   * @param firstPositionForModel
-   */
-  protected void appendRange(StringBuilder sb, int start, int end,
-          String chain, boolean firstPositionForModel)
-  {
-    if (!firstPositionForModel)
-    {
-      sb.append(",");
-    }
-    if (end == start)
-    {
-      sb.append(start);
-    }
-    else
-    {
-      sb.append(start).append("-").append(end);
-    }
-
-    sb.append(".");
-    if (!" ".equals(chain)) {
-      sb.append(chain);
-    }
-  }
-}
index 3caaac3..43cdeb1 100644 (file)
  */
 package jalview.ext.rbvi.chimera;
 
-import jalview.api.AlignViewportI;
-import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureRenderer;
-import jalview.api.SequenceRenderer;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.MappedFeatures;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.gui.Desktop;
-import jalview.renderer.seqfeatures.FeatureColourFinder;
-import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
-import jalview.structure.StructureSelectionManager;
-import jalview.util.ColorUtils;
-import jalview.util.Comparison;
-
 import java.awt.Color;
 import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsBase;
+import jalview.util.ColorUtils;
+
 /**
  * Routines for generating Chimera commands for Jalview/Chimera binding
  * 
  * @author JimP
  * 
  */
-public class ChimeraCommands
+public class ChimeraCommands extends StructureCommandsBase
 {
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/focus.html
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand("focus");
+
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html#listresattr
+  private static final StructureCommand LIST_RESIDUE_ATTRIBUTES = new StructureCommand("list resattr");
+
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/stop.html
+  private static final StructureCommand CLOSE_CHIMERA = new StructureCommand("stop really");
+
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html
+  private static final StructureCommand STOP_NOTIFY_SELECTION = new StructureCommand("listen stop selection");
+
+  private static final StructureCommand STOP_NOTIFY_MODELS = new StructureCommand("listen stop models");
+
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html#listselection
+  private static final StructureCommand GET_SELECTION = new StructureCommand("list selection level residue");
 
-  public static final String NAMESPACE_PREFIX = "jv_";
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "~display all;~ribbon;chain @CA|P");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "color white;color red ::ASP,GLU;color blue ::LYS,ARG;color yellow ::CYS");
+
+  // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/rainbow.html
+  private static final StructureCommandI COLOUR_BY_CHAIN = new StructureCommand(
+          "rainbow chain");
+
+  // Chimera clause to exclude alternate locations in atom selection
+  private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
+
+  @Override
+  public StructureCommandI colourResidues(String atomSpec, Color colour)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/color.html
+    String colourCode = getColourString(colour);
+    return new StructureCommand("color " + colourCode + " " + atomSpec);
+  }
 
   /**
-   * Constructs Chimera commands to colour residues as per the Jalview alignment
+   * Returns a colour formatted suitable for use in viewer command syntax
    * 
-   * @param ssm
-   * @param files
-   * @param sequence
-   * @param sr
-   * @param fr
-   * @param viewPanel
+   * @param colour
    * @return
    */
-  public static StructureMappingcommandSet[] getColourBySequenceCommand(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
-          AlignmentViewPanel viewPanel)
+  protected String getColourString(Color colour)
   {
-    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, files,
-            sequence, sr, viewPanel);
-
-    List<String> colourCommands = buildColourCommands(colourMap);
-
-    StructureMappingcommandSet cs = new StructureMappingcommandSet(
-            ChimeraCommands.class, null,
-            colourCommands.toArray(new String[colourCommands.size()]));
-
-    return new StructureMappingcommandSet[] { cs };
+    return ColorUtils.toTkCode(colour);
   }
 
   /**
-   * Traverse the map of colours/models/chains/positions to construct a list of
-   * 'color' commands (one per distinct colour used). The format of each command
-   * is
+   * Traverse the map of features/values/models/chains/positions to construct a
+   * list of 'setattr' commands (one per distinct feature type and value).
+   * <p>
+   * The format of each command is
    * 
    * <pre>
-   * <blockquote> 
-   * color colorname #modelnumber:range.chain 
-   * e.g. color #00ff00 #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
+   * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
+   * e.g. setattr r jv_chain &lt;value&gt; #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
    * </blockquote>
    * </pre>
    * 
-   * @param colourMap
+   * @param featureMap
    * @return
    */
-  protected static List<String> buildColourCommands(
-          Map<Object, AtomSpecModel> colourMap)
+  @Override
+  public List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureMap)
   {
-    /*
-     * This version concatenates all commands into a single String (semi-colon
-     * delimited). If length limit issues arise, refactor to return one color
-     * command per colour.
-     */
-    List<String> commands = new ArrayList<>();
-    StringBuilder sb = new StringBuilder(256);
-    boolean firstColour = true;
-    for (Object key : colourMap.keySet())
+    List<StructureCommandI> commands = new ArrayList<>();
+    for (String featureType : featureMap.keySet())
     {
-      Color colour = (Color) key;
-      String colourCode = ColorUtils.toTkCode(colour);
-      if (!firstColour)
-      {
-        sb.append("; ");
-      }
-      sb.append("color ").append(colourCode).append(" ");
-      firstColour = false;
-      final AtomSpecModel colourData = colourMap.get(colour);
-      sb.append(colourData.getAtomSpec());
-    }
-    commands.add(sb.toString());
-    return commands;
-  }
+      String attributeName = makeAttributeName(featureType);
 
-  /**
-   * Traverses a map of { modelNumber, {chain, {list of from-to ranges} } } and
-   * builds a Chimera format atom spec
-   * 
-   * @param modelAndChainRanges
-   */
-  protected static String getAtomSpec(
-          Map<Integer, Map<String, List<int[]>>> modelAndChainRanges)
-  {
-    StringBuilder sb = new StringBuilder(128);
-    boolean firstModelForColour = true;
-    for (Integer model : modelAndChainRanges.keySet())
-    {
-      boolean firstPositionForModel = true;
-      if (!firstModelForColour)
-      {
-        sb.append("|");
-      }
-      firstModelForColour = false;
-      sb.append("#").append(model).append(":");
+      /*
+       * clear down existing attributes for this feature
+       */
+      // 'problem' - sets attribute to None on all residues - overkill?
+      // commands.add("~setattr r " + attributeName + " :*");
 
-      final Map<String, List<int[]>> modelData = modelAndChainRanges
-              .get(model);
-      for (String chain : modelData.keySet())
+      Map<Object, AtomSpecModel> values = featureMap.get(featureType);
+      for (Object value : values.keySet())
       {
-        boolean hasChain = !"".equals(chain.trim());
-        for (int[] range : modelData.get(chain))
-        {
-          if (!firstPositionForModel)
-          {
-            sb.append(",");
-          }
-          if (range[0] == range[1])
-          {
-            sb.append(range[0]);
-          }
-          else
-          {
-            sb.append(range[0]).append("-").append(range[1]);
-          }
-          if (hasChain)
-          {
-            sb.append(".").append(chain);
-          }
-          firstPositionForModel = false;
-        }
+        /*
+         * for each distinct value recorded for this feature type,
+         * add a command to set the attribute on the mapped residues
+         * Put values in single quotes, encoding any embedded single quotes
+         */
+        AtomSpecModel atomSpecModel = values.get(value);
+        String featureValue = value.toString();
+        featureValue = featureValue.replaceAll("\\'", "&#39;");
+        StructureCommandI cmd = setAttribute(attributeName, featureValue,
+                atomSpecModel);
+        commands.add(cmd);
       }
     }
-    return sb.toString();
+
+    return commands;
   }
 
   /**
+   * Returns a viewer command to set the given residue attribute value on
+   * residues specified by the AtomSpecModel, for example
+   * 
    * <pre>
-   * Build a data structure which records contiguous subsequences for each colour. 
-   * From this we can easily generate the Chimera command for colour by sequence.
-   * Color
-   *     Model number
-   *         Chain
-   *             list of start/end ranges
-   * Ordering is by order of addition (for colours and positions), natural ordering (for models and chains)
+   * setatr res jv_chain 'primary' #1:12-34,48-55.B
    * </pre>
+   * 
+   * @param attributeName
+   * @param attributeValue
+   * @param atomSpecModel
+   * @return
    */
-  protected static Map<Object, AtomSpecModel> buildColoursMap(
-          StructureSelectionManager ssm, String[] files,
-          SequenceI[][] sequence, SequenceRenderer sr,
-          AlignmentViewPanel viewPanel)
+  protected StructureCommandI setAttribute(String attributeName,
+          String attributeValue,
+          AtomSpecModel atomSpecModel)
   {
-    FeatureRenderer fr = viewPanel.getFeatureRenderer();
-    FeatureColourFinder finder = new FeatureColourFinder(fr);
-    AlignViewportI viewport = viewPanel.getAlignViewport();
-    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
-    AlignmentI al = viewport.getAlignment();
-    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
-    Color lastColour = null;
-
-    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-    {
-      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
-
-      if (mapping == null || mapping.length < 1)
-      {
-        continue;
-      }
-
-      int startPos = -1, lastPos = -1;
-      String lastChain = "";
-      for (int s = 0; s < sequence[pdbfnum].length; s++)
-      {
-        for (int sp, m = 0; m < mapping.length; m++)
-        {
-          final SequenceI seq = sequence[pdbfnum][s];
-          if (mapping[m].getSequence() == seq
-                  && (sp = al.findIndex(seq)) > -1)
-          {
-            SequenceI asp = al.getSequenceAt(sp);
-            for (int r = 0; r < asp.getLength(); r++)
-            {
-              // no mapping to gaps in sequence
-              if (Comparison.isGap(asp.getCharAt(r)))
-              {
-                continue;
-              }
-              int pos = mapping[m].getPDBResNum(asp.findPosition(r));
-
-              if (pos < 1 || pos == lastPos)
-              {
-                continue;
-              }
-
-              Color colour = sr.getResidueColour(seq, r, finder);
-
-              /*
-               * darker colour for hidden regions
-               */
-              if (!cs.isVisible(r))
-              {
-                colour = Color.GRAY;
-              }
-
-              final String chain = mapping[m].getChain();
-
-              /*
-               * Just keep incrementing the end position for this colour range
-               * _unless_ colour, PDB model or chain has changed, or there is a
-               * gap in the mapped residue sequence
-               */
-              final boolean newColour = !colour.equals(lastColour);
-              final boolean nonContig = lastPos + 1 != pos;
-              final boolean newChain = !chain.equals(lastChain);
-              if (newColour || nonContig || newChain)
-              {
-                if (startPos != -1)
-                {
-                  addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
-                          lastPos, lastChain);
-                }
-                startPos = pos;
-              }
-              lastColour = colour;
-              lastPos = pos;
-              lastChain = chain;
-            }
-            // final colour range
-            if (lastColour != null)
-            {
-              addAtomSpecRange(colourMap, lastColour, pdbfnum, startPos,
-                      lastPos, lastChain);
-            }
-            // break;
-          }
-        }
-      }
-    }
-    return colourMap;
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("setattr res ").append(attributeName).append(" '")
+            .append(attributeValue).append("' ");
+    sb.append(getAtomSpec(atomSpecModel, false));
+    return new StructureCommand(sb.toString());
   }
 
   /**
-   * Helper method to add one contiguous range to the AtomSpec model for the given
-   * value (creating the model if necessary). As used by Jalview, {@code value} is
-   * <ul>
-   * <li>a colour, when building a 'colour structure by sequence' command</li>
-   * <li>a feature value, when building a 'set Chimera attributes from features'
-   * command</li>
-   * </ul>
+   * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
+   * for a 'Jalview' namespace, and any non-alphanumeric character is converted
+   * to an underscore.
    * 
-   * @param map
-   * @param value
-   * @param model
-   * @param startPos
-   * @param endPos
-   * @param chain
+   * @param featureType
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
    */
-  protected static void addAtomSpecRange(Map<Object, AtomSpecModel> map,
-          Object value, int model, int startPos, int endPos, String chain)
+  @Override
+  protected String makeAttributeName(String featureType)
   {
+    String attName = super.makeAttributeName(featureType);
+
     /*
-     * Get/initialize map of data for the colour
+     * Chimera treats an attribute name ending in 'color' as colour-valued;
+     * Jalview doesn't, so prevent this by appending an underscore
      */
-    AtomSpecModel atomSpec = map.get(value);
-    if (atomSpec == null)
+    if (attName.toUpperCase().endsWith("COLOR"))
     {
-      atomSpec = new AtomSpecModel();
-      map.put(value, atomSpec);
+      attName += "_";
     }
 
-    atomSpec.addRange(model, startPos, endPos, chain);
+    return attName;
   }
 
-  /**
-   * Constructs and returns Chimera commands to set attributes on residues
-   * corresponding to features in Jalview. Attribute names are the Jalview
-   * feature type, with a "jv_" prefix.
-   * 
-   * @param ssm
-   * @param files
-   * @param seqs
-   * @param viewPanel
-   * @return
-   */
-  public static StructureMappingcommandSet getSetAttributeCommandsForFeatures(
-          StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
-          AlignmentViewPanel viewPanel)
+  @Override
+  public StructureCommandI colourByChain()
   {
-    Map<String, Map<Object, AtomSpecModel>> featureMap = buildFeaturesMap(
-            ssm, files, seqs, viewPanel);
-
-    List<String> commands = buildSetAttributeCommands(featureMap);
-
-    StructureMappingcommandSet cs = new StructureMappingcommandSet(
-            ChimeraCommands.class, null,
-            commands.toArray(new String[commands.size()]));
+    return COLOUR_BY_CHAIN;
+  }
 
-    return cs;
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
   }
 
-  /**
-   * <pre>
-   * Helper method to build a map of 
-   *   { featureType, { feature value, AtomSpecModel } }
-   * </pre>
-   * 
-   * @param ssm
-   * @param files
-   * @param seqs
-   * @param viewPanel
-   * @return
-   */
-  protected static Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
-          StructureSelectionManager ssm, String[] files, SequenceI[][] seqs,
-          AlignmentViewPanel viewPanel)
+  @Override
+  public String getResidueSpec(String residue)
   {
-    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
+    return "::" + residue;
+  }
 
-    FeatureRenderer fr = viewPanel.getFeatureRenderer();
-    if (fr == null)
-    {
-      return theMap;
-    }
+  @Override
+  public StructureCommandI setBackgroundColour(Color col)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/set.html#bgcolor
+    return new StructureCommand("set bgColor " + ColorUtils.toTkCode(col));
+  }
 
-    AlignViewportI viewport = viewPanel.getAlignViewport();
-    List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
+  @Override
+  public StructureCommandI focusView()
+  {
+    return FOCUS_VIEW;
+  }
 
+  @Override
+  public List<StructureCommandI> showChains(List<String> toShow)
+  {
     /*
-     * if alignment is showing features from complement, we also transfer
-     * these features to the corresponding mapped structure residues
+     * Construct a chimera command like
+     * 
+     * ~display #*;~ribbon #*;ribbon :.A,:.B
      */
-    boolean showLinkedFeatures = viewport.isShowComplementFeatures();
-    List<String> complementFeatures = new ArrayList<>();
-    FeatureRenderer complementRenderer = null;
-    if (showLinkedFeatures)
+    StringBuilder cmd = new StringBuilder(64);
+    boolean first = true;
+    for (String chain : toShow)
     {
-      AlignViewportI comp = fr.getViewport().getCodingComplement();
-      if (comp != null)
+      String[] tokens = chain.split(":");
+      if (tokens.length == 2)
       {
-        complementRenderer = Desktop.getAlignFrameFor(comp)
-                .getFeatureRenderer();
-        complementFeatures = complementRenderer.getDisplayedFeatureTypes();
+        String showChainCmd = tokens[0] + ":." + tokens[1];
+        if (!first)
+        {
+          cmd.append(",");
+        }
+        cmd.append(showChainCmd);
+        first = false;
       }
     }
-    if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
-    {
-      return theMap;
-    }
 
-    AlignmentI alignment = viewPanel.getAlignment();
-    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-    {
-      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+    /*
+     * could append ";focus" to this command to resize the display to fill the
+     * window, but it looks more helpful not to (easier to relate chains to the
+     * whole)
+     */
+    final String command = "~display #*; ~ribbon #*; ribbon :"
+            + cmd.toString();
+    return Arrays.asList(new StructureCommand(command));
+  }
 
-      if (mapping == null || mapping.length < 1)
-      {
-        continue;
-      }
+  @Override
+  public List<StructureCommandI> superposeStructures(AtomSpecModel ref,
+          AtomSpecModel spec)
+  {
+    /*
+     * Form Chimera match command to match spec to ref
+     * (the first set of atoms are moved on to the second)
+     * 
+     * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
+     * 
+     * @see https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
+     */
+    StringBuilder cmd = new StringBuilder();
+    String atomSpecAlphaOnly = getAtomSpec(spec, true);
+    String refSpecAlphaOnly = getAtomSpec(ref, true);
+    cmd.append("match ").append(atomSpecAlphaOnly).append(" ").append(refSpecAlphaOnly);
 
-      for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
-      {
-        for (int m = 0; m < mapping.length; m++)
-        {
-          final SequenceI seq = seqs[pdbfnum][seqNo];
-          int sp = alignment.findIndex(seq);
-          StructureMapping structureMapping = mapping[m];
-          if (structureMapping.getSequence() == seq && sp > -1)
-          {
-            /*
-             * found a sequence with a mapping to a structure;
-             * now scan its features
-             */
-            if (!visibleFeatures.isEmpty())
-            {
-              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
-                      theMap, pdbfnum);
-            }
-            if (showLinkedFeatures)
-            {
-              scanComplementFeatures(complementRenderer, structureMapping,
-                      seq, theMap, pdbfnum);
-            }
-          }
-        }
-      }
-    }
-    return theMap;
+    /*
+     * show superposed residues as ribbon
+     */
+    String atomSpec = getAtomSpec(spec, false);
+    String refSpec = getAtomSpec(ref, false);
+    cmd.append("; ribbon ");
+    cmd.append(atomSpec).append("|").append(refSpec).append("; focus");
+
+    return Arrays.asList(new StructureCommand(cmd.toString()));
+  }
+
+  @Override
+  public StructureCommandI openCommandFile(String path)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
+    return new StructureCommand("open cmd:" + path);
+  }
+
+  @Override
+  public StructureCommandI saveSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/save.html
+    return new StructureCommand("save " + filepath);
   }
 
   /**
-   * Scans visible features in mapped positions of the CDS/peptide complement, and
-   * adds any found to the map of attribute values/structure positions
+   * Returns the range(s) modelled by {@code atomSpec} formatted as a Chimera
+   * atomspec string, e.g.
    * 
-   * @param complementRenderer
-   * @param structureMapping
-   * @param seq
-   * @param theMap
-   * @param modelNumber
+   * <pre>
+   * #0:15.A,28.A,54.A,70-72.A|#1:2.A,6.A,11.A,13-14.A
+   * </pre>
+   * 
+   * where
+   * <ul>
+   * <li>#0 is a model number</li>
+   * <li>15 or 70-72 is a residue number, or range of residue numbers</li>
+   * <li>.A is a chain identifier</li>
+   * <li>residue ranges are separated by comma</li>
+   * <li>atomspecs for distinct models are separated by | (or)</li>
+   * </ul>
+   * 
+   * <pre>
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
    */
-  protected static void scanComplementFeatures(
-          FeatureRenderer complementRenderer,
-          StructureMapping structureMapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    /*
-     * for each sequence residue mapped to a structure position...
-     */
-    for (int seqPos : structureMapping.getMapping().keySet())
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (String model : atomSpec.getModels())
     {
-      /*
-       * find visible complementary features at mapped position(s)
-       */
-      MappedFeatures mf = complementRenderer
-              .findComplementFeaturesAtResidue(seq, seqPos);
-      if (mf != null)
+      if (!firstModel)
       {
-        for (SequenceFeature sf : mf.features)
-        {
-          String type = sf.getType();
-
-          /*
-           * Don't copy features which originated from Chimera
-           */
-          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-                  .equals(sf.getFeatureGroup()))
-          {
-            continue;
-          }
-
-          /*
-           * record feature 'value' (score/description/type) as at the
-           * corresponding structure position
-           */
-          List<int[]> mappedRanges = structureMapping
-                  .getPDBResNumRanges(seqPos, seqPos);
-
-          if (!mappedRanges.isEmpty())
-          {
-            String value = sf.getDescription();
-            if (value == null || value.length() == 0)
-            {
-              value = type;
-            }
-            float score = sf.getScore();
-            if (score != 0f && !Float.isNaN(score))
-            {
-              value = Float.toString(score);
-            }
-            Map<Object, AtomSpecModel> featureValues = theMap.get(type);
-            if (featureValues == null)
-            {
-              featureValues = new HashMap<>();
-              theMap.put(type, featureValues);
-            }
-            for (int[] range : mappedRanges)
-            {
-              addAtomSpecRange(featureValues, value, modelNumber, range[0],
-                      range[1], structureMapping.getChain());
-            }
-          }
-        }
+        sb.append("|");
       }
+      firstModel = false;
+      appendModel(sb, model, atomSpec, alphaOnly);
     }
+    return sb.toString();
   }
 
   /**
-   * Inspect features on the sequence; for each feature that is visible, determine
-   * its mapped ranges in the structure (if any) according to the given mapping,
-   * and add them to the map.
+   * A helper method to append an atomSpec string for atoms in the given model
    * 
-   * @param visibleFeatures
-   * @param mapping
-   * @param seq
-   * @param theMap
-   * @param modelNumber
+   * @param sb
+   * @param model
+   * @param atomSpec
+   * @param alphaOnly
    */
-  protected static void scanSequenceFeatures(List<String> visibleFeatures,
-          StructureMapping mapping, SequenceI seq,
-          Map<String, Map<Object, AtomSpecModel>> theMap, int modelNumber)
+  protected void appendModel(StringBuilder sb, String model,
+          AtomSpecModel atomSpec, boolean alphaOnly)
   {
-    List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
-            visibleFeatures.toArray(new String[visibleFeatures.size()]));
-    for (SequenceFeature sf : sfs)
-    {
-      String type = sf.getType();
+    sb.append("#").append(model).append(":");
 
-      /*
-       * Don't copy features which originated from Chimera
-       */
-      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
-              .equals(sf.getFeatureGroup()))
-      {
-        continue;
-      }
+    boolean firstPositionForModel = true;
 
-      List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
-              sf.getEnd());
+    for (String chain : atomSpec.getChains(model))
+    {
+      chain = " ".equals(chain) ? chain : chain.trim();
 
-      if (!mappedRanges.isEmpty())
+      List<int[]> rangeList = atomSpec.getRanges(model, chain);
+      for (int[] range : rangeList)
       {
-        String value = sf.getDescription();
-        if (value == null || value.length() == 0)
-        {
-          value = type;
-        }
-        float score = sf.getScore();
-        if (score != 0f && !Float.isNaN(score))
-        {
-          value = Float.toString(score);
-        }
-        Map<Object, AtomSpecModel> featureValues = theMap.get(type);
-        if (featureValues == null)
-        {
-          featureValues = new HashMap<>();
-          theMap.put(type, featureValues);
-        }
-        for (int[] range : mappedRanges)
-        {
-          addAtomSpecRange(featureValues, value, modelNumber, range[0],
-                  range[1], mapping.getChain());
-        }
+        appendRange(sb, range[0], range[1], chain, firstPositionForModel,
+                false);
+        firstPositionForModel = false;
       }
     }
-  }
-
-  /**
-   * Traverse the map of features/values/models/chains/positions to construct a
-   * list of 'setattr' commands (one per distinct feature type and value).
-   * <p>
-   * The format of each command is
-   * 
-   * <pre>
-   * <blockquote> setattr r <featureName> " " #modelnumber:range.chain 
-   * e.g. setattr r jv:chain <value> #0:2.B,4.B,9-12.B|#1:1.A,2-6.A,...
-   * </blockquote>
-   * </pre>
-   * 
-   * @param featureMap
-   * @return
-   */
-  protected static List<String> buildSetAttributeCommands(
-          Map<String, Map<Object, AtomSpecModel>> featureMap)
-  {
-    List<String> commands = new ArrayList<>();
-    for (String featureType : featureMap.keySet())
+    if (alphaOnly)
     {
-      String attributeName = makeAttributeName(featureType);
-
       /*
-       * clear down existing attributes for this feature
+       * restrict to alpha carbon, no alternative locations
+       * (needed to ensuring matching atom counts for superposition)
        */
-      // 'problem' - sets attribute to None on all residues - overkill?
-      // commands.add("~setattr r " + attributeName + " :*");
-
-      Map<Object, AtomSpecModel> values = featureMap.get(featureType);
-      for (Object value : values.keySet())
-      {
-        /*
-         * for each distinct value recorded for this feature type,
-         * add a command to set the attribute on the mapped residues
-         * Put values in single quotes, encoding any embedded single quotes
-         */
-        StringBuilder sb = new StringBuilder(128);
-        String featureValue = value.toString();
-        featureValue = featureValue.replaceAll("\\'", "&#39;");
-        sb.append("setattr r ").append(attributeName).append(" '")
-                .append(featureValue).append("' ");
-        sb.append(values.get(value).getAtomSpec());
-        commands.add(sb.toString());
-      }
+      // TODO @P instead if RNA - add nucleotide flag to AtomSpecModel?
+      sb.append("@CA").append(NO_ALTLOCS);
     }
+  }
 
-    return commands;
+  @Override
+  public List<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public StructureCommandI loadFile(String file)
+  {
+    return new StructureCommand("open " + file);
   }
 
   /**
-   * Makes a prefixed and valid Chimera attribute name. A jv_ prefix is applied
-   * for a 'Jalview' namespace, and any non-alphanumeric character is converted
-   * to an underscore.
-   * 
-   * @param featureType
-   * @return
-   * 
-   *         <pre>
-   * &#64;see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/setattr.html
-   *         </pre>
+   * Overrides the default method to concatenate colour commands into one
    */
-  protected static String makeAttributeName(String featureType)
+  @Override
+  public List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
   {
-    StringBuilder sb = new StringBuilder();
-    if (featureType != null)
+    List<StructureCommandI> commands = new ArrayList<>();
+    StringBuilder sb = new StringBuilder(colourMap.size() * 20);
+    boolean first = true;
+    for (Object key : colourMap.keySet())
     {
-      for (char c : featureType.toCharArray())
+      Color colour = (Color) key;
+      final AtomSpecModel colourData = colourMap.get(colour);
+      StructureCommandI command = getColourCommand(colourData, colour);
+      if (!first)
       {
-        sb.append(Character.isLetterOrDigit(c) ? c : '_');
+        sb.append(getCommandSeparator());
       }
+      first = false;
+      sb.append(command.getCommand());
     }
-    String attName = NAMESPACE_PREFIX + sb.toString();
 
-    /*
-     * Chimera treats an attribute name ending in 'color' as colour-valued;
-     * Jalview doesn't, so prevent this by appending an underscore
-     */
-    if (attName.toUpperCase().endsWith("COLOR"))
-    {
-      attName += "_";
-    }
+    commands.add(new StructureCommand(sb.toString()));
+    return commands;
+  }
 
-    return attName;
+  @Override
+  public StructureCommandI openSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/filetypes.html
+    // this version of the command has no dependency on file extension
+    return new StructureCommand("open chimera:" + filepath);
+  }
+
+  @Override
+  public StructureCommandI closeViewer()
+  {
+    return CLOSE_CHIMERA;
+  }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    // https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/listen.html
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(new StructureCommand("listen start models url " + uri));
+    cmds.add(new StructureCommand("listen start select prefix SelectionChanged url " + uri));
+    return cmds;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(STOP_NOTIFY_MODELS);
+    cmds.add(STOP_NOTIFY_SELECTION);
+    return cmds;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return GET_SELECTION;
+  }
+
+  @Override
+  public StructureCommandI listResidueAttributes()
+  {
+    return LIST_RESIDUE_ATTRIBUTES;
+  }
+
+  @Override
+  public StructureCommandI getResidueAttributes(String attName)
+  {
+    // this alternative command
+    // list residues spec ':*/attName' attr attName
+    // doesn't report 'None' values (which is good), but
+    // fails for 'average.bfactor' (which is bad):
+    return new StructureCommand("list residues attr '" + attName + "'");
   }
 
 }
index a0d74bc..40b0ff0 100644 (file)
@@ -114,17 +114,25 @@ public class ChimeraListener extends AbstractRequestHandler
   {
     // dumpRequest(request);
     String message = request.getParameter(CHIMERA_NOTIFICATION);
-    if (SELECTION_CHANGED.equals(message))
+    if (message == null)
     {
-      this.chimeraBinding.highlightChimeraSelection();
+      message = request.getParameter("chimerax_notification");
     }
-    else if (message != null && message.startsWith(MODEL_CHANGED))
+    if (message != null)
     {
-      processModelChanged(message.substring(MODEL_CHANGED.length()));
-    }
-    else
-    {
-      System.err.println("Unexpected chimeraNotification: " + message);
+      if (message.startsWith("SelectionChanged"))
+      {
+        this.chimeraBinding.highlightChimeraSelection();
+      }
+      else if (message.startsWith(MODEL_CHANGED))
+      {
+        System.err.println(message);
+        processModelChanged(message.substring(MODEL_CHANGED.length()));
+      }
+      else
+      {
+        System.err.println("Unexpected chimeraNotification: " + message);
+      }
     }
   }
 
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java b/src/jalview/ext/rbvi/chimera/ChimeraXCommands.java
new file mode 100644 (file)
index 0000000..314e6db
--- /dev/null
@@ -0,0 +1,278 @@
+/*
+ * 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.ext.rbvi.chimera;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.util.ColorUtils;
+
+/**
+ * Routines for generating ChimeraX commands for Jalview/ChimeraX binding
+ */
+public class ChimeraXCommands extends ChimeraCommands
+{
+  // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/info.html#resattr
+  private static final StructureCommand LIST_RESIDUE_ATTRIBUTES = new StructureCommand("info resattr");
+
+  // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/exit.html
+  private static final StructureCommand CLOSE_CHIMERAX = new StructureCommand("exit");
+
+  // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/info.html#notify
+  private static final StructureCommand STOP_NOTIFY_SELECTION = new StructureCommand("info notify stop selection jalview");
+
+  private static final StructureCommand STOP_NOTIFY_MODELS = new StructureCommand("info notify stop models jalview");
+
+  // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/info.html#selection
+  private static final StructureCommand GET_SELECTION = new StructureCommand(
+          "info selection level residue");
+
+  private static final StructureCommand SHOW_BACKBONE = new StructureCommand(
+          "~display all;~ribbon;show @CA|P atoms");
+
+  // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/view.html
+  private static final StructureCommand FOCUS_VIEW = new StructureCommand(
+          "view");
+
+  private static final StructureCommandI COLOUR_BY_CHARGE = new StructureCommand(
+          "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow");
+
+  @Override
+  public List<StructureCommandI> colourByCharge()
+  {
+    return Arrays.asList(COLOUR_BY_CHARGE);
+  }
+
+  @Override
+  public String getResidueSpec(String residue)
+  {
+    return ":" + residue;
+  }
+
+  @Override
+  public StructureCommandI colourResidues(String atomSpec, Color colour)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/color.html
+    String colourCode = getColourString(colour);
+
+    return new StructureCommand("color " + atomSpec + " " + colourCode);
+  }
+
+  @Override
+  public StructureCommandI focusView()
+  {
+    return FOCUS_VIEW;
+  }
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 1;
+  }
+
+  /**
+   * Returns a viewer command to set the given residue attribute value on
+   * residues specified by the AtomSpecModel, for example
+   * 
+   * <pre>
+   * setattr #0/A:3-9,14-20,39-43 res jv_strand 'strand' create true
+   * </pre>
+   * 
+   * @param attributeName
+   * @param attributeValue
+   * @param atomSpecModel
+   * @return
+   */
+  @Override
+  protected StructureCommandI setAttribute(String attributeName,
+          String attributeValue, AtomSpecModel atomSpecModel)
+  {
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("setattr ").append(getAtomSpec(atomSpecModel, false));
+    sb.append(" res ").append(attributeName).append(" '")
+            .append(attributeValue).append("'");
+    sb.append(" create true");
+    return new StructureCommand(sb.toString());
+  }
+
+  @Override
+  public StructureCommandI openCommandFile(String path)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html
+    return new StructureCommand("open " + path);
+  }
+
+  @Override
+  public StructureCommandI saveSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html
+    // note ChimeraX will append ".cxs" to the filepath!
+    return new StructureCommand("save " + filepath + " format session");
+  }
+
+  /**
+   * Returns the range(s) formatted as a ChimeraX atomspec, for example
+   * <p>
+   * #1/A:2-20,30-40/B:10-20|#2/A:12-30
+   * 
+   * @return
+   */
+  @Override
+  public String getAtomSpec(AtomSpecModel atomSpec, boolean alphaOnly)
+  {
+    StringBuilder sb = new StringBuilder(128);
+    boolean firstModel = true;
+    for (String model : atomSpec.getModels())
+    {
+      if (!firstModel)
+      {
+        sb.append("|");
+      }
+      firstModel = false;
+      appendModel(sb, model, atomSpec);
+      if (alphaOnly)
+      {
+        // TODO @P if RNA - add nucleotide flag to AtomSpecModel?
+        sb.append("@CA");
+      }
+      // todo: is there ChimeraX syntax to exclude altlocs?
+    }
+    return sb.toString();
+  }
+
+  /**
+   * A helper method to append an atomSpec string for atoms in the given model
+   * 
+   * @param sb
+   * @param model
+   * @param atomSpec
+   */
+  protected void appendModel(StringBuilder sb, String model,
+          AtomSpecModel atomSpec)
+  {
+    sb.append("#").append(model);
+
+    for (String chain : atomSpec.getChains(model))
+    {
+      boolean firstPositionForChain = true;
+      sb.append("/").append(chain.trim()).append(":");
+      List<int[]> rangeList = atomSpec.getRanges(model, chain);
+      boolean first = true;
+      for (int[] range : rangeList)
+      {
+        if (!first)
+        {
+          sb.append(",");
+        }
+        first = false;
+        appendRange(sb, range[0], range[1], chain, firstPositionForChain,
+                true);
+      }
+    }
+  }
+
+  @Override
+  public List<StructureCommandI> showBackbone()
+  {
+    return Arrays.asList(SHOW_BACKBONE);
+  }
+
+  @Override
+  public List<StructureCommandI> superposeStructures(AtomSpecModel ref,
+          AtomSpecModel spec)
+  {
+    /*
+     * Form ChimeraX match command to match spec to ref
+     * 
+     * match #1/A:2-94 toAtoms #2/A:1-93
+     * 
+     * @see https://www.cgl.ucsf.edu/chimerax/docs/user/commands/align.html
+     */
+    StringBuilder cmd = new StringBuilder();
+    String atomSpec = getAtomSpec(spec, true);
+    String refSpec = getAtomSpec(ref, true);
+    cmd.append("align ").append(atomSpec).append(" toAtoms ")
+            .append(refSpec);
+
+    /*
+     * show superposed residues as ribbon, others as chain
+     */
+    cmd.append("; ribbon ");
+    cmd.append(getAtomSpec(spec, false)).append("|");
+    cmd.append(getAtomSpec(ref, false)).append("; view");
+
+    return Arrays.asList(new StructureCommand(cmd.toString()));
+  }
+
+  @Override
+  public StructureCommandI openSession(String filepath)
+  {
+    // https://www.cgl.ucsf.edu/chimerax/docs/user/commands/open.html#composite
+    // this version of the command has no dependency on file extension
+    return new StructureCommand("open " + filepath + " format session");
+  }
+
+  @Override
+  public StructureCommandI closeViewer()
+  {
+    return CLOSE_CHIMERAX;
+  }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(new StructureCommand("info notify start models prefix ModelChanged jalview url " + uri));
+    cmds.add(new StructureCommand("info notify start selection jalview prefix SelectionChanged url " + uri));
+    return cmds;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    List<StructureCommandI> cmds = new ArrayList<>();
+    cmds.add(STOP_NOTIFY_MODELS);
+    cmds.add(STOP_NOTIFY_SELECTION);
+    return cmds;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return GET_SELECTION;
+  }
+
+  @Override
+  public StructureCommandI listResidueAttributes()
+  {
+    return LIST_RESIDUE_ATTRIBUTES;
+  }
+}
diff --git a/src/jalview/ext/rbvi/chimera/ChimeraXManager.java b/src/jalview/ext/rbvi/chimera/ChimeraXManager.java
new file mode 100644 (file)
index 0000000..9d89ac7
--- /dev/null
@@ -0,0 +1,50 @@
+package jalview.ext.rbvi.chimera;
+
+import java.util.List;
+
+import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
+
+/**
+ * A class to help Jalview start, stop and send commands to ChimeraX.
+ * <p>
+ * Much of the functionality is common with Chimera, so for convenience we
+ * extend ChimeraManager, however note this class is <em>not</em> based on the
+ * Cytoscape class at
+ * {@code https://github.com/RBVI/structureVizX/blob/master/src/main/java/edu/ucsf/rbvi/structureVizX/internal/model/ChimeraManager.java}.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class ChimeraXManager extends ChimeraManager
+{
+
+  public ChimeraXManager(StructureManager structureManager)
+  {
+    super(structureManager);
+  }
+
+  public boolean isChimeraX()
+  {
+    return true;
+  }
+
+  /**
+   * Returns "POST" as the HTTP request method to use for REST service calls to ChimeraX
+   * @return
+   */
+  protected String getHttpRequestMethod()
+  {
+    return "GET";
+  }
+
+  /**
+   * Adds command-line arguments to start the REST server
+   */
+  protected void addLaunchArguments(List<String> args)
+  {
+    args.add("--cmd");
+    args.add("remote rest start");
+  }
+
+}
index 00446f2..66420b0 100644 (file)
  */
 package jalview.ext.rbvi.chimera;
 
-import jalview.api.AlignmentViewPanel;
-import jalview.api.SequenceRenderer;
-import jalview.api.structures.JalviewStructureDisplayI;
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SearchResultMatchI;
-import jalview.datamodel.SearchResultsI;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.httpserver.AbstractRequestHandler;
-import jalview.io.DataSourceType;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.ResidueProperties;
-import jalview.structure.AtomSpec;
-import jalview.structure.StructureMappingcommandSet;
-import jalview.structure.StructureSelectionManager;
-import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.MessageManager;
-
-import java.awt.Color;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.BindException;
 import java.util.ArrayList;
-import java.util.BitSet;
 import java.util.Collections;
-import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -59,31 +35,33 @@ import ext.edu.ucsf.rbvi.strucviz2.ChimeraManager;
 import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SearchResultMatchI;
+import jalview.datamodel.SearchResultsI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.httpserver.AbstractRequestHandler;
+import jalview.io.DataSourceType;
+import jalview.structure.AtomSpec;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureSelectionManager;
+import jalview.structures.models.AAStructureBindingModel;
 
 public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 {
-  public static final String CHIMERA_FEATURE_GROUP = "Chimera";
-
-  // Chimera clause to exclude alternate locations in atom selection
-  private static final String NO_ALTLOCS = "&~@.B-Z&~@.2-9";
+  public static final String CHIMERA_SESSION_EXTENSION = ".py";
 
-  private static final String COLOURING_CHIMERA = MessageManager
-          .getString("status.colouring_chimera");
-
-  private static final boolean debug = false;
-
-  private static final String PHOSPHORUS = "P";
-
-  private static final String ALPHACARBON = "CA";
-
-  private List<String> chainNames = new ArrayList<String>();
-
-  private Hashtable<String, String> chainFile = new Hashtable<String, String>();
+  public static final String CHIMERA_FEATURE_GROUP = "Chimera";
 
   /*
    * Object through which we talk to Chimera
    */
-  private ChimeraManager viewer;
+  private ChimeraManager chimeraManager;
 
   /*
    * Object which listens to Chimera notifications
@@ -91,39 +69,27 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   private AbstractRequestHandler chimeraListener;
 
   /*
-   * set if chimera state is being restored from some source - instructs binding
-   * not to apply default display style when structure set is updated for first
-   * time.
-   */
-  private boolean loadingFromArchive = false;
-
-  /*
-   * flag to indicate if the Chimera viewer should ignore sequence colouring
-   * events from the structure manager because the GUI is still setting up
-   */
-  private boolean loadingFinished = true;
-
-  /*
    * Map of ChimeraModel objects keyed by PDB full local file name
    */
-  private Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<String, List<ChimeraModel>>();
+  protected Map<String, List<ChimeraModel>> chimeraMaps = new LinkedHashMap<>();
 
   String lastHighlightCommand;
 
-  /*
-   * incremented every time a load notification is successfully handled -
-   * lightweight mechanism for other threads to detect when they can start
-   * referring to new structures.
+  /**
+   * Returns a model of the structure positions described by the Chimera format atomspec
+   * @param atomSpec
+   * @return
    */
-  private long loadNotifiesHandled = 0;
-
-  private Thread chimeraMonitor;
+  protected  AtomSpec parseAtomSpec(String atomSpec)
+  {
+    return AtomSpec.fromChimeraAtomspec(atomSpec);
+  }
 
   /**
    * Open a PDB structure file in Chimera and set up mappings from Jalview.
    * 
-   * We check if the PDB model id is already loaded in Chimera, if so don't
-   * reopen it. This is the case if Chimera has opened a saved session file.
+   * We check if the PDB model id is already loaded in Chimera, if so don't reopen
+   * it. This is the case if Chimera has opened a saved session file.
    * 
    * @param pe
    * @return
@@ -133,8 +99,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     String file = pe.getFile();
     try
     {
-      List<ChimeraModel> modelsToMap = new ArrayList<ChimeraModel>();
-      List<ChimeraModel> oldList = viewer.getModelList();
+      List<ChimeraModel> modelsToMap = new ArrayList<>();
+      List<ChimeraModel> oldList = chimeraManager.getModelList();
       boolean alreadyOpen = false;
 
       /*
@@ -155,16 +121,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
        */
       if (!alreadyOpen)
       {
-        viewer.openModel(file, pe.getId(), ModelType.PDB_MODEL);
-        List<ChimeraModel> newList = viewer.getModelList();
-        // JAL-1728 newList.removeAll(oldList) does not work
-        for (ChimeraModel cm : newList)
-        {
-          if (cm.getModelName().equals(pe.getId()))
-          {
-            modelsToMap.add(cm);
-          }
-        }
+        chimeraManager.openModel(file, pe.getId(), ModelType.PDB_MODEL);
+        addChimeraModel(pe, modelsToMap);
       }
 
       chimeraMaps.put(file, modelsToMap);
@@ -184,6 +142,31 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
+   * Adds the ChimeraModel corresponding to the given PDBEntry, based on model
+   * name matching PDB id
+   * 
+   * @param pe
+   * @param modelsToMap
+   */
+  protected void addChimeraModel(PDBEntry pe,
+          List<ChimeraModel> modelsToMap)
+  {
+    /*
+     * Chimera: query for actual models and find the one with
+     * matching model name - already set in viewer.openModel()
+     */
+    List<ChimeraModel> newList = chimeraManager.getModelList();
+    // JAL-1728 newList.removeAll(oldList) does not work
+    for (ChimeraModel cm : newList)
+    {
+      if (cm.getModelName().equals(pe.getId()))
+      {
+        modelsToMap.add(cm);
+      }
+    }
+  }
+
+  /**
    * Constructor
    * 
    * @param ssm
@@ -196,50 +179,27 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
           DataSourceType protocol)
   {
     super(ssm, pdbentry, sequenceIs, protocol);
-    viewer = new ChimeraManager(new StructureManager(true));
+    boolean chimeraX = ViewerType.CHIMERAX.equals(getViewerType());
+    chimeraManager = chimeraX ? new ChimeraXManager(new StructureManager(true)) : new ChimeraManager(new StructureManager(true));
+    setStructureCommands(chimeraX ? new ChimeraXCommands() : new ChimeraCommands());
   }
 
-  /**
-   * Starts a thread that waits for the Chimera process to finish, so that we
-   * can then close the associated resources. This avoids leaving orphaned
-   * Chimera viewer panels in Jalview if the user closes Chimera.
-   */
-  protected void startChimeraProcessMonitor()
+  @Override
+  protected ViewerType getViewerType()
   {
-    final Process p = viewer.getChimeraProcess();
-    chimeraMonitor = new Thread(new Runnable()
-    {
-
-      @Override
-      public void run()
-      {
-        try
-        {
-          p.waitFor();
-          JalviewStructureDisplayI display = getViewer();
-          if (display != null)
-          {
-            display.closeViewer(false);
-          }
-        } catch (InterruptedException e)
-        {
-          // exit thread if Chimera Viewer is closed in Jalview
-        }
-      }
-    });
-    chimeraMonitor.start();
+    return ViewerType.CHIMERA;
   }
 
   /**
-   * Start a dedicated HttpServer to listen for Chimera notifications, and tell
-   * it to start listening
+   * Start a dedicated HttpServer to listen for Chimera notifications, and tell it
+   * to start listening
    */
   public void startChimeraListener()
   {
     try
     {
       chimeraListener = new ChimeraListener(this);
-      viewer.startListening(chimeraListener.getUri());
+      startListening(chimeraListener.getUri());
     } catch (BindException e)
     {
       System.err.println(
@@ -248,308 +208,31 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Tells Chimera to display only the specified chains
-   * 
-   * @param toshow
-   */
-  public void showChains(List<String> toshow)
-  {
-    /*
-     * Construct a chimera command like
-     * 
-     * ~display #*;~ribbon #*;ribbon :.A,:.B
-     */
-    StringBuilder cmd = new StringBuilder(64);
-    boolean first = true;
-    for (String chain : toshow)
-    {
-      int modelNumber = getModelNoForChain(chain);
-      String showChainCmd = modelNumber == -1 ? ""
-              : modelNumber + ":." + chain.split(":")[1];
-      if (!first)
-      {
-        cmd.append(",");
-      }
-      cmd.append(showChainCmd);
-      first = false;
-    }
-
-    /*
-     * could append ";focus" to this command to resize the display to fill the
-     * window, but it looks more helpful not to (easier to relate chains to the
-     * whole)
-     */
-    final String command = "~display #*; ~ribbon #*; ribbon :"
-            + cmd.toString();
-    sendChimeraCommand(command, false);
-  }
-
-  /**
    * Close down the Jalview viewer and listener, and (optionally) the associated
    * Chimera window.
    */
+  @Override
   public void closeViewer(boolean closeChimera)
   {
-    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
-    if (closeChimera)
-    {
-      viewer.exitChimera();
-    }
+    super.closeViewer(closeChimera);
     if (this.chimeraListener != null)
     {
       chimeraListener.shutdown();
       chimeraListener = null;
     }
-    viewer = null;
-
-    if (chimeraMonitor != null)
-    {
-      chimeraMonitor.interrupt();
-    }
-    releaseUIResources();
-  }
-
-  @Override
-  public void colourByChain()
-  {
-    colourBySequence = false;
-    sendAsynchronousCommand("rainbow chain", COLOURING_CHIMERA);
-  }
-
-  /**
-   * Constructs and sends a Chimera command to colour by charge
-   * <ul>
-   * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
-   * <li>Lysine and Arginine (positive charge) blue</li>
-   * <li>Cysteine - yellow</li>
-   * <li>all others - white</li>
-   * </ul>
-   */
-  @Override
-  public void colourByCharge()
-  {
-    colourBySequence = false;
-    String command = "color white;color red ::ASP;color red ::GLU;color blue ::LYS;color blue ::ARG;color yellow ::CYS";
-    sendAsynchronousCommand(command, COLOURING_CHIMERA);
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  @Override
-  public String superposeStructures(AlignmentI[] _alignment,
-          int[] _refStructure, HiddenColumns[] _hiddenCols)
-  {
-    StringBuilder allComs = new StringBuilder(128);
-    String[] files = getStructureFiles();
-
-    if (!waitForFileLoad(files))
+    
+    /*
+     * the following call is added to avoid a stack trace error in Chimera
+     * after "stop really" is sent; Chimera > 1.14 will not need it; see also 
+     * http://plato.cgl.ucsf.edu/trac/chimera/ticket/17597
+     */
+    if (closeChimera && (getViewerType() == ViewerType.CHIMERA))
     {
-      return null;
+      chimeraManager.getChimeraProcess().destroy();
     }
 
-    refreshPdbEntries();
-    StringBuilder selectioncom = new StringBuilder(256);
-    for (int a = 0; a < _alignment.length; a++)
-    {
-      int refStructure = _refStructure[a];
-      AlignmentI alignment = _alignment[a];
-      HiddenColumns hiddenCols = _hiddenCols[a];
-
-      if (refStructure >= files.length)
-      {
-        System.err.println("Ignoring invalid reference structure value "
-                + refStructure);
-        refStructure = -1;
-      }
-
-      /*
-       * 'matched' bit i will be set for visible alignment columns i where
-       * all sequences have a residue with a mapping to the PDB structure
-       */
-      BitSet matched = new BitSet();
-      for (int m = 0; m < alignment.getWidth(); m++)
-      {
-        if (hiddenCols == null || hiddenCols.isVisible(m))
-        {
-          matched.set(m);
-        }
-      }
-
-      SuperposeData[] structures = new SuperposeData[files.length];
-      for (int f = 0; f < files.length; f++)
-      {
-        structures[f] = new SuperposeData(alignment.getWidth());
-      }
-
-      /*
-       * Calculate the superposable alignment columns ('matched'), and the
-       * corresponding structure residue positions (structures.pdbResNo)
-       */
-      int candidateRefStructure = findSuperposableResidues(alignment,
-              matched, structures);
-      if (refStructure < 0)
-      {
-        /*
-         * If no reference structure was specified, pick the first one that has
-         * a mapping in the alignment
-         */
-        refStructure = candidateRefStructure;
-      }
-
-      int nmatched = matched.cardinality();
-      if (nmatched < 4)
-      {
-        return MessageManager.formatMessage("label.insufficient_residues",
-                nmatched);
-      }
-
-      /*
-       * Generate select statements to select regions to superimpose structures
-       */
-      String[] selcom = new String[files.length];
-      for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-      {
-        String chainCd = "." + structures[pdbfnum].chain;
-        int lpos = -1;
-        boolean run = false;
-        StringBuilder molsel = new StringBuilder();
-
-        int nextColumnMatch = matched.nextSetBit(0);
-        while (nextColumnMatch != -1)
-        {
-          int pdbResNum = structures[pdbfnum].pdbResNo[nextColumnMatch];
-          if (lpos != pdbResNum - 1)
-          {
-            /*
-             * discontiguous - append last residue now
-             */
-            if (lpos != -1)
-            {
-              molsel.append(String.valueOf(lpos));
-              molsel.append(chainCd);
-              molsel.append(",");
-            }
-            run = false;
-          }
-          else
-          {
-            /*
-             * extending a contiguous run
-             */
-            if (!run)
-            {
-              /*
-               * start the range selection
-               */
-              molsel.append(String.valueOf(lpos));
-              molsel.append("-");
-            }
-            run = true;
-          }
-          lpos = pdbResNum;
-          nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
-        }
-
-        /*
-         * and terminate final selection
-         */
-        if (lpos != -1)
-        {
-          molsel.append(String.valueOf(lpos));
-          molsel.append(chainCd);
-        }
-        if (molsel.length() > 1)
-        {
-          selcom[pdbfnum] = molsel.toString();
-          selectioncom.append("#").append(String.valueOf(pdbfnum))
-                  .append(":");
-          selectioncom.append(selcom[pdbfnum]);
-          selectioncom.append(" ");
-          if (pdbfnum < files.length - 1)
-          {
-            selectioncom.append("| ");
-          }
-        }
-        else
-        {
-          selcom[pdbfnum] = null;
-        }
-      }
-
-      StringBuilder command = new StringBuilder(256);
-      for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
-      {
-        if (pdbfnum == refStructure || selcom[pdbfnum] == null
-                || selcom[refStructure] == null)
-        {
-          continue;
-        }
-        if (command.length() > 0)
-        {
-          command.append(";");
-        }
-
-        /*
-         * Form Chimera match command, from the 'new' structure to the
-         * 'reference' structure e.g. (50 residues, chain B/A, alphacarbons):
-         * 
-         * match #1:1-30.B,81-100.B@CA #0:21-40.A,61-90.A@CA
-         * 
-         * @see
-         * https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/midas/match.html
-         */
-        command.append("match ").append(getModelSpec(pdbfnum)).append(":");
-        command.append(selcom[pdbfnum]);
-        command.append("@").append(
-                structures[pdbfnum].isRna ? PHOSPHORUS : ALPHACARBON);
-        // JAL-1757 exclude alternate CA locations
-        command.append(NO_ALTLOCS);
-        command.append(" ").append(getModelSpec(refStructure)).append(":");
-        command.append(selcom[refStructure]);
-        command.append("@").append(
-                structures[refStructure].isRna ? PHOSPHORUS : ALPHACARBON);
-        command.append(NO_ALTLOCS);
-      }
-      if (selectioncom.length() > 0)
-      {
-        if (debug)
-        {
-          System.out.println("Select regions:\n" + selectioncom.toString());
-          System.out.println(
-                  "Superimpose command(s):\n" + command.toString());
-        }
-        allComs.append("~display all; chain @CA|P; ribbon ")
-                .append(selectioncom.toString())
-                .append(";" + command.toString());
-      }
-    }
-
-    String error = null;
-    if (selectioncom.length() > 0)
-    {
-      // TODO: visually distinguish regions that were superposed
-      if (selectioncom.substring(selectioncom.length() - 1).equals("|"))
-      {
-        selectioncom.setLength(selectioncom.length() - 1);
-      }
-      if (debug)
-      {
-        System.out.println("Select regions:\n" + selectioncom.toString());
-      }
-      allComs.append("; ~display all; chain @CA|P; ribbon ")
-              .append(selectioncom.toString()).append("; focus");
-      List<String> chimeraReplies = sendChimeraCommand(allComs.toString(),
-              true);
-      for (String reply : chimeraReplies)
-      {
-        if (reply.toLowerCase().contains("unequal numbers of atoms"))
-        {
-          error = reply;
-        }
-      }
-    }
-    return error;
+    chimeraManager.clearOnChimeraExit();
+    chimeraManager = null;
   }
 
   /**
@@ -569,7 +252,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   {
     if (pdbfnum < 0 || pdbfnum >= getPdbCount())
     {
-      return "";
+      return "#" + pdbfnum; // temp hack for ChimeraX
     }
 
     /*
@@ -592,16 +275,15 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    */
   public boolean launchChimera()
   {
-    if (viewer.isChimeraLaunched())
+    if (chimeraManager.isChimeraLaunched())
     {
       return true;
     }
 
-    boolean launched = viewer
-            .launchChimera(StructureManager.getChimeraPaths());
+    boolean launched = chimeraManager.launchChimera(getChimeraPaths());
     if (launched)
     {
-      startChimeraProcessMonitor();
+      startExternalViewerMonitor(chimeraManager.getChimeraProcess());
     }
     else
     {
@@ -611,143 +293,63 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
+   * Returns a list of candidate paths to the Chimera program executable
+   * 
+   * @return
+   */
+  protected List<String> getChimeraPaths()
+  {
+    return StructureManager.getChimeraPaths(false);
+  }
+
+  /**
    * Answers true if the Chimera process is still running, false if ended or not
    * started.
    * 
    * @return
    */
-  public boolean isChimeraRunning()
+  @Override
+  public boolean isViewerRunning()
   {
-    return viewer.isChimeraLaunched();
+    return chimeraManager.isChimeraLaunched();
   }
 
   /**
    * Send a command to Chimera, and optionally log and return any responses.
-   * <p>
-   * Does nothing, and returns null, if the command is the same as the last one
-   * sent [why?].
    * 
    * @param command
    * @param getResponse
    */
-  public List<String> sendChimeraCommand(final String command,
+  @Override
+  public List<String> executeCommand(final StructureCommandI command,
           boolean getResponse)
   {
-    if (viewer == null)
+    if (chimeraManager == null || command == null)
     {
       // ? thread running after viewer shut down
       return null;
     }
     List<String> reply = null;
-    viewerCommandHistory(false);
-    if (true /*lastCommand == null || !lastCommand.equals(command)*/)
+    // trim command or it may never find a match in the replyLog!!
+    String cmd = command.getCommand().trim();
+    List<String> lastReply = chimeraManager
+            .sendChimeraCommand(cmd, getResponse);
+    if (getResponse)
     {
-      // trim command or it may never find a match in the replyLog!!
-      List<String> lastReply = viewer.sendChimeraCommand(command.trim(),
-              getResponse);
-      if (getResponse)
-      {
-        reply = lastReply;
-        if (debug)
-        {
-          log("Response from command ('" + command + "') was:\n"
-                  + lastReply);
-        }
+      reply = lastReply;
+      if (Cache.log.isDebugEnabled()) {
+        Cache.log.debug(
+              "Response from command ('" + cmd + "') was:\n" + lastReply); 
       }
     }
-    viewerCommandHistory(true);
 
     return reply;
   }
 
-  /**
-   * Send a Chimera command asynchronously in a new thread. If the progress
-   * message is not null, display this message while the command is executing.
-   * 
-   * @param command
-   * @param progressMsg
-   */
-  protected abstract void sendAsynchronousCommand(String command,
-          String progressMsg);
-
-  /**
-   * Sends a set of colour commands to the structure viewer
-   * 
-   * @param colourBySequenceCommands
-   */
-  @Override
-  protected void colourBySequence(
-          StructureMappingcommandSet[] colourBySequenceCommands)
-  {
-    for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
-    {
-      for (String command : cpdbbyseq.commands)
-      {
-        sendAsynchronousCommand(command, COLOURING_CHIMERA);
-      }
-    }
-  }
-
-  /**
-   * @param files
-   * @param sr
-   * @param viewPanel
-   * @return
-   */
-  @Override
-  protected StructureMappingcommandSet[] getColourBySequenceCommands(
-          String[] files, SequenceRenderer sr, AlignmentViewPanel viewPanel)
-  {
-    return ChimeraCommands.getColourBySequenceCommand(getSsm(), files,
-            getSequence(), sr, viewPanel);
-  }
-
-  /**
-   * @param command
-   */
-  protected void executeWhenReady(String command)
-  {
-    waitForChimera();
-    sendChimeraCommand(command, false);
-    waitForChimera();
-  }
-
-  private void waitForChimera()
-  {
-    while (viewer != null && viewer.isBusy())
-    {
-      try
-      {
-        Thread.sleep(15);
-      } catch (InterruptedException q)
-      {
-      }
-    }
-  }
-
-  // End StructureListener
-  // //////////////////////////
-
-  /**
-   * instruct the Jalview binding to update the pdbentries vector if necessary
-   * prior to matching the viewer's contents to the list of structure files
-   * Jalview knows about.
-   */
-  public abstract void refreshPdbEntries();
-
-  /**
-   * map between index of model filename returned from getPdbFile and the first
-   * index of models from this file in the viewer. Note - this is not trimmed -
-   * use getPdbFile to get number of unique models.
-   */
-  private int _modelFileNameMap[];
-
-  // ////////////////////////////////
-  // /StructureListener
   @Override
   public synchronized String[] getStructureFiles()
   {
-    if (viewer == null)
+    if (chimeraManager == null)
     {
       return new String[0];
     }
@@ -757,9 +359,8 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Construct and send a command to highlight zero, one or more atoms. We do
-   * this by sending an "rlabel" command to show the residue label at that
-   * position.
+   * Construct and send a command to highlight zero, one or more atoms. We do this
+   * by sending an "rlabel" command to show the residue label at that position.
    */
   @Override
   public void highlightAtoms(List<AtomSpec> atoms)
@@ -769,6 +370,7 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       return;
     }
 
+    boolean forChimeraX = chimeraManager.isChimeraX();
     StringBuilder cmd = new StringBuilder(128);
     boolean first = true;
     boolean found = false;
@@ -783,18 +385,26 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
       {
         if (first)
         {
-          cmd.append("rlabel #").append(cms.get(0).getModelNumber())
-                  .append(":");
+          cmd.append(forChimeraX ? "label #" : "rlabel #");
         }
         else
         {
           cmd.append(",");
         }
         first = false;
-        cmd.append(pdbResNum);
-        if (!chain.equals(" "))
+        if (forChimeraX)
         {
-          cmd.append(".").append(chain);
+          cmd.append(cms.get(0).getModelNumber())
+                  .append("/").append(chain).append(":").append(pdbResNum);
+        }
+        else
+        {
+          cmd.append(cms.get(0).getModelNumber())
+                  .append(":").append(pdbResNum);
+          if (!chain.equals(" ") && !forChimeraX)
+          {
+            cmd.append(".").append(chain);
+          }
         }
         found = true;
       }
@@ -814,11 +424,11 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
      */
     if (lastHighlightCommand != null)
     {
-      viewer.sendChimeraCommand("~" + lastHighlightCommand, false);
+      executeCommand(false,  null,  new StructureCommand("~" + lastHighlightCommand));
     }
     if (found)
     {
-      viewer.sendChimeraCommand(command, false);
+      executeCommand(false,  null,  new StructureCommand(command));
     }
     this.lastHighlightCommand = command;
   }
@@ -831,24 +441,56 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     /*
      * Ask Chimera for its current selection
      */
-    List<String> selection = viewer.getSelectedResidueSpecs();
+    StructureCommandI command = getCommandGenerator().getSelectedResidues();
+    
+    Runnable action = new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        List<String> chimeraReply = executeCommand(command, true);
+        
+        List<String> selectedResidues = new ArrayList<>();
+        if (chimeraReply != null)
+        {
+          /*
+           * expect 0, 1 or more lines of the format either
+           * Chimera:
+           * residue id #0:43.A type GLY
+           * ChimeraX:
+           * residue id /A:89 name THR index 88
+           * We are only interested in the atomspec (third token of the reply)
+           */
+          for (String inputLine : chimeraReply)
+          {
+            String[] inputLineParts = inputLine.split("\\s+");
+            if (inputLineParts.length >= 5)
+            {
+              selectedResidues.add(inputLineParts[2]);
+            }
+          }
+        }
 
-    /*
-     * Parse model number, residue and chain for each selected position,
-     * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
-     */
-    List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
-            selection);
+        /*
+         * Parse model number, residue and chain for each selected position,
+         * formatted as #0:123.A or #1.2:87.B (#model.submodel:residue.chain)
+         */
+        List<AtomSpec> atomSpecs = convertStructureResiduesToAlignment(
+                selectedResidues);
 
-    /*
-     * Broadcast the selection (which may be empty, if the user just cleared all
-     * selections)
-     */
-    getSsm().mouseOverStructure(atomSpecs);
+        /*
+         * Broadcast the selection (which may be empty, if the user just cleared all
+         * selections)
+         */
+        getSsm().mouseOverStructure(atomSpecs);
+        
+      }
+    };
+    new Thread(action).start();
   }
 
   /**
-   * Converts a list of Chimera atomspecs to a list of AtomSpec representing the
+   * Converts a list of Chimera(X) atomspecs to a list of AtomSpec representing the
    * corresponding residues (if any) in Jalview
    * 
    * @param structureSelection
@@ -857,18 +499,18 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   protected List<AtomSpec> convertStructureResiduesToAlignment(
           List<String> structureSelection)
   {
-    List<AtomSpec> atomSpecs = new ArrayList<AtomSpec>();
+    List<AtomSpec> atomSpecs = new ArrayList<>();
     for (String atomSpec : structureSelection)
     {
       try
       {
-        AtomSpec spec = AtomSpec.fromChimeraAtomspec(atomSpec);
+        AtomSpec spec = parseAtomSpec(atomSpec);
         String pdbfilename = getPdbFileForModel(spec.getModelNumber());
         spec.setPdbFile(pdbfilename);
         atomSpecs.add(spec);
       } catch (IllegalArgumentException e)
       {
-        System.err.println("Failed to parse atomspec: " + atomSpec);
+        Cache.log.error("Failed to parse atomspec: " + atomSpec);
       }
     }
     return atomSpecs;
@@ -903,196 +545,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     System.err.println("## Chimera log: " + message);
   }
 
-  private void viewerCommandHistory(boolean enable)
-  {
-    // log("(Not yet implemented) History "
-    // + ((debug || enable) ? "on" : "off"));
-  }
-
-  public long getLoadNotifiesHandled()
-  {
-    return loadNotifiesHandled;
-  }
-
-  @Override
-  public void setJalviewColourScheme(ColourSchemeI cs)
-  {
-    colourBySequence = false;
-
-    if (cs == null)
-    {
-      return;
-    }
-
-    // Chimera expects RBG values in the range 0-1
-    final double normalise = 255D;
-    viewerCommandHistory(false);
-    StringBuilder command = new StringBuilder(128);
-
-    List<String> residueSet = ResidueProperties.getResidues(isNucleotide(),
-            false);
-    for (String resName : residueSet)
-    {
-      char res = resName.length() == 3
-              ? ResidueProperties.getSingleCharacterCode(resName)
-              : resName.charAt(0);
-      Color col = cs.findColour(res, 0, null, null, 0f);
-      command.append("color " + col.getRed() / normalise + ","
-              + col.getGreen() / normalise + "," + col.getBlue() / normalise
-              + " ::" + resName + ";");
-    }
-
-    sendAsynchronousCommand(command.toString(), COLOURING_CHIMERA);
-    viewerCommandHistory(true);
-  }
-
-  /**
-   * called when the binding thinks the UI needs to be refreshed after a Chimera
-   * state change. this could be because structures were loaded, or because an
-   * error has occurred.
-   */
-  public abstract void refreshGUI();
-
-  @Override
-  public void setLoadingFromArchive(boolean loadingFromArchive)
-  {
-    this.loadingFromArchive = loadingFromArchive;
-  }
-
-  /**
-   * 
-   * @return true if Chimeral is still restoring state or loading is still going
-   *         on (see setFinsihedLoadingFromArchive)
-   */
-  @Override
-  public boolean isLoadingFromArchive()
-  {
-    return loadingFromArchive && !loadingFinished;
-  }
-
-  /**
-   * modify flag which controls if sequence colouring events are honoured by the
-   * binding. Should be true for normal operation
-   * 
-   * @param finishedLoading
-   */
-  @Override
-  public void setFinishedLoadingFromArchive(boolean finishedLoading)
-  {
-    loadingFinished = finishedLoading;
-  }
-
-  /**
-   * Send the Chimera 'background solid <color>" command.
-   * 
-   * @see https
-   *      ://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/background
-   *      .html
-   * @param col
-   */
-  @Override
-  public void setBackgroundColour(Color col)
-  {
-    viewerCommandHistory(false);
-    double normalise = 255D;
-    final String command = "background solid " + col.getRed() / normalise
-            + "," + col.getGreen() / normalise + ","
-            + col.getBlue() / normalise + ";";
-    viewer.sendChimeraCommand(command, false);
-    viewerCommandHistory(true);
-  }
-
-  /**
-   * Ask Chimera to save its session to the given file. Returns true if
-   * successful, else false.
-   * 
-   * @param filepath
-   * @return
-   */
-  public boolean saveSession(String filepath)
-  {
-    if (isChimeraRunning())
-    {
-      List<String> reply = viewer.sendChimeraCommand("save " + filepath,
-              true);
-      if (reply.contains("Session written"))
-      {
-        return true;
-      }
-      else
-      {
-        Cache.log
-                .error("Error saving Chimera session: " + reply.toString());
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Ask Chimera to open a session file. Returns true if successful, else false.
-   * The filename must have a .py extension for this command to work.
-   * 
-   * @param filepath
-   * @return
-   */
-  public boolean openSession(String filepath)
-  {
-    sendChimeraCommand("open " + filepath, true);
-    // todo: test for failure - how?
-    return true;
-  }
-
-  /**
-   * Returns a list of chains mapped in this viewer. Note this list is not
-   * currently scoped per structure.
-   * 
-   * @return
-   */
-  @Override
-  public List<String> getChainNames()
-  {
-    return chainNames;
-  }
-
-  /**
-   * Send a 'focus' command to Chimera to recentre the visible display
-   */
-  public void focusView()
-  {
-    sendChimeraCommand("focus", false);
-  }
-
-  /**
-   * Send a 'show' command for all atoms in the currently selected columns
-   * 
-   * TODO: pull up to abstract structure viewer interface
-   * 
-   * @param vp
-   */
-  public void highlightSelection(AlignmentViewPanel vp)
-  {
-    List<Integer> cols = vp.getAlignViewport().getColumnSelection()
-            .getSelected();
-    AlignmentI alignment = vp.getAlignment();
-    StructureSelectionManager sm = getSsm();
-    for (SequenceI seq : alignment.getSequences())
-    {
-      /*
-       * convert selected columns into sequence positions
-       */
-      int[] positions = new int[cols.size()];
-      int i = 0;
-      for (Integer col : cols)
-      {
-        positions[i++] = seq.findPosition(col);
-      }
-      sm.highlightStructure(this, seq, positions);
-    }
-  }
-
   /**
    * Constructs and send commands to Chimera to set attributes on residues for
-   * features visible in Jalview
+   * features visible in Jalview.
+   * <p>
+   * The syntax is: setattr r &lt;attName&gt; &lt;attValue&gt; &lt;atomSpec&gt;
+   * <p>
+   * For example: setattr r jv_chain "Ferredoxin-1, Chloroplastic" #0:94.A
    * 
    * @param avp
    * @return
@@ -1100,54 +559,46 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   public int sendFeaturesToViewer(AlignmentViewPanel avp)
   {
     // TODO refactor as required to pull up to an interface
-    AlignmentI alignment = avp.getAlignment();
 
-    String[] files = getStructureFiles();
-    if (files == null)
-    {
-      return 0;
-    }
-
-    StructureMappingcommandSet commandSet = ChimeraCommands
-            .getSetAttributeCommandsForFeatures(getSsm(), files,
-                    getSequence(), avp);
-    String[] commands = commandSet.commands;
-    if (commands.length > 10)
+    Map<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
+            avp);
+    List<StructureCommandI> commands = getCommandGenerator()
+            .setAttributes(featureValues);
+    if (commands.size() > 10)
     {
       sendCommandsByFile(commands);
     }
     else
     {
-      for (String command : commands)
-      {
-        sendAsynchronousCommand(command, null);
-      }
+      executeCommands(commands, false, null);
     }
-    return commands.length;
+    return commands.size();
   }
 
   /**
-   * Write commands to a temporary file, and send a command to Chimera to open
-   * the file as a commands script. For use when sending a large number of
-   * separate commands would overload the REST interface mechanism.
+   * Write commands to a temporary file, and send a command to Chimera to open the
+   * file as a commands script. For use when sending a large number of separate
+   * commands would overload the REST interface mechanism.
    * 
    * @param commands
    */
-  protected void sendCommandsByFile(String[] commands)
+  protected void sendCommandsByFile(List<StructureCommandI> commands)
   {
     try
     {
-      File tmp = File.createTempFile("chim", ".com");
+      File tmp = File.createTempFile("chim", getCommandFileExtension());
       tmp.deleteOnExit();
       PrintWriter out = new PrintWriter(new FileOutputStream(tmp));
-      for (String command : commands)
+      for (StructureCommandI command : commands)
       {
-        out.println(command);
+        out.println(command.getCommand());
       }
       out.flush();
       out.close();
       String path = tmp.getAbsolutePath();
-      sendAsynchronousCommand("open cmd:" + path, null);
+      StructureCommandI command = getCommandGenerator()
+              .openCommandFile(path);
+      executeCommand(false, null, command);
     } catch (IOException e)
     {
       System.err.println("Sending commands to Chimera via file failed with "
@@ -1156,33 +607,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
   }
 
   /**
-   * Get Chimera residues which have the named attribute, find the mapped
-   * positions in the Jalview sequence(s), and set as sequence features
-   * 
-   * @param attName
-   * @param alignmentPanel
+   * Returns the file extension required for a file of commands to be read by
+   * the structure viewer
+   * @return
    */
-  public void copyStructureAttributesToFeatures(String attName,
-          AlignmentViewPanel alignmentPanel)
+  protected String getCommandFileExtension()
   {
-    // todo pull up to AAStructureBindingModel (and interface?)
-
-    /*
-     * ask Chimera to list residues with the attribute, reporting its value
-     */
-    // this alternative command
-    // list residues spec ':*/attName' attr attName
-    // doesn't report 'None' values (which is good), but
-    // fails for 'average.bfactor' (which is bad):
-
-    String cmd = "list residues attr '" + attName + "'";
-    List<String> residues = sendChimeraCommand(cmd, true);
-
-    boolean featureAdded = createFeaturesForAttributes(attName, residues);
-    if (featureAdded)
-    {
-      alignmentPanel.getFeatureRenderer().featuresAdded();
-    }
+    return ".com";
   }
 
   /**
@@ -1198,12 +629,12 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
    * 
    * @param attName
    * @param residues
-   * @return
+   * @return the number of features added
    */
-  protected boolean createFeaturesForAttributes(String attName,
+  protected int createFeaturesForAttributes(String attName,
           List<String> residues)
   {
-    boolean featureAdded = false;
+    int featuresAdded = 0;
     String featureGroup = getViewerFeatureGroup();
 
     for (String residue : residues)
@@ -1228,10 +659,10 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
 
       try
       {
-        spec = AtomSpec.fromChimeraAtomspec(atomSpec);
+        spec = parseAtomSpec(atomSpec);
       } catch (IllegalArgumentException e)
       {
-        System.err.println("Problem parsing atomspec " + atomSpec);
+        Cache.log.error("Problem parsing atomspec " + atomSpec);
         continue;
       }
 
@@ -1271,10 +702,13 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
                 start, end, score, featureGroup);
         // todo: should SequenceFeature have an explicit property for chain?
         // note: repeating the action shouldn't duplicate features
-        featureAdded |= seq.addSequenceFeature(sf);
+        if (seq.addSequenceFeature(sf))
+        {
+          featuresAdded++;
+        }
       }
     }
-    return featureAdded;
+    return featuresAdded;
   }
 
   /**
@@ -1289,23 +723,63 @@ public abstract class JalviewChimeraBinding extends AAStructureBindingModel
     return CHIMERA_FEATURE_GROUP;
   }
 
-  public Hashtable<String, String> getChainFile()
+  @Override
+  public String getModelIdForFile(String pdbFile)
+  {
+    List<ChimeraModel> foundModels = chimeraMaps.get(pdbFile);
+    if (foundModels != null && !foundModels.isEmpty())
+    {
+      return String.valueOf(foundModels.get(0).getModelNumber());
+    }
+    return "";
+  }
+
+  /**
+   * Answers a (possibly empty) list of attribute names in Chimera[X], excluding
+   * any which were added from Jalview
+   * 
+   * @return
+   */
+  public List<String> getChimeraAttributes()
   {
-    return chainFile;
+    List<String> attributes = new ArrayList<>();
+    StructureCommandI command = getCommandGenerator().listResidueAttributes();
+    final List<String> reply = executeCommand(command, true);
+    if (reply != null)
+    {
+      for (String inputLine : reply)
+      {
+        String[] lineParts = inputLine.split("\\s");
+        if (lineParts.length == 2 && lineParts[0].equals("resattr"))
+        {
+          String attName = lineParts[1];
+          /*
+           * exclude attributes added from Jalview
+           */
+          if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
+          {
+            attributes.add(attName);
+          }
+        }
+      }
+    }
+    return attributes;
   }
 
-  public List<ChimeraModel> getChimeraModelByChain(String chain)
+  /**
+   * Returns the file extension to use for a saved viewer session file (.py)
+   * 
+   * @return
+   */
+  @Override
+  public String getSessionFileExtension()
   {
-    return chimeraMaps.get(chainFile.get(chain));
+    return CHIMERA_SESSION_EXTENSION;
   }
 
-  public int getModelNoForChain(String chain)
+  @Override
+  public String getHelpURL()
   {
-    List<ChimeraModel> foundModels = getChimeraModelByChain(chain);
-    if (foundModels != null && !foundModels.isEmpty())
-    {
-      return foundModels.get(0).getModelNumber();
-    }
-    return -1;
+    return "https://www.cgl.ucsf.edu/chimera/docs/UsersGuide";
   }
 }
index 617d4ea..7a8a695 100644 (file)
  */
 package jalview.fts.core;
 
-import jalview.fts.api.FTSDataColumnI;
-import jalview.fts.api.FTSDataColumnI.FTSDataColumnGroupI;
-import jalview.fts.api.FTSRestClientI;
-import jalview.util.MessageManager;
-
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -33,6 +28,10 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Objects;
 
+import jalview.fts.api.FTSDataColumnI;
+import jalview.fts.api.FTSDataColumnI.FTSDataColumnGroupI;
+import jalview.fts.api.FTSRestClientI;
+
 /**
  * Base class providing implementation for common methods defined in
  * FTSRestClientI
@@ -456,8 +455,7 @@ public abstract class FTSRestClient implements FTSRestClientI
       break;
 
     case 410:
-      message = MessageManager.formatMessage(
-              service + " rest services no longer available!");
+      message = service + " rest services no longer available!";
       break;
     case 403:
     case 404:
index c5a1b66..fb9b707 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.gui;
 
 import jalview.util.MessageManager;
index 7818748..910ab63 100644 (file)
@@ -414,7 +414,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     addKeyListener();
 
-    final List<AlignmentPanel> selviews = new ArrayList<>();
+    final List<AlignmentViewPanel> selviews = new ArrayList<>();
     final List<AlignmentPanel> origview = new ArrayList<>();
     final String menuLabel = MessageManager
             .getString("label.copy_format_from");
@@ -2512,7 +2512,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     viewport.setSelectionGroup(null);
     viewport.getColumnSelection().clear();
-    viewport.setSelectionGroup(null);
+    viewport.setSearchResults(null);
     alignPanel.getIdPanel().getIdCanvas().searchResults = null;
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
index e89c1c2..ad013f5 100644 (file)
@@ -53,8 +53,6 @@ import net.miginfocom.swing.MigLayout;
 @SuppressWarnings("serial")
 public class AnnotationColourChooser extends AnnotationRowFilter
 {
-  private static final int ONETHOUSAND = 1000;
-
   private ColourSchemeI oldcs;
 
   private JButton defColours;
@@ -147,7 +145,8 @@ public class AnnotationColourChooser extends AnnotationRowFilter
                 "error.implementation_error_dont_know_about_threshold_setting"));
       }
       thresholdIsMin.setSelected(acg.isThresholdIsMinMax());
-      thresholdValue.setText("" + acg.getAnnotationThreshold());
+      thresholdValue
+              .setText(String.valueOf(acg.getAnnotationThreshold()));
     }
 
     jbInit();
@@ -329,7 +328,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       {
         updateView();
       }
-      getCurrentAnnotation().threshold.value = slider.getValue() / 1000f;
+      getCurrentAnnotation().threshold.value = getSliderValue();
       propagateSeqAssociatedThreshold(updateAllAnnotation,
               getCurrentAnnotation());
       ap.paintAlignment(false, false);
@@ -369,6 +368,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     thresholdValue.setEnabled(true);
     thresholdIsMin.setEnabled(!useOriginalColours.isSelected());
 
+    final AlignmentAnnotation currentAnnotation = getCurrentAnnotation();
     if (selectedThresholdItem == AnnotationColourGradient.NO_THRESHOLD)
     {
       slider.setEnabled(false);
@@ -377,33 +377,26 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       thresholdIsMin.setEnabled(false);
     }
     else if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD
-            && getCurrentAnnotation().threshold == null)
+            && currentAnnotation.threshold == null)
     {
-      getCurrentAnnotation().setThreshold(new GraphLine(
-              (getCurrentAnnotation().graphMax
-                      - getCurrentAnnotation().graphMin) / 2f,
+      currentAnnotation.setThreshold(new GraphLine(
+              (currentAnnotation.graphMax - currentAnnotation.graphMin)
+                      / 2f,
               "Threshold", Color.black));
     }
 
     if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD)
     {
       adjusting = true;
-      float range = getCurrentAnnotation().graphMax * ONETHOUSAND
-              - getCurrentAnnotation().graphMin * ONETHOUSAND;
-
-      slider.setMinimum(
-              (int) (getCurrentAnnotation().graphMin * ONETHOUSAND));
-      slider.setMaximum(
-              (int) (getCurrentAnnotation().graphMax * ONETHOUSAND));
-      slider.setValue(
-              (int) (getCurrentAnnotation().threshold.value * ONETHOUSAND));
-      thresholdValue.setText(getCurrentAnnotation().threshold.value + "");
-      slider.setMajorTickSpacing((int) (range / 10f));
+      setSliderModel(currentAnnotation.graphMin, currentAnnotation.graphMax,
+              currentAnnotation.threshold.value);
       slider.setEnabled(true);
+
+      setThresholdValueText();
       thresholdValue.setEnabled(true);
       adjusting = false;
     }
-    colorAlignmentContaining(getCurrentAnnotation(), selectedThresholdItem);
+    colorAlignmentContaining(currentAnnotation, selectedThresholdItem);
 
     ap.alignmentChanged();
   }
index fbc93b5..589f4bd 100644 (file)
@@ -21,6 +21,7 @@
 
 package jalview.gui;
 
+import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.HiddenColumns;
 import jalview.io.cache.JvCacheableInputBox;
 import jalview.schemes.AnnotationColourGradient;
@@ -255,7 +256,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
   {
     if (slider.isEnabled())
     {
-      getCurrentAnnotation().threshold.value = slider.getValue() / 1000f;
+      getCurrentAnnotation().threshold.value = getSliderValue();
       updateView();
       propagateSeqAssociatedThreshold(updateAllAnnotation,
               getCurrentAnnotation());
@@ -285,6 +286,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
     thresholdValue.setEnabled(true);
     percentThreshold.setEnabled(true);
 
+    final AlignmentAnnotation currentAnnotation = getCurrentAnnotation();
     if (selectedThresholdItem == AnnotationColourGradient.NO_THRESHOLD)
     {
       slider.setEnabled(false);
@@ -295,26 +297,22 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
     }
     else if (selectedThresholdItem != AnnotationColourGradient.NO_THRESHOLD)
     {
-      if (getCurrentAnnotation().threshold == null)
+      if (currentAnnotation.threshold == null)
       {
-        getCurrentAnnotation().setThreshold(new jalview.datamodel.GraphLine(
-                (getCurrentAnnotation().graphMax
-                        - getCurrentAnnotation().graphMin) / 2f,
+        currentAnnotation.setThreshold(new jalview.datamodel.GraphLine(
+                (currentAnnotation.graphMax
+                        - currentAnnotation.graphMin) / 2f,
                 "Threshold", Color.black));
       }
 
       adjusting = true;
-      float range = getCurrentAnnotation().graphMax * 1000
-              - getCurrentAnnotation().graphMin * 1000;
 
-      slider.setMinimum((int) (getCurrentAnnotation().graphMin * 1000));
-      slider.setMaximum((int) (getCurrentAnnotation().graphMax * 1000));
-      slider.setValue(
-              (int) (getCurrentAnnotation().threshold.value * 1000));
+      setSliderModel(currentAnnotation.graphMin,
+              currentAnnotation.graphMax,
+              currentAnnotation.threshold.value);
 
       setThresholdValueText();
 
-      slider.setMajorTickSpacing((int) (range / 10f));
       slider.setEnabled(true);
       thresholdValue.setEnabled(true);
       adjusting = false;
@@ -322,10 +320,10 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
       // build filter params
       filterParams.setThresholdType(
               AnnotationFilterParameter.ThresholdType.NO_THRESHOLD);
-      if (getCurrentAnnotation().isQuantitative())
+      if (currentAnnotation.isQuantitative())
       {
         filterParams
-                .setThresholdValue(getCurrentAnnotation().threshold.value);
+                .setThresholdValue(currentAnnotation.threshold.value);
 
         if (selectedThresholdItem == AnnotationColourGradient.ABOVE_THRESHOLD)
         {
@@ -381,7 +379,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
     // adding them to the selection
     av.showAllHiddenColumns();
     av.getColumnSelection().filterAnnotations(
-            getCurrentAnnotation().annotations, filterParams);
+            currentAnnotation.annotations, filterParams);
 
     boolean hideCols = getActionOption() == ACTION_OPTION_HIDE;
     if (hideCols)
index f13cb10..6f72e10 100644 (file)
@@ -35,6 +35,8 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.math.BigDecimal;
+import java.math.MathContext;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Vector;
@@ -44,7 +46,6 @@ import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
-import javax.swing.JSlider;
 import javax.swing.JTextField;
 import javax.swing.event.ChangeEvent;
 import javax.swing.event.ChangeListener;
@@ -52,6 +53,10 @@ import javax.swing.event.ChangeListener;
 @SuppressWarnings("serial")
 public abstract class AnnotationRowFilter extends JPanel
 {
+  private static final String TWO_DP = "%.2f";
+
+  private final static MathContext FOUR_SIG_FIG = new MathContext(4);
+
   protected AlignViewport av;
 
   protected AlignmentPanel ap;
@@ -64,7 +69,7 @@ public abstract class AnnotationRowFilter extends JPanel
 
   protected JCheckBox percentThreshold = new JCheckBox();
 
-  protected JSlider slider = new JSlider();
+  protected Slider slider;
 
   protected JTextField thresholdValue = new JTextField(20);
 
@@ -103,6 +108,8 @@ public abstract class AnnotationRowFilter extends JPanel
   {
     this.av = viewport;
     this.ap = alignPanel;
+    this.slider = new Slider(0f, 100f, 50f);
+
     thresholdValue.addFocusListener(new FocusAdapter()
     {
       @Override
@@ -140,16 +147,62 @@ public abstract class AnnotationRowFilter extends JPanel
     adjusting = true;
     if (percentThreshold.isSelected())
     {
-      thresholdValue.setText("" + (slider.getValue() - slider.getMinimum())
-              * 100f / (slider.getMaximum() - slider.getMinimum()));
+      thresholdValue
+              .setText(String.format(TWO_DP, getSliderPercentageValue()));
     }
     else
     {
-      thresholdValue.setText((slider.getValue() / 1000f) + "");
+      /*
+       * round to 4 significant digits without trailing zeroes
+       */
+      float f = getSliderValue();
+      BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
+              .stripTrailingZeros();
+      thresholdValue.setText(formatted.toPlainString());
     }
     adjusting = oldadj;
   }
 
+  /**
+   * Answers the value of the slider position (descaled to 'true' value)
+   * 
+   * @return
+   */
+  protected float getSliderValue()
+  {
+    return slider.getSliderValue();
+  }
+
+  /**
+   * Sets the slider value (scaled from the true value to the slider range)
+   * 
+   * @param value
+   */
+  protected void setSliderValue(float value)
+  {
+    slider.setSliderValue(value);
+  }
+  /**
+   * Answers the value of the slider position as a percentage between minimum and
+   * maximum of its range
+   * 
+   * @return
+   */
+  protected float getSliderPercentageValue()
+  {
+    return slider.getSliderPercentageValue();
+  }
+
+  /**
+   * Sets the slider position for a given percentage value of its min-max range
+   * 
+   * @param pct
+   */
+  protected void setSliderPercentageValue(float pct)
+  {
+    slider.setSliderPercentageValue(pct);
+  }
+
   protected void addSliderMouseListeners()
   {
 
@@ -290,6 +343,10 @@ public abstract class AnnotationRowFilter extends JPanel
     updateView();
   }
 
+  /**
+   * Updates the slider position, and the display, for an update in the slider's
+   * text input field
+   */
   protected void thresholdValue_actionPerformed()
   {
     try
@@ -297,12 +354,11 @@ public abstract class AnnotationRowFilter extends JPanel
       float f = Float.parseFloat(thresholdValue.getText());
       if (percentThreshold.isSelected())
       {
-        slider.setValue(slider.getMinimum() + ((int) ((f / 100f)
-                * (slider.getMaximum() - slider.getMinimum()))));
+        setSliderPercentageValue(f);
       }
       else
       {
-        slider.setValue((int) (f * 1000));
+        setSliderValue(f);
       }
       updateView();
     } catch (NumberFormatException ex)
@@ -528,4 +584,23 @@ public abstract class AnnotationRowFilter extends JPanel
       valueChanged(true);
     }
   }
+
+  /**
+   * Sets the min-max range and current value of the slider, with rescaling from
+   * true values to slider range as required
+   * 
+   * @param min
+   * @param max
+   * @param value
+   */
+  protected void setSliderModel(float min, float max, float value)
+  {
+    slider.setSliderModel(min, max, value);
+
+    /*
+     * tick mark every 10th position
+     */
+    slider.setMajorTickSpacing(
+            (slider.getMaximum() - slider.getMinimum()) / 10);
+  }
 }
index e13df4a..7cf10e7 100644 (file)
  */
 package jalview.gui;
 
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.gui.ImageExporter.ImageWriterI;
-import jalview.gui.StructureViewer.ViewerType;
-import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.BrowserLauncher;
-import jalview.util.ImageMaker;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.ws.dbsources.Pdb;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
-import java.awt.Rectangle;
-import java.awt.event.ActionEvent;
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Vector;
+import java.util.Map;
 
-import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
 import javax.swing.SwingUtilities;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.StructureViewerModel;
+import jalview.datamodel.StructureViewerModel.StructureData;
+import jalview.gui.ImageExporter.ImageWriterI;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.structure.StructureCommand;
+import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.BrowserLauncher;
+import jalview.util.ImageMaker;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.ws.dbsources.Pdb;
+
 public class AppJmol extends StructureViewerBase
 {
   // ms to wait for Jmol to load files
@@ -88,48 +89,55 @@ public class AppJmol extends StructureViewerBase
    * @param bounds
    * @param viewid
    */
-  public AppJmol(String[] files, String[] ids, SequenceI[][] seqs,
-          AlignmentPanel ap, boolean usetoColour, boolean useToAlign,
-          boolean leaveColouringToJmol, String loadStatus, Rectangle bounds,
-          String viewid)
+  public AppJmol(StructureViewerModel viewerModel, AlignmentPanel ap,
+          String sessionFile, String viewid)
   {
-    PDBEntry[] pdbentrys = new PDBEntry[files.length];
-    for (int i = 0; i < pdbentrys.length; i++)
+    Map<File, StructureData> pdbData = viewerModel.getFileData();
+    PDBEntry[] pdbentrys = new PDBEntry[pdbData.size()];
+    SequenceI[][] seqs = new SequenceI[pdbData.size()][];
+    int i = 0;
+    for (StructureData data : pdbData.values())
     {
-      // PDBEntry pdbentry = new PDBEntry(files[i], ids[i]);
-      PDBEntry pdbentry = new PDBEntry(ids[i], null, PDBEntry.Type.PDB,
-              files[i]);
+      PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
+              PDBEntry.Type.PDB, data.getFilePath());
       pdbentrys[i] = pdbentry;
+      List<SequenceI> sequencesForPdb = data.getSeqList();
+      seqs[i] = sequencesForPdb
+              .toArray(new SequenceI[sequencesForPdb.size()]);
+      i++;
     }
-    // / TODO: check if protocol is needed to be set, and if chains are
+
+    // TODO: check if protocol is needed to be set, and if chains are
     // autodiscovered.
     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
             pdbentrys, seqs, null);
 
     jmb.setLoadingFromArchive(true);
     addAlignmentPanel(ap);
-    if (useToAlign)
+    if (viewerModel.isAlignWithPanel())
     {
       useAlignmentPanelForSuperposition(ap);
     }
     initMenus();
-    if (leaveColouringToJmol || !usetoColour)
+    boolean useToColour = viewerModel.isColourWithAlignPanel();
+    boolean leaveColouringToJmol = viewerModel.isColourByViewer();
+    if (leaveColouringToJmol || !useToColour)
     {
       jmb.setColourBySequence(false);
       seqColour.setSelected(false);
       viewerColour.setSelected(true);
     }
-    else if (usetoColour)
+    else if (useToColour)
     {
       useAlignmentPanelForColourbyseq(ap);
       jmb.setColourBySequence(true);
       seqColour.setSelected(true);
       viewerColour.setSelected(false);
     }
-    this.setBounds(bounds);
+
+    this.setBounds(viewerModel.getX(), viewerModel.getY(),
+            viewerModel.getWidth(), viewerModel.getHeight());
     setViewId(viewid);
-    // jalview.gui.Desktop.addInternalFrame(this, "Loading File",
-    // bounds.width,bounds.height);
 
     this.addInternalFrameListener(new InternalFrameAdapter()
     {
@@ -140,7 +148,10 @@ public class AppJmol extends StructureViewerBase
         closeViewer(false);
       }
     });
-    initJmol(loadStatus); // pdbentry, seq, JBPCHECK!
+    StringBuilder cmd = new StringBuilder();
+    cmd.append("load FILES ").append(QUOTE)
+            .append(Platform.escapeBackslashes(sessionFile)).append(QUOTE);
+    initJmol(cmd.toString());
   }
 
   @Override
@@ -148,22 +159,12 @@ public class AppJmol extends StructureViewerBase
   {
     super.initMenus();
 
-    viewerActionMenu.setText(MessageManager.getString("label.jmol"));
-
     viewerColour
             .setText(MessageManager.getString("label.colour_with_jmol"));
     viewerColour.setToolTipText(MessageManager
             .getString("label.let_jmol_manage_structure_colours"));
   }
 
-  IProgressIndicator progressBar = null;
-
-  @Override
-  protected IProgressIndicator getIProgressIndicator()
-  {
-    return progressBar;
-  }
-  
   /**
    * display a single PDB structure in a new Jmol view
    * 
@@ -175,7 +176,7 @@ public class AppJmol extends StructureViewerBase
   public AppJmol(PDBEntry pdbentry, SequenceI[] seq, String[] chains,
           final AlignmentPanel ap)
   {
-    progressBar = ap.alignFrame;
+    setProgressIndicator(ap.alignFrame);
 
     openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
             new SequenceI[][]
@@ -186,14 +187,17 @@ public class AppJmol extends StructureViewerBase
           PDBEntry[] pdbentrys,
           SequenceI[][] seqs)
   {
-    progressBar = ap.alignFrame;
+    setProgressIndicator(ap.alignFrame);
     jmb = new AppJmolBinding(this, ap.getStructureSelectionManager(),
             pdbentrys, seqs, null);
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
 
     alignAddedStructures = alignAdded;
-    useAlignmentPanelForSuperposition(ap);
+    if (pdbentrys.length > 1)
+    {
+      useAlignmentPanelForSuperposition(ap);
+    }
 
     jmb.setColourBySequence(true);
     setSize(400, 400); // probably should be a configurable/dynamic default here
@@ -256,47 +260,12 @@ public class AppJmol extends StructureViewerBase
     {
       command = "";
     }
-    jmb.evalStateCommand(command);
-    jmb.evalStateCommand("set hoverDelay=0.1");
+    jmb.executeCommand(new StructureCommand(command), false);
+    jmb.executeCommand(new StructureCommand("set hoverDelay=0.1"), false);
     jmb.setFinishedInit(true);
   }
 
   @Override
-  void showSelectedChains()
-  {
-    Vector<String> toshow = new Vector<>();
-    for (int i = 0; i < chainMenu.getItemCount(); i++)
-    {
-      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
-      {
-        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
-        if (item.isSelected())
-        {
-          toshow.addElement(item.getText());
-        }
-      }
-    }
-    jmb.centerViewer(toshow);
-  }
-
-  @Override
-  public void closeViewer(boolean closeExternalViewer)
-  {
-    // Jmol does not use an external viewer
-    if (jmb != null)
-    {
-      jmb.closeViewer();
-    }
-    setAlignmentPanel(null);
-    _aps.clear();
-    _alignwith.clear();
-    _colourwith.clear();
-    // TODO: check for memory leaks where instance isn't finalised because jmb
-    // holds a reference to the window
-    jmb = null;
-  }
-
-  @Override
   public void run()
   {
     _started = true;
@@ -354,12 +323,12 @@ public class AppJmol extends StructureViewerBase
       cmd.append("loadingJalviewdata=true\nload APPEND ");
       cmd.append(filesString);
       cmd.append("\nloadingJalviewdata=null");
-      final String command = cmd.toString();
+      final StructureCommand command = new StructureCommand(cmd.toString());
       lastnotify = jmb.getLoadNotifiesHandled();
 
       try
       {
-        jmb.evalStateCommand(command);
+        jmb.executeCommand(command, false);
       } catch (OutOfMemoryError oomerror)
       {
         new OOMWarning("When trying to add structures to the Jmol viewer!",
@@ -385,8 +354,10 @@ public class AppJmol extends StructureViewerBase
       try
       {
         Cache.log.debug("Waiting around for jmb notify.");
-        Thread.sleep(waitFor);
         waitTotal += waitFor;
+
+        // Thread.sleep() throws an exception in JS
+        Thread.sleep(waitFor);
       } catch (Exception e)
       {
       }
@@ -403,7 +374,7 @@ public class AppJmol extends StructureViewerBase
     }
 
     // refresh the sequence colours for the new structure(s)
-    for (AlignmentPanel ap : _colourwith)
+    for (AlignmentViewPanel ap : _colourwith)
     {
       jmb.updateColours(ap);
     }
@@ -425,7 +396,7 @@ public class AppJmol extends StructureViewerBase
       @Override
       public void run()
       {
-        if (jmb.viewer.isScriptExecuting())
+        if (jmb.jmolViewer.isScriptExecuting())
         {
           SwingUtilities.invokeLater(this);
           try
@@ -438,7 +409,7 @@ public class AppJmol extends StructureViewerBase
         }
         else
         {
-          alignStructs_withAllAlignPanels();
+          alignStructsWithAllAlignPanels();
         }
       }
     });
@@ -476,12 +447,9 @@ public class AppJmol extends StructureViewerBase
           AlignmentI pdbseq = null;
           pdbid = jmb.getPdbEntry(pi).getId();
           long hdl = pdbid.hashCode() - System.currentTimeMillis();
-          if (progressBar != null)
-          {
-            progressBar.setProgressBar(MessageManager
-                    .formatMessage("status.fetching_pdb", new String[]
-                    { pdbid }), hdl);
-          }
+          setProgressMessage(MessageManager
+                  .formatMessage("status.fetching_pdb", new String[]
+                  { pdbid }), hdl);
           try
           {
             pdbseq = pdbclient.getSequenceRecords(pdbid);
@@ -494,12 +462,8 @@ public class AppJmol extends StructureViewerBase
             errormsgs.append("'").append(pdbid).append("'");
           } finally
           {
-            if (progressBar != null)
-            {
-              progressBar.setProgressBar(
-                      MessageManager.getString("label.state_completed"),
-                      hdl);
-            }
+            setProgressMessage(
+                    MessageManager.getString("label.state_completed"), hdl);
           }
           if (pdbseq != null)
           {
@@ -563,6 +527,7 @@ public class AppJmol extends StructureViewerBase
    * 
    * @param type
    */
+  @Override
   public void makePDBImage(ImageMaker.TYPE type)
   {
     int width = getWidth();
@@ -572,17 +537,17 @@ public class AppJmol extends StructureViewerBase
       @Override
       public void exportImage(Graphics g) throws Exception
       {
-        jmb.viewer.renderScreenImage(g, width, height);
+        jmb.jmolViewer.renderScreenImage(g, width, height);
       }
     };
     String view = MessageManager.getString("action.view").toLowerCase();
     ImageExporter exporter = new ImageExporter(writer,
-            jmb.getIProgressIndicator(), type, getTitle());
+            getProgressIndicator(), type, getTitle());
     exporter.doExport(null, this, width, height, view);
   }
 
   @Override
-  public void showHelp_actionPerformed(ActionEvent actionEvent)
+  public void showHelp_actionPerformed()
   {
     try
     {
@@ -590,12 +555,13 @@ public class AppJmol extends StructureViewerBase
               .openURL("http://wiki.jmol.org");//http://jmol.sourceforge.net/docs/JmolUserGuide/");
     } catch (Exception ex)
     {
+      System.err.println("Show Jmol help failed with: " + ex.getMessage());
     }
   }
 
+  @Override
   public void showConsole(boolean showConsole)
   {
-
     if (showConsole)
     {
       if (splitPane == null)
@@ -661,7 +627,7 @@ public class AppJmol extends StructureViewerBase
           }
         }
       }
-      else if (jmb == null || jmb.viewer == null || !jmb.isFinishedInit())
+      else if (jmb == null || jmb.jmolViewer == null || !jmb.isFinishedInit())
       {
         g.setColor(Color.black);
         g.fillRect(0, 0, currentSize.width, currentSize.height);
@@ -672,7 +638,7 @@ public class AppJmol extends StructureViewerBase
       }
       else
       {
-        jmb.viewer.renderScreenImage(g, currentSize.width,
+        jmb.jmolViewer.renderScreenImage(g, currentSize.width,
                 currentSize.height);
       }
     }
@@ -685,12 +651,6 @@ public class AppJmol extends StructureViewerBase
   }
 
   @Override
-  public String getStateInfo()
-  {
-    return jmb == null ? null : jmb.viewer.getStateInfo();
-  }
-
-  @Override
   public ViewerType getViewerType()
   {
     return ViewerType.JMOL;
index 75b98bc..98787cb 100644 (file)
  */
 package jalview.gui;
 
+import java.awt.Container;
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JComponent;
+
+import org.jmol.api.JmolAppConsoleInterface;
+import org.openscience.jmol.app.jmolpanel.console.AppConsole;
+
 import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
@@ -29,32 +39,16 @@ import jalview.ext.jmol.JalviewJmolBinding;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.Platform;
-
-import java.awt.Container;
-import java.io.File;
-import java.util.List;
-import java.util.Map;
-
-import org.jmol.api.JmolAppConsoleInterface;
-
 import javajs.util.BS;
 
 public class AppJmolBinding extends JalviewJmolBinding
 {
-  protected AppJmol appJmolWindow;
-
   public AppJmolBinding(AppJmol appJmol, StructureSelectionManager sSm,
           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
           DataSourceType protocol)
   {
     super(sSm, pdbentry, sequenceIs, protocol);
-    appJmolWindow = appJmol;
-  }
-
-  @Override
-  protected IProgressIndicator getIProgressIndicator()
-  {
-    return appJmolWindow.progressBar;
+    setViewer(appJmol);
   }
 
   @Override
@@ -99,36 +93,26 @@ public class AppJmolBinding extends JalviewJmolBinding
   @Override
   public void refreshGUI()
   {
+    if (getMappedStructureCount() == 0)
+    {
+      // too soon!
+      return;
+    }
     // appJmolWindow.repaint();
     javax.swing.SwingUtilities.invokeLater(new Runnable()
     {
       @Override
       public void run()
       {
-        appJmolWindow.updateTitleAndMenus();
-        // initiates a colourbySequence
-        // via seqColour_ActionPerformed.
-        appJmolWindow.revalidate();
+        JalviewStructureDisplayI theViewer = getViewer();
+        // invokes colourbySequence() via seqColour_ActionPerformed()
+        theViewer.updateTitleAndMenus();
+        ((JComponent) theViewer).revalidate();
       }
     });
   }
 
   @Override
-  public void updateColours(Object source)
-  {
-    AlignmentPanel ap = (AlignmentPanel) source;
-    // ignore events from panels not used to colour this view
-    if (!appJmolWindow.isUsedforcolourby(ap))
-    {
-      return;
-    }
-    if (!isLoadingFromArchive())
-    {
-      colourBySequence(ap);
-    }
-  }
-
-  @Override
   public void notifyScriptTermination(String strStatus, int msWalltime)
   {
     // todo - script termination doesn't happen ?
@@ -152,35 +136,28 @@ public class AppJmolBinding extends JalviewJmolBinding
   @Override
   public void selectionChanged(BS arg0)
   {
-    // TODO Auto-generated method stub
-
-  }
-
-  @Override
-  public void refreshPdbEntries()
-  {
-    // TODO Auto-generated method stub
-
   }
 
   @Override
   public void showConsole(boolean b)
   {
-    appJmolWindow.showConsole(b);
+    getViewer().showConsole(b);
   }
 
   @Override
   protected JmolAppConsoleInterface createJmolConsole(
           Container consolePanel, String buttonsToShow)
   {
-    viewer.setJmolCallbackListener(this);
-    return null;//BH can't do this yet. new AppConsole(viewer, consolePanel, buttonsToShow);
+    jmolViewer.setJmolCallbackListener(this);
+    // BH comment: can't do this yet [for JS only, or generally?]
+    return Platform.isJS() ? null
+            : new AppConsole(jmolViewer, consolePanel, buttonsToShow);
   }
 
   @Override
   protected void releaseUIResources()
   {
-    appJmolWindow = null;
+    setViewer(null);
     closeConsole();
   }
 
@@ -189,7 +166,7 @@ public class AppJmolBinding extends JalviewJmolBinding
   {
     if (svl instanceof SeqPanel)
     {
-      appJmolWindow.removeAlignmentPanel(((SeqPanel) svl).ap);
+      getViewer().removeAlignmentPanel(((SeqPanel) svl).ap);
     }
   }
 
@@ -200,27 +177,6 @@ public class AppJmolBinding extends JalviewJmolBinding
     return null;
   }
 
-  @Override
-  public JalviewStructureDisplayI getViewer()
-  {
-    return appJmolWindow;
-  }
-
-  @Override
-  public jalview.api.FeatureRenderer getFeatureRenderer(
-          AlignmentViewPanel alignment)
-  {
-    AlignmentPanel ap = (alignment == null)
-            ? appJmolWindow.getAlignmentPanel()
-            : (AlignmentPanel) alignment;
-    if (ap.av.isShowSequenceFeatures())
-    {
-      return ap.av.getAlignPanel().getSeqPanel().seqCanvas.fr;
-    }
-
-    return null;
-  }
-
   @SuppressWarnings("unused")
   public void cacheFiles(List<File> files)
   {
index c6d6e97..810f40d 100644 (file)
  */
 package jalview.gui;
 
-import jalview.api.FeatureRenderer;
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.ext.rbvi.chimera.ChimeraCommands;
-import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
-import jalview.gui.StructureViewer.ViewerType;
-import jalview.io.DataSourceType;
-import jalview.io.StructureFile;
-import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.BrowserLauncher;
-import jalview.util.ImageMaker.TYPE;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.ws.dbsources.Pdb;
-
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Random;
+import java.util.Map;
 
-import javax.swing.JCheckBoxMenuItem;
 import javax.swing.JInternalFrame;
 import javax.swing.JMenu;
 import javax.swing.JMenuItem;
 import javax.swing.event.InternalFrameAdapter;
 import javax.swing.event.InternalFrameEvent;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.StructureViewerModel;
+import jalview.datamodel.StructureViewerModel.StructureData;
+import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
+import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.ImageMaker.TYPE;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+
 /**
  * GUI elements for handling an external chimera display
  * 
@@ -67,8 +62,6 @@ public class ChimeraViewFrame extends StructureViewerBase
 {
   private JalviewChimeraBinding jmb;
 
-  private IProgressIndicator progressBar = null;
-
   /*
    * Path to Chimera session file. This is set when an open Jalview/Chimera
    * session is saved, or on restore from a Jalview project (if it holds the
@@ -76,8 +69,6 @@ public class ChimeraViewFrame extends StructureViewerBase
    */
   private String chimeraSessionFile = null;
 
-  private Random random = new Random();
-
   private int myWidth = 500;
 
   private int myHeight = 150;
@@ -90,21 +81,13 @@ public class ChimeraViewFrame extends StructureViewerBase
   {
     super.initMenus();
 
-    viewerActionMenu.setText(MessageManager.getString("label.chimera"));
-
-    viewerColour
-            .setText(MessageManager.getString("label.colour_with_chimera"));
-    viewerColour.setToolTipText(MessageManager
-            .getString("label.let_chimera_manage_structure_colours"));
-
-    helpItem.setText(MessageManager.getString("label.chimera_help"));
     savemenu.setVisible(false); // not yet implemented
     viewMenu.add(fitToWindow);
 
     JMenuItem writeFeatures = new JMenuItem(
-            MessageManager.getString("label.create_chimera_attributes"));
+            MessageManager.getString("label.create_viewer_attributes"));
     writeFeatures.setToolTipText(MessageManager
-            .getString("label.create_chimera_attributes_tip"));
+            .getString("label.create_viewer_attributes_tip"));
     writeFeatures.addActionListener(new ActionListener()
     {
       @Override
@@ -132,64 +115,42 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * Query Chimera for its residue attribute names and add them as items off the
-   * attributes menu
+   * Query the structure viewer for its residue attribute names and add them as
+   * items off the attributes menu
    * 
    * @param attributesMenu
    */
   protected void buildAttributesMenu(JMenu attributesMenu)
   {
-    List<String> atts = jmb.sendChimeraCommand("list resattr", true);
-    if (atts == null)
-    {
-      return;
-    }
+    List<String> atts = jmb.getChimeraAttributes();
     attributesMenu.removeAll();
     Collections.sort(atts);
-    for (String att : atts)
+    for (String attName : atts)
     {
-      final String attName = att.split(" ")[1];
-
-      /*
-       * ignore 'jv_*' attributes, as these are Jalview features that have
-       * been transferred to residue attributes in Chimera!
-       */
-      if (!attName.startsWith(ChimeraCommands.NAMESPACE_PREFIX))
+      JMenuItem menuItem = new JMenuItem(attName);
+      menuItem.addActionListener(new ActionListener()
       {
-        JMenuItem menuItem = new JMenuItem(attName);
-        menuItem.addActionListener(new ActionListener()
+        @Override
+        public void actionPerformed(ActionEvent e)
         {
-          @Override
-          public void actionPerformed(ActionEvent e)
+          if (getBinding().copyStructureAttributesToFeatures(attName,
+                  getAlignmentPanel()) > 0)
           {
-            getChimeraAttributes(attName);
+            getAlignmentPanel().getFeatureRenderer().featuresAdded();
           }
-        });
-        attributesMenu.add(menuItem);
-      }
+        }
+      });
+      attributesMenu.add(menuItem);
     }
   }
 
   /**
-   * Read residues in Chimera with the given attribute name, and set as features
-   * on the corresponding sequence positions (if any)
-   * 
-   * @param attName
-   */
-  protected void getChimeraAttributes(String attName)
-  {
-    jmb.copyStructureAttributesToFeatures(attName, getAlignmentPanel());
-  }
-
-  /**
-   * Send a command to Chimera to create residue attributes for Jalview features
-   * <p>
-   * The syntax is: setattr r <attName> <attValue> <atomSpec>
-   * <p>
-   * For example: setattr r jv:chain "Ferredoxin-1, Chloroplastic" #0:94.A
+   * Sends command(s) to the structure viewer to create residue attributes for
+   * visible Jalview features
    */
   protected void sendFeaturesToChimera()
   {
+    // todo pull up?
     int count = jmb.sendFeaturesToViewer(getAlignmentPanel());
     statusBar.setText(
             MessageManager.formatMessage("label.attributes_set", count));
@@ -218,9 +179,9 @@ public class ChimeraViewFrame extends StructureViewerBase
    */
   protected void createProgressBar()
   {
-    if (progressBar == null)
+    if (getProgressIndicator() == null)
     {
-      progressBar = new ProgressBar(statusPanel, statusBar);
+      setProgressIndicator(new ProgressBar(statusPanel, statusBar));
     }
   }
 
@@ -228,8 +189,7 @@ public class ChimeraViewFrame extends StructureViewerBase
           SequenceI[][] seqs)
   {
     createProgressBar();
-    jmb = new JalviewChimeraBindingModel(this,
-            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+    jmb = newBindingModel(ap, pdbentrys, seqs);
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
 
@@ -257,6 +217,13 @@ public class ChimeraViewFrame extends StructureViewerBase
 
   }
 
+  protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
+          PDBEntry[] pdbentrys, SequenceI[][] seqs)
+  {
+    return new JalviewChimeraBindingModel(this,
+            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+  }
+
   /**
    * Create a new viewer from saved session state data including Chimera session
    * file
@@ -269,22 +236,34 @@ public class ChimeraViewFrame extends StructureViewerBase
    * @param colourBySequence
    * @param newViewId
    */
-  public ChimeraViewFrame(String chimeraSessionFile,
-          AlignmentPanel alignPanel, PDBEntry[] pdbArray,
-          SequenceI[][] seqsArray, boolean colourByChimera,
-          boolean colourBySequence, String newViewId)
+  public ChimeraViewFrame(StructureViewerModel viewerData,
+          AlignmentPanel alignPanel, String sessionFile, String vid)
   {
     this();
-    setViewId(newViewId);
-    this.chimeraSessionFile = chimeraSessionFile;
+    setViewId(vid);
+    this.chimeraSessionFile = sessionFile;
+    Map<File, StructureData> pdbData = viewerData.getFileData();
+    PDBEntry[] pdbArray = new PDBEntry[pdbData.size()];
+    SequenceI[][] seqsArray = new SequenceI[pdbData.size()][];
+    int i = 0;
+    for (StructureData data : pdbData.values())
+    {
+      PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
+              PDBEntry.Type.PDB, data.getFilePath());
+      pdbArray[i] = pdbentry;
+      List<SequenceI> sequencesForPdb = data.getSeqList();
+      seqsArray[i] = sequencesForPdb
+              .toArray(new SequenceI[sequencesForPdb.size()]);
+      i++;
+    }
     openNewChimera(alignPanel, pdbArray, seqsArray);
-    if (colourByChimera)
+    if (viewerData.isColourByViewer())
     {
       jmb.setColourBySequence(false);
       seqColour.setSelected(false);
       viewerColour.setSelected(true);
     }
-    else if (colourBySequence)
+    else if (viewerData.isColourWithAlignPanel())
     {
       jmb.setColourBySequence(true);
       seqColour.setSelected(true);
@@ -337,7 +316,8 @@ public class ChimeraViewFrame extends StructureViewerBase
     if (!jmb.launchChimera())
     {
       JvOptionPane.showMessageDialog(Desktop.desktop,
-              MessageManager.getString("label.chimera_failed"),
+              MessageManager.formatMessage("label.open_viewer_failed",
+                      getViewerName()),
               MessageManager.getString("label.error_loading_file"),
               JvOptionPane.ERROR_MESSAGE);
       this.dispose();
@@ -358,71 +338,6 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * Show only the selected chain(s) in the viewer
-   */
-  @Override
-  void showSelectedChains()
-  {
-    List<String> toshow = new ArrayList<>();
-    for (int i = 0; i < chainMenu.getItemCount(); i++)
-    {
-      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
-      {
-        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
-        if (item.isSelected())
-        {
-          toshow.add(item.getText());
-        }
-      }
-    }
-    jmb.showChains(toshow);
-  }
-
-  /**
-   * Close down this instance of Jalview's Chimera viewer, giving the user the
-   * option to close the associated Chimera window (process). They may wish to
-   * keep it open until they have had an opportunity to save any work.
-   * 
-   * @param closeChimera
-   *          if true, close any linked Chimera process; if false, prompt first
-   */
-  @Override
-  public void closeViewer(boolean closeChimera)
-  {
-    if (jmb != null && jmb.isChimeraRunning())
-    {
-      if (!closeChimera)
-      {
-        String prompt = MessageManager
-                .formatMessage("label.confirm_close_chimera", new Object[]
-                { jmb.getViewerTitle(getViewerName(), false) });
-        prompt = JvSwingUtils.wrapTooltip(true, prompt);
-        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
-                MessageManager.getString("label.close_viewer"),
-                JvOptionPane.YES_NO_CANCEL_OPTION);
-        /*
-         * abort closure if user hits escape or Cancel
-         */
-        if (confirm == JvOptionPane.CANCEL_OPTION
-                || confirm == JvOptionPane.CLOSED_OPTION)
-        {
-          return;
-        }
-        closeChimera = confirm == JvOptionPane.YES_OPTION;
-      }
-      jmb.closeViewer(closeChimera);
-    }
-    setAlignmentPanel(null);
-    _aps.clear();
-    _alignwith.clear();
-    _colourwith.clear();
-    // TODO: check for memory leaks where instance isn't finalised because jmb
-    // holds a reference to the window
-    jmb = null;
-    dispose();
-  }
-
-  /**
    * Open any newly added PDB structures in Chimera, having first fetched data
    * from PDB (if not already saved).
    */
@@ -550,8 +465,8 @@ public class ChimeraViewFrame extends StructureViewerBase
 
             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
                     jmb.getChains()[pos], pe.getFile(), protocol,
-                    progressBar);
-            stashFoundChains(pdb, pe.getFile());
+                    getProgressIndicator());
+            jmb.stashFoundChains(pdb, pe.getFile());
 
           } catch (OutOfMemoryError oomerror)
           {
@@ -585,7 +500,7 @@ public class ChimeraViewFrame extends StructureViewerBase
       }
 
       // refresh the sequence colours for the new structure(s)
-      for (AlignmentPanel ap : _colourwith)
+      for (AlignmentViewPanel ap : _colourwith)
       {
         jmb.updateColours(ap);
       }
@@ -597,7 +512,7 @@ public class ChimeraViewFrame extends StructureViewerBase
           @Override
           public void run()
           {
-            alignStructs_withAllAlignPanels();
+            alignStructsWithAllAlignPanels();
           }
         }).start();
       }
@@ -607,105 +522,6 @@ public class ChimeraViewFrame extends StructureViewerBase
     worker = null;
   }
 
-  /**
-   * Fetch PDB data and save to a local file. Returns the full path to the file,
-   * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
-   * 
-   * @param processingEntry
-   * @return
-   * @throws Exception
-   */
-
-  private void stashFoundChains(StructureFile pdb, String file)
-  {
-    for (int i = 0; i < pdb.getChains().size(); i++)
-    {
-      String chid = new String(
-              pdb.getId() + ":" + pdb.getChains().elementAt(i).id);
-      jmb.getChainNames().add(chid);
-      jmb.getChainFile().put(chid, file);
-    }
-  }
-
-  private String fetchPdbFile(PDBEntry processingEntry) throws Exception
-  {
-    String filePath = null;
-    Pdb pdbclient = new Pdb();
-    AlignmentI pdbseq = null;
-    String pdbid = processingEntry.getId();
-    long handle = System.currentTimeMillis()
-            + Thread.currentThread().hashCode();
-
-    /*
-     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
-     */
-    String msg = MessageManager.formatMessage("status.fetching_pdb",
-            new Object[]
-            { pdbid });
-    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-    // long hdl = startProgressBar(MessageManager.formatMessage(
-    // "status.fetching_pdb", new Object[]
-    // { pdbid }));
-    try
-    {
-      pdbseq = pdbclient.getSequenceRecords(pdbid);
-    } catch (OutOfMemoryError oomerror)
-    {
-      new OOMWarning("Retrieving PDB id " + pdbid, oomerror);
-    } finally
-    {
-      msg = pdbid + " " + MessageManager.getString("label.state_completed");
-      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
-      // stopProgressBar(msg, hdl);
-    }
-    /*
-     * If PDB data were saved and are not invalid (empty alignment), return the
-     * file path.
-     */
-    if (pdbseq != null && pdbseq.getHeight() > 0)
-    {
-      // just use the file name from the first sequence's first PDBEntry
-      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
-              .elementAt(0).getFile()).getAbsolutePath();
-      processingEntry.setFile(filePath);
-    }
-    return filePath;
-  }
-
-  /**
-   * Convenience method to update the progress bar if there is one. Be sure to
-   * call stopProgressBar with the returned handle to remove the message.
-   * 
-   * @param msg
-   * @param handle
-   */
-  public long startProgressBar(String msg)
-  {
-    // TODO would rather have startProgress/stopProgress as the
-    // IProgressIndicator interface
-    long tm = random.nextLong();
-    if (progressBar != null)
-    {
-      progressBar.setProgressBar(msg, tm);
-    }
-    return tm;
-  }
-
-  /**
-   * End the progress bar with the specified handle, leaving a message (if not
-   * null) on the status bar
-   * 
-   * @param msg
-   * @param handle
-   */
-  public void stopProgressBar(String msg, long handle)
-  {
-    if (progressBar != null)
-    {
-      progressBar.setProgressBar(msg, handle);
-    }
-  }
-
   @Override
   public void makePDBImage(TYPE imageType)
   {
@@ -714,99 +530,11 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   @Override
-  public void showHelp_actionPerformed(ActionEvent actionEvent)
-  {
-    try
-    {
-      BrowserLauncher
-              .openURL("https://www.cgl.ucsf.edu/chimera/docs/UsersGuide");
-    } catch (IOException ex)
-    {
-    }
-  }
-
-  @Override
   public AAStructureBindingModel getBinding()
   {
     return jmb;
   }
 
-  /**
-   * Ask Chimera to save its session to the designated file path, or to a
-   * temporary file if the path is null. Returns the file path if successful,
-   * else null.
-   * 
-   * @param filepath
-   * @see getStateInfo
-   */
-  protected String saveSession(String filepath)
-  {
-    String pathUsed = filepath;
-    try
-    {
-      if (pathUsed == null)
-      {
-        File tempFile = File.createTempFile("chimera", ".py");
-        tempFile.deleteOnExit();
-        pathUsed = tempFile.getPath();
-      }
-      boolean result = jmb.saveSession(pathUsed);
-      if (result)
-      {
-        this.chimeraSessionFile = pathUsed;
-        return pathUsed;
-      }
-    } catch (IOException e)
-    {
-    }
-    return null;
-  }
-
-  /**
-   * Returns a string representing the state of the Chimera session. This is
-   * done by requesting Chimera to save its session to a temporary file, then
-   * reading the file contents. Returns an empty string on any error.
-   */
-  @Override
-  public String getStateInfo()
-  {
-    String sessionFile = saveSession(null);
-    if (sessionFile == null)
-    {
-      return "";
-    }
-    InputStream is = null;
-    try
-    {
-      File f = new File(sessionFile);
-      byte[] bytes = new byte[(int) f.length()];
-      is = new FileInputStream(sessionFile);
-      is.read(bytes);
-      return new String(bytes);
-    } catch (IOException e)
-    {
-      return "";
-    } finally
-    {
-      if (is != null)
-      {
-        try
-        {
-          is.close();
-        } catch (IOException e)
-        {
-          // ignore
-        }
-      }
-    }
-  }
-
-  @Override
-  protected void fitToWindow_actionPerformed()
-  {
-    jmb.focusView();
-  }
-
   @Override
   public ViewerType getViewerType()
   {
@@ -818,26 +546,4 @@ public class ChimeraViewFrame extends StructureViewerBase
   {
     return "Chimera";
   }
-
-  /**
-   * Sends commands to align structures according to associated alignment(s).
-   * 
-   * @return
-   */
-  @Override
-  protected String alignStructs_withAllAlignPanels()
-  {
-    String reply = super.alignStructs_withAllAlignPanels();
-    if (reply != null)
-    {
-      statusBar.setText("Superposition failed: " + reply);
-    }
-    return reply;
-  }
-
-  @Override
-  protected IProgressIndicator getIProgressIndicator()
-  {
-    return progressBar;
-  }
 }
diff --git a/src/jalview/gui/ChimeraXViewFrame.java b/src/jalview/gui/ChimeraXViewFrame.java
new file mode 100644 (file)
index 0000000..517eb4f
--- /dev/null
@@ -0,0 +1,64 @@
+package jalview.gui;
+
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.StructureViewerModel;
+import jalview.gui.StructureViewer.ViewerType;
+
+/**
+ * A class for the gui frame through which Jalview interacts with the ChimeraX
+ * structure viewer. Mostly the same as ChimeraViewFrame with a few overrides
+ * for the differences.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class ChimeraXViewFrame extends ChimeraViewFrame
+{
+
+  public ChimeraXViewFrame(PDBEntry pdb, SequenceI[] seqsForPdb,
+          String[] chains, AlignmentPanel ap)
+  {
+    super(pdb, seqsForPdb, chains, ap);
+  }
+
+  public ChimeraXViewFrame(PDBEntry[] pdbsForFile, boolean superposeAdded,
+          SequenceI[][] theSeqs, AlignmentPanel ap)
+  {
+    super(pdbsForFile, superposeAdded, theSeqs, ap);
+  }
+
+  /**
+   * Constructor given a session file to be loaded
+   * 
+   * @param viewerData
+   * @param alignPanel
+   * @param sessionFile
+   * @param vid
+   */
+  public ChimeraXViewFrame(StructureViewerModel viewerData,
+          AlignmentPanel alignPanel, String sessionFile, String vid)
+  {
+    super(viewerData, alignPanel, sessionFile, vid);
+  }
+
+  @Override
+  public ViewerType getViewerType()
+  {
+    return ViewerType.CHIMERAX;
+  }
+
+  @Override
+  protected String getViewerName()
+  {
+    return "ChimeraX";
+  }
+
+  @Override
+  protected JalviewChimeraBindingModel newBindingModel(AlignmentPanel ap,
+          PDBEntry[] pdbentrys, SequenceI[][] seqs)
+  {
+    return new JalviewChimeraXBindingModel(this,
+            ap.getStructureSelectionManager(), pdbentrys, seqs, null);
+  }
+}
index 59541bb..37d542f 100644 (file)
@@ -1784,7 +1784,7 @@ public class Desktop extends jalview.jbgui.GDesktop
                          JvOptionPane.WARNING_MESSAGE);
                }
           }
-        }).start();
+        }, "Project Loader").start();
       }
     });
     
index b49593a..e636455 100644 (file)
@@ -150,7 +150,7 @@ public class FeatureSettings extends JPanel
    */
   Object[][] originalData;
 
-  float originalTransparency;
+  private float originalTransparency;
 
   private ViewStyleI originalViewStyle;
 
@@ -182,7 +182,7 @@ public class FeatureSettings extends JPanel
   /*
    * true when Feature Settings are updating from feature renderer
    */
-  boolean handlingUpdate = false;
+  private boolean handlingUpdate = false;
 
   /*
    * a change listener to ensure the dialog is updated if
index 200911d..54eeba7 100644 (file)
@@ -50,6 +50,8 @@ import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+import java.math.BigDecimal;
+import java.math.MathContext;
 import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.List;
@@ -63,7 +65,6 @@ import javax.swing.JComboBox;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JRadioButton;
-import javax.swing.JSlider;
 import javax.swing.JTextField;
 import javax.swing.border.EmptyBorder;
 import javax.swing.border.LineBorder;
@@ -79,6 +80,8 @@ import javax.swing.event.ChangeListener;
  */
 public class FeatureTypeSettings extends JalviewDialog
 {
+  private final static MathContext FOUR_SIG_FIG = new MathContext(4);
+
   private final static String LABEL_18N = MessageManager
           .getString("label.label");
 
@@ -142,11 +145,6 @@ public class FeatureTypeSettings extends JalviewDialog
   private float max;
 
   /*
-   * scale factor for conversion between absolute min-max and slider
-   */
-  float scaleFactor;
-
-  /*
    * radio button group, to select what to colour by:
    * simple colour, by category (text), or graduated
    */
@@ -168,7 +166,7 @@ public class FeatureTypeSettings extends JalviewDialog
 
   private JComboBox<Object> threshold = new JComboBox<>();
 
-  JSlider slider = new JSlider();
+  private Slider slider;
 
   JTextField thresholdValue = new JTextField(20);
 
@@ -347,12 +345,11 @@ public class FeatureTypeSettings extends JalviewDialog
        * update min-max scaling if there is a range to work with,
        * else disable the widgets (this shouldn't happen if only 
        * valid options are offered in the combo box)
+       * offset slider to have only non-negative values if necessary (JAL-2983)
        */
-      scaleFactor = (max == min) ? 1f : 100f / (max - min);
-      float range = (max - min) * scaleFactor;
-      slider.setMinimum((int) (min * scaleFactor));
-      slider.setMaximum((int) (max * scaleFactor));
-      slider.setMajorTickSpacing((int) (range / 10f));
+      slider.setSliderModel(min, max, min);
+      slider.setMajorTickSpacing(
+              (int) ((slider.getMaximum() - slider.getMinimum()) / 10f));
 
       threshline = new GraphLine((max - min) / 2f, "Threshold",
               Color.black);
@@ -364,8 +361,8 @@ public class FeatureTypeSettings extends JalviewDialog
                 fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
                         : BELOW_THRESHOLD_OPTION);
         slider.setEnabled(true);
-        slider.setValue((int) (fc.getThreshold() * scaleFactor));
-        thresholdValue.setText(String.valueOf(fc.getThreshold()));
+        slider.setSliderValue(fc.getThreshold());
+        setThresholdValueText(fc.getThreshold());
         thresholdValue.setEnabled(true);
         thresholdIsMin.setEnabled(true);
       }
@@ -643,6 +640,7 @@ public class FeatureTypeSettings extends JalviewDialog
         thresholdValue_actionPerformed();
       }
     });
+    slider = new Slider(0f, 100f, 50f);
     slider.setPaintLabels(false);
     slider.setPaintTicks(true);
     slider.setBackground(Color.white);
@@ -659,8 +657,7 @@ public class FeatureTypeSettings extends JalviewDialog
       {
         if (!adjusting)
         {
-          thresholdValue
-                  .setText(String.valueOf(slider.getValue() / scaleFactor));
+          setThresholdValueText(slider.getSliderValue());
           thresholdValue.setBackground(Color.white); // to reset red for invalid
           sliderValueChanged();
         }
@@ -1050,8 +1047,8 @@ public class FeatureTypeSettings extends JalviewDialog
       float f = Float.parseFloat(thresholdValue.getText());
       f = Float.max(f,  this.min);
       f = Float.min(f, this.max);
-      thresholdValue.setText(String.valueOf(f));
-      slider.setValue((int) (f * scaleFactor));
+      setThresholdValueText(f);
+      slider.setSliderValue(f);
       threshline.value = f;
       thresholdValue.setBackground(Color.white); // ok
       adjusting = false;
@@ -1064,13 +1061,25 @@ public class FeatureTypeSettings extends JalviewDialog
   }
 
   /**
+   * Sets the text field for threshold value, rounded to four significant figures
+   * 
+   * @param f
+   */
+  void setThresholdValueText(float f)
+  {
+    BigDecimal formatted = new BigDecimal(f).round(FOUR_SIG_FIG)
+            .stripTrailingZeros();
+    thresholdValue.setText(formatted.toPlainString());
+  }
+
+  /**
    * Action on change of threshold slider value. This may be done interactively
    * (by moving the slider), or programmatically (to update the slider after
    * manual input of a threshold value).
    */
   protected void sliderValueChanged()
   {
-    threshline.value = getRoundedSliderValue();
+    threshline.value = slider.getSliderValue();
 
     /*
      * repaint alignment, but not Overview or structure,
@@ -1079,21 +1088,6 @@ public class FeatureTypeSettings extends JalviewDialog
     colourChanged(false);
   }
 
-  /**
-   * Converts the slider value to its absolute value by dividing by the
-   * scaleFactor. Rounding errors are squashed by forcing min/max of slider
-   * range to the actual min/max of feature score range
-   * 
-   * @return
-   */
-  private float getRoundedSliderValue()
-  {
-    int value = slider.getValue();
-    float f = value == slider.getMaximum() ? max
-            : (value == slider.getMinimum() ? min : value / scaleFactor);
-    return f;
-  }
-
   void addActionListener(ActionListener listener)
   {
     if (featureSettings != null)
index 10c0787..1a8e79f 100755 (executable)
@@ -324,6 +324,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
       if (hasHiddenRows || alignViewport.isDisplayReferenceSeq())
       {
         g.setFont(getHiddenFont(sequence, alignViewport));
+        fm = g.getFontMetrics();
       }
 
       // Selected sequence colours
index f53d8b3..4b5e9d4 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceGroup;
-import jalview.datamodel.SequenceI;
-import jalview.gui.SeqPanel.MousePos;
-import jalview.io.SequenceAnnotationReport;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.viewmodel.AlignmentViewport;
-import jalview.viewmodel.ViewportRanges;
-
 import java.awt.BorderLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -47,6 +36,17 @@ import javax.swing.SwingUtilities;
 import javax.swing.Timer;
 import javax.swing.ToolTipManager;
 
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.gui.SeqPanel.MousePos;
+import jalview.io.SequenceAnnotationReport;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.ViewportRanges;
+
 /**
  * This panel hosts alignment sequence ids and responds to mouse clicks on them,
  * as well as highlighting ids matched by a search from the Find menu.
@@ -65,8 +65,6 @@ public class IdPanel extends JPanel
 
   ScrollThread scrollThread = null;
 
-  String linkImageURL;
-
   int offy;
 
   // int width;
@@ -87,8 +85,7 @@ public class IdPanel extends JPanel
     this.av = av;
     alignPanel = parent;
     setIdCanvas(new IdCanvas(av));
-    linkImageURL = getClass().getResource("/images/link.gif").toString();
-    seqAnnotReport = new SequenceAnnotationReport(linkImageURL);
+    seqAnnotReport = new SequenceAnnotationReport(true);
     setLayout(new BorderLayout());
     add(getIdCanvas(), BorderLayout.CENTER);
     addMouseListener(this);
index 1f0c35a..aa09849 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.gui;
 
 import java.awt.Font;
index 9d63c6a..689106a 100644 (file)
@@ -20,6 +20,9 @@
  */
 package jalview.gui;
 
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
 import jalview.api.AlignmentViewPanel;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.datamodel.PDBEntry;
@@ -27,33 +30,15 @@ import jalview.datamodel.SequenceI;
 import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
-import jalview.viewmodel.seqfeatures.FeatureRendererModel;
-
-import javax.swing.SwingUtilities;
 
 public class JalviewChimeraBindingModel extends JalviewChimeraBinding
 {
-  private ChimeraViewFrame cvf;
-
   public JalviewChimeraBindingModel(ChimeraViewFrame chimeraViewFrame,
           StructureSelectionManager ssm, PDBEntry[] pdbentry,
           SequenceI[][] sequenceIs, DataSourceType protocol)
   {
     super(ssm, pdbentry, sequenceIs, protocol);
-    cvf = chimeraViewFrame;
-  }
-
-  @Override
-  public FeatureRendererModel getFeatureRenderer(AlignmentViewPanel alignment)
-  {
-    AlignmentPanel ap = (alignment == null) ? cvf.getAlignmentPanel()
-            : (AlignmentPanel) alignment;
-    if (ap.av.isShowSequenceFeatures())
-    {
-      return ap.getSeqPanel().seqCanvas.fr;
-    }
-
-    return null;
+    setViewer(chimeraViewFrame);
   }
 
   @Override
@@ -66,79 +51,15 @@ public class JalviewChimeraBindingModel extends JalviewChimeraBinding
   @Override
   public void refreshGUI()
   {
-    javax.swing.SwingUtilities.invokeLater(new Runnable()
-    {
-      @Override
-      public void run()
-      {
-        cvf.updateTitleAndMenus();
-        cvf.revalidate();
-      }
-    });
-  }
-
-  @Override
-  public void updateColours(Object source)
-  {
-    AlignmentPanel ap = (AlignmentPanel) source;
-    // ignore events from panels not used to colour this view
-    if (!cvf.isUsedforcolourby(ap))
-    {
-      return;
-    }
-    if (!isLoadingFromArchive())
-    {
-      colourBySequence(ap);
-    }
-  }
-
-  @Override
-  public void releaseReferences(Object svl)
-  {
-  }
-
-  @Override
-  protected void releaseUIResources()
-  {
-  }
-
-  @Override
-  public void refreshPdbEntries()
-  {
-  }
-
-  /**
-   * Send an asynchronous command to Chimera, in a new thread, optionally with
-   * an 'in progress' message in a progress bar somewhere
-   */
-  @Override
-  protected void sendAsynchronousCommand(final String command,
-          final String progressMsg)
-  {
-    final long handle = progressMsg == null ? 0
-            : cvf.startProgressBar(progressMsg);
     SwingUtilities.invokeLater(new Runnable()
     {
       @Override
       public void run()
       {
-        try
-        {
-          sendChimeraCommand(command, false);
-        } finally
-        {
-          if (progressMsg != null)
-          {
-            cvf.stopProgressBar(null, handle);
-          }
-        }
+        JalviewStructureDisplayI theViewer = getViewer();
+        theViewer.updateTitleAndMenus();
+        ((JComponent) theViewer).revalidate();
       }
     });
   }
-
-  @Override
-  public JalviewStructureDisplayI getViewer()
-  {
-    return cvf;
-  }
 }
diff --git a/src/jalview/gui/JalviewChimeraXBindingModel.java b/src/jalview/gui/JalviewChimeraXBindingModel.java
new file mode 100644 (file)
index 0000000..5b7a928
--- /dev/null
@@ -0,0 +1,101 @@
+package jalview.gui;
+
+import java.util.List;
+
+import ext.edu.ucsf.rbvi.strucviz2.ChimeraModel;
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
+import ext.edu.ucsf.rbvi.strucviz2.StructureManager.ModelType;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.ext.rbvi.chimera.ChimeraXCommands;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
+import jalview.structure.AtomSpec;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureSelectionManager;
+
+public class JalviewChimeraXBindingModel extends JalviewChimeraBindingModel
+{
+  public static final String CHIMERAX_SESSION_EXTENSION = ".cxs";
+
+  public JalviewChimeraXBindingModel(ChimeraViewFrame chimeraViewFrame,
+          StructureSelectionManager ssm, PDBEntry[] pdbentry,
+          SequenceI[][] sequenceIs, DataSourceType protocol)
+  {
+    super(chimeraViewFrame, ssm, pdbentry, sequenceIs, protocol);
+    setStructureCommands(new ChimeraXCommands());
+  }
+
+  @Override
+  protected List<String> getChimeraPaths()
+  {
+    return StructureManager.getChimeraPaths(true);
+  }
+
+  @Override
+  protected void addChimeraModel(PDBEntry pe,
+          List<ChimeraModel> modelsToMap)
+  {
+    /*
+     * ChimeraX hack: force chimera model name to pdbId here
+     */
+    int modelNumber = chimeraMaps.size() + 1;
+    String command = "setattr #" + modelNumber + " models name "
+            + pe.getId();
+    executeCommand(new StructureCommand(command), false);
+    modelsToMap.add(new ChimeraModel(pe.getId(), ModelType.PDB_MODEL,
+            modelNumber, 0));
+  }
+
+  /**
+   * {@inheritDoc}
+   * 
+   * @return
+   */
+  @Override
+  protected String getCommandFileExtension()
+  {
+    return ".cxc";
+  }
+
+  /**
+   * Returns the file extension to use for a saved viewer session file (.cxs)
+   * 
+   * @return
+   * @see https://www.cgl.ucsf.edu/chimerax/docs/user/commands/save.html#sesformat
+   */
+  @Override
+  public String getSessionFileExtension()
+  {
+    return CHIMERAX_SESSION_EXTENSION;
+  }
+
+  @Override
+  public String getHelpURL()
+  {
+    return "http://www.rbvi.ucsf.edu/chimerax/docs/user/index.html";
+  }
+
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.CHIMERAX;
+  }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum + 1);
+  }
+
+  /**
+   * Returns a model of the structure positions described by the ChimeraX format atomspec
+   * @param atomSpec
+   * @return
+   */
+  protected AtomSpec parseAtomSpec(String atomSpec)
+  {
+    return AtomSpec.fromChimeraXAtomspec(atomSpec);
+  }
+
+}
index 5342c90..0f4d0e7 100644 (file)
@@ -53,7 +53,6 @@ import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
-import javax.swing.JSlider;
 import javax.swing.JTextArea;
 import javax.swing.JTextField;
 import javax.swing.border.TitledBorder;
@@ -277,7 +276,7 @@ public class OptsAndParamsPage
 
     boolean choice = false;
 
-    JComboBox choicebox;
+    JComboBox<String> choicebox;
 
     JPanel controlPanel = new JPanel();
 
@@ -289,7 +288,7 @@ public class OptsAndParamsPage
 
     boolean integ = false;
 
-    Object lastVal;
+    String lastVal;
 
     ParameterI parameter;
 
@@ -299,7 +298,7 @@ public class OptsAndParamsPage
 
     JButton showDesc = new JButton();
 
-    JSlider slider = null;
+    Slider slider = null;
 
     JTextArea string = new JTextArea();
 
@@ -363,7 +362,7 @@ public class OptsAndParamsPage
       validate();
     }
 
-    private void makeExpanderParam(ParameterI parm)
+    private void makeExpanderParam(final ParameterI parm)
     {
       setPreferredSize(new Dimension(PARAM_WIDTH, PARAM_CLOSEDHEIGHT));
       setBorder(new TitledBorder(parm.getName()));
@@ -451,6 +450,9 @@ public class OptsAndParamsPage
       validate();
     }
 
+    /**
+     * Action on input in text field
+     */
     @Override
     public void actionPerformed(ActionEvent e)
     {
@@ -467,24 +469,20 @@ public class OptsAndParamsPage
 
     private void checkIfModified()
     {
-      Object cstate = updateSliderFromValueField();
-      boolean notmod = false;
-      if (cstate.getClass() == lastVal.getClass())
-      {
-        if (cstate instanceof int[])
-        {
-          notmod = (((int[]) cstate)[0] == ((int[]) lastVal)[0]);
-        }
-        else if (cstate instanceof float[])
-        {
-          notmod = (((float[]) cstate)[0] == ((float[]) lastVal)[0]);
-        }
-        else if (cstate instanceof String[])
-        {
-          notmod = (((String[]) cstate)[0].equals(((String[]) lastVal)[0]));
-        }
-      }
-      pmdialogbox.argSetModified(this, !notmod);
+      Object cstate = getCurrentValue();
+      boolean modified = !cstate.equals(lastVal);
+      pmdialogbox.argSetModified(this, modified);
+    }
+
+    /**
+     * Answers the current value of the parameter, as text
+     * 
+     * @return
+     */
+    private String getCurrentValue()
+    {
+      return choice ? (String) choicebox.getSelectedItem()
+              : valueField.getText();
     }
 
     @Override
@@ -566,16 +564,20 @@ public class OptsAndParamsPage
 
     }
 
+    /**
+     * Action on change of slider value
+     */
     @Override
     public void stateChanged(ChangeEvent e)
     {
       if (!adjusting)
       {
-        valueField.setText("" + ((integ) ? ("" + slider.getValue())
-                : ("" + slider.getValue() / 1000f)));
+        float value = slider.getSliderValue();
+        valueField.setText(
+                integ ? Integer.toString((int) value)
+                        : Float.toString(value));
         checkIfModified();
       }
-
     }
 
     public void updateControls(ParameterI parm)
@@ -592,8 +594,6 @@ public class OptsAndParamsPage
         }
         else
         {
-          slider = new JSlider();
-          slider.addChangeListener(this);
           valueField = new JTextField();
           valueField.addActionListener(this);
           valueField.addKeyListener(new KeyListener()
@@ -622,9 +622,11 @@ public class OptsAndParamsPage
             }
           });
           valueField.setPreferredSize(new Dimension(60, 25));
+          slider = makeSlider(parm.getValidValue());
+          slider.addChangeListener(this);
+
           controlPanel.add(slider, BorderLayout.WEST);
           controlPanel.add(valueField, BorderLayout.EAST);
-
         }
       }
 
@@ -634,8 +636,8 @@ public class OptsAndParamsPage
         {
           if (init)
           {
-            List vals = parm.getPossibleValues();
-            for (Object val : vals)
+            List<String> vals = parm.getPossibleValues();
+            for (String val : vals)
             {
               choicebox.addItem(val);
             }
@@ -651,96 +653,105 @@ public class OptsAndParamsPage
           valueField.setText(parm.getValue());
         }
       }
-      lastVal = updateSliderFromValueField();
+      lastVal = getCurrentValue();
       adjusting = false;
     }
 
-    public Object updateSliderFromValueField()
+    private Slider makeSlider(ValueConstrainI validValue)
+    {
+      if (validValue != null)
+      {
+        final Number minValue = validValue.getMin();
+        final Number maxValue = validValue.getMax();
+        if (minValue != null && maxValue != null)
+        {
+          return new Slider(minValue.floatValue(), maxValue.floatValue(),
+                  minValue.floatValue());
+        }
+      }
+
+      /*
+       * otherwise, a nominal slider which will not be visible
+       */
+      return new Slider(0, 100, 50);
+    }
+
+    public void updateSliderFromValueField()
     {
-      int iVal;
-      float fVal;
       if (validator != null)
       {
+        final Number minValue = validator.getMin();
+        final Number maxValue = validator.getMax();
         if (integ)
         {
-          iVal = 0;
+          int iVal = 0;
           try
           {
             valueField.setText(valueField.getText().trim());
             iVal = Integer.valueOf(valueField.getText());
-            if (validator.getMin() != null
-                    && validator.getMin().intValue() > iVal)
+            if (minValue != null
+                    && minValue.intValue() > iVal)
             {
-              iVal = validator.getMin().intValue();
+              iVal = minValue.intValue();
               // TODO: provide visual indication that hard limit was reached for
               // this parameter
             }
-            if (validator.getMax() != null
-                    && validator.getMax().intValue() < iVal)
+            if (maxValue != null && maxValue.intValue() < iVal)
             {
-              iVal = validator.getMax().intValue();
-              // TODO: provide visual indication that hard limit was reached for
-              // this parameter
+              iVal = maxValue.intValue();
             }
-          } catch (Exception e)
+          } catch (NumberFormatException e)
           {
+            System.err.println(e.toString());
           }
-          ;
-          // update value field to reflect any bound checking we performed.
-          valueField.setText("" + iVal);
-          if (validator.getMin() != null && validator.getMax() != null)
+          if (minValue != null || maxValue != null)
           {
-            slider.getModel().setRangeProperties(iVal, 1,
-                    validator.getMin().intValue(),
-                    validator.getMax().intValue() + 1, true);
+            valueField.setText(String.valueOf(iVal));
+            slider.setSliderValue(iVal);
           }
           else
           {
             slider.setVisible(false);
           }
-          return new int[] { iVal };
         }
         else
         {
-          fVal = 0f;
+          float fVal = 0f;
           try
           {
             valueField.setText(valueField.getText().trim());
             fVal = Float.valueOf(valueField.getText());
-            if (validator.getMin() != null
-                    && validator.getMin().floatValue() > fVal)
+            if (minValue != null
+                    && minValue.floatValue() > fVal)
             {
-              fVal = validator.getMin().floatValue();
+              fVal = minValue.floatValue();
               // TODO: provide visual indication that hard limit was reached for
               // this parameter
               // update value field to reflect any bound checking we performed.
               valueField.setText("" + fVal);
             }
-            if (validator.getMax() != null
-                    && validator.getMax().floatValue() < fVal)
+            if (maxValue != null
+                    && maxValue.floatValue() < fVal)
             {
-              fVal = validator.getMax().floatValue();
+              fVal = maxValue.floatValue();
               // TODO: provide visual indication that hard limit was reached for
               // this parameter
               // update value field to reflect any bound checking we performed.
               valueField.setText("" + fVal);
             }
-          } catch (Exception e)
+          } catch (NumberFormatException e)
           {
+            System.err.println(e.toString());
           }
-          ;
-          if (validator.getMin() != null && validator.getMax() != null)
+          if (minValue != null && maxValue != null)
           {
-            slider.getModel().setRangeProperties((int) (fVal * 1000f), 1,
-                    (int) (validator.getMin().floatValue() * 1000f),
-                    1 + (int) (validator.getMax().floatValue() * 1000f),
-                    true);
+            slider.setSliderModel(minValue.floatValue(),
+                    maxValue.floatValue(), fVal);
           }
           else
           {
             slider.setVisible(false);
           }
-          return new float[] { fVal };
         }
       }
       else
@@ -748,14 +759,8 @@ public class OptsAndParamsPage
         if (!choice)
         {
           slider.setVisible(false);
-          return new String[] { valueField.getText().trim() };
-        }
-        else
-        {
-          return new String[] { (String) choicebox.getSelectedItem() };
         }
       }
-
     }
   }
 
@@ -801,9 +806,9 @@ public class OptsAndParamsPage
 
   URL linkImageURL = getClass().getResource("/images/link.gif");
 
-  Map<String, OptionBox> optSet = new java.util.LinkedHashMap<String, OptionBox>();
+  Map<String, OptionBox> optSet = new java.util.LinkedHashMap<>();
 
-  Map<String, ParamBox> paramSet = new java.util.LinkedHashMap<String, ParamBox>();
+  Map<String, ParamBox> paramSet = new java.util.LinkedHashMap<>();
 
   public Map<String, OptionBox> getOptSet()
   {
@@ -904,7 +909,7 @@ public class OptsAndParamsPage
    */
   public List<ArgumentI> getCurrentSettings()
   {
-    List<ArgumentI> argSet = new ArrayList<ArgumentI>();
+    List<ArgumentI> argSet = new ArrayList<>();
     for (OptionBox opts : getOptSet().values())
     {
       OptionI opt = opts.getOptionIfEnabled();
index 8be93a1..2a7fb9f 100644 (file)
  */
 package jalview.gui;
 
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JInternalFrame;
+import javax.swing.JLabel;
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.JScrollPane;
+
 import jalview.analysis.AAFrequency;
 import jalview.analysis.AlignmentAnnotationUtils;
 import jalview.analysis.AlignmentUtils;
@@ -58,35 +87,6 @@ import jalview.util.StringUtils;
 import jalview.util.UrlLink;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Hashtable;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.Vector;
-
-import javax.swing.ButtonGroup;
-import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JInternalFrame;
-import javax.swing.JLabel;
-import javax.swing.JMenu;
-import javax.swing.JMenuItem;
-import javax.swing.JPanel;
-import javax.swing.JPopupMenu;
-import javax.swing.JRadioButtonMenuItem;
-import javax.swing.JScrollPane;
-
 /**
  * The popup menu that is displayed on right-click on a sequence id, or in the
  * sequence alignment.
@@ -761,14 +761,16 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
   }
 
   /**
-   * Add a link to show feature details for each sequence feature
+   * Add a menu item to show feature details for each sequence feature. Any
+   * linked 'virtual' features (CDS/protein) are also optionally found and
+   * included.
    * 
    * @param features
-   * @param column
    * @param seq
+   * @param column
    */
   protected void addFeatureDetails(List<SequenceFeature> features,
-          SequenceI seq, int column)
+          final SequenceI seq, final int column)
   {
     /*
      * add features in CDS/protein complement at the corresponding
@@ -803,39 +805,49 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     String name = seq.getName();
     for (final SequenceFeature sf : features)
     {
-      addFeatureDetailsMenuItem(details, name, sf);
+      addFeatureDetailsMenuItem(details, name, sf, null);
     }
 
     if (mf != null)
     {
-      name = mf.fromSeq == seq ? mf.mapping.getTo().getName()
-              : mf.fromSeq.getName();
       for (final SequenceFeature sf : mf.features)
       {
-        addFeatureDetailsMenuItem(details, name, sf);
+        addFeatureDetailsMenuItem(details, name, sf, mf);
       }
     }
   }
 
   /**
-   * A helper method to add one menu item whose action is to show details for one
-   * feature. The menu text includes feature description, but this may be
+   * A helper method to add one menu item whose action is to show details for
+   * one feature. The menu text includes feature description, but this may be
    * truncated.
    * 
    * @param details
    * @param seqName
    * @param sf
+   * @param mf
    */
   void addFeatureDetailsMenuItem(JMenu details, final String seqName,
-          final SequenceFeature sf)
+          final SequenceFeature sf, MappedFeatures mf)
   {
     int start = sf.getBegin();
     int end = sf.getEnd();
+    if (mf != null)
+    {
+      /*
+       * show local rather than linked feature coordinates
+       */
+      int[] beginRange = mf.getMappedPositions(start, start);
+      start = beginRange[0];
+      int[] endRange = mf.getMappedPositions(end, end);
+      end = endRange[endRange.length - 1];
+    }
     StringBuilder desc = new StringBuilder();
     desc.append(sf.getType()).append(" ").append(String.valueOf(start));
     if (start != end)
     {
-      desc.append("-").append(String.valueOf(end));
+      desc.append(sf.isContactFeature() ? ":" : "-");
+      desc.append(String.valueOf(end));
     }
     String description = sf.getDescription();
     if (description != null)
@@ -866,20 +878,21 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        showFeatureDetails(seqName, sf);
+        showFeatureDetails(sf, seqName, mf);
       }
     });
     details.add(item);
   }
 
   /**
-   * Opens a panel showing a text report of feature dteails
-   * 
-   * @param seqName
+   * Opens a panel showing a text report of feature details
    * 
    * @param sf
+   * @param seqName
+   * @param mf
    */
-  protected void showFeatureDetails(String seqName, SequenceFeature sf)
+  protected void showFeatureDetails(SequenceFeature sf, String seqName,
+          MappedFeatures mf)
   {
     JInternalFrame details;
     if (Platform.isJS())
@@ -891,7 +904,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       // TODO JAL-3026 set style of table correctly for feature details
       JLabel reprt = new JLabel(MessageManager
               .formatMessage("label.html_content", new Object[]
-              { sf.getDetailsReport(seqName) }));
+              { sf.getDetailsReport(seqName, mf) }));
       reprt.setBackground(Color.WHITE);
       reprt.setOpaque(true);
       panel.add(reprt, BorderLayout.CENTER);
@@ -906,10 +919,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
      */
     {
       CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
-      // it appears Java's CSS does not support border-collaps :-(
+      // it appears Java's CSS does not support border-collapse :-(
       cap.addStylesheetRule("table { border-collapse: collapse;}");
       cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
-      cap.setText(sf.getDetailsReport(seqName));
+      cap.setText(sf.getDetailsReport(seqName, mf));
       details = cap;
     }
     Desktop.addInternalFrame(details,
@@ -1795,7 +1808,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               "label.create_sequence_details_report_annotation_for",
               new Object[]
               { seq.getDisplayId(true) }) + "</h2></p><p>");
-      new SequenceAnnotationReport(null).createSequenceAnnotationReport(
+      new SequenceAnnotationReport(false).createSequenceAnnotationReport(
               contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
       contents.append("</p>");
     }
index 04b83a3..c61c70e 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
-import jalview.bin.Cache;
-import jalview.gui.Help.HelpId;
-import jalview.gui.StructureViewer.ViewerType;
-import jalview.io.BackupFiles;
-import jalview.io.BackupFilesPresetEntry;
-import jalview.io.FileFormatI;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
-import jalview.jbgui.GPreferences;
-import jalview.jbgui.GSequenceLink;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.ColourSchemes;
-import jalview.schemes.ResidueColourScheme;
-import jalview.urls.UrlLinkTableModel;
-import jalview.urls.api.UrlProviderFactoryI;
-import jalview.urls.api.UrlProviderI;
-import jalview.urls.desktop.DesktopUrlProviderFactory;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.util.UrlConstants;
-import jalview.ws.sifts.SiftsSettings;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
@@ -74,6 +51,29 @@ import javax.swing.table.TableModel;
 import javax.swing.table.TableRowSorter;
 
 import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
+import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+import jalview.bin.Cache;
+import jalview.ext.pymol.PymolManager;
+import jalview.gui.Help.HelpId;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.BackupFiles;
+import jalview.io.BackupFilesPresetEntry;
+import jalview.io.FileFormatI;
+import jalview.io.JalviewFileChooser;
+import jalview.io.JalviewFileView;
+import jalview.jbgui.GPreferences;
+import jalview.jbgui.GSequenceLink;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
+import jalview.schemes.ResidueColourScheme;
+import jalview.urls.UrlLinkTableModel;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.api.UrlProviderI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.util.UrlConstants;
+import jalview.ws.sifts.SiftsSettings;
 
 /**
  * DOCUMENT ME!
@@ -105,6 +105,10 @@ public class Preferences extends GPreferences
 
   public static final String CHIMERA_PATH = "CHIMERA_PATH";
 
+  public static final String CHIMERAX_PATH = "CHIMERAX_PATH";
+
+  public static final String PYMOL_PATH = "PYMOL_PATH";
+
   public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
 
   public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE";
@@ -337,7 +341,7 @@ public class Preferences extends GPreferences
             .setSelected(Cache.getDefault(SHOW_OV_HIDDEN_AT_START, false));
 
     /*
-     * Set Structure tab defaults.
+     * Set Structure tab defaults
      */
     final boolean structSelected = Cache.getDefault(STRUCT_FROM_PDB, false);
     structFromPdb.setSelected(structSelected);
@@ -347,15 +351,52 @@ public class Preferences extends GPreferences
     addSecondaryStructure.setEnabled(structSelected);
     addTempFactor.setSelected(Cache.getDefault(ADD_TEMPFACT_ANN, false));
     addTempFactor.setEnabled(structSelected);
-    structViewer.setSelectedItem(
-            Cache.getDefault(STRUCTURE_DISPLAY, ViewerType.JMOL.name()));
-    chimeraPath.setText(Cache.getDefault(CHIMERA_PATH, ""));
-    chimeraPath.addActionListener(new ActionListener()
+
+    /*
+     * set choice of structure viewer, and path if saved as a preference;
+     * default to Jmol (first choice) if an unexpected value is found
+     */
+    String viewerType = Cache.getDefault(STRUCTURE_DISPLAY, ViewerType.JMOL.name());
+    structViewer.setSelectedItem(viewerType);
+    String viewerPath = "";
+    ViewerType type = null;
+    try
+    {
+      type = ViewerType.valueOf(viewerType);
+      switch (type)
+      {
+      case JMOL:
+        break;
+      case CHIMERA:
+        viewerPath = Cache.getDefault(CHIMERA_PATH, "");
+        break;
+      case CHIMERAX:
+        viewerPath = Cache.getDefault(CHIMERAX_PATH, "");
+        break;
+      case PYMOL:
+        viewerPath = Cache.getDefault(PYMOL_PATH, "");
+        break;
+      }
+    } catch (IllegalArgumentException e)
+    {
+      Cache.log.error("Unknown structure viewer type: " + viewerType
+              + ", defaulting to Jmol");
+      type = ViewerType.JMOL;
+    }
+    structureViewerPath.setText(viewerPath);
+
+    structureViewerPath.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        validateChimeraPath();
+        if (validateViewerPath())
+        {
+          Cache.setProperty(structViewer.getSelectedItem()
+                  .equals(ViewerType.CHIMERAX.name())
+                  ? CHIMERAX_PATH
+                  : CHIMERA_PATH, structureViewerPath.getText());
+        }
       }
     });
 
@@ -707,9 +748,22 @@ public class Preferences extends GPreferences
             Boolean.toString(useRnaView.isSelected()));
     Cache.applicationProperties.setProperty(STRUCT_FROM_PDB,
             Boolean.toString(structFromPdb.isSelected()));
+    String viewer = structViewer.getSelectedItem().toString();
+    String viewerPath = structureViewerPath.getText();
     Cache.applicationProperties.setProperty(STRUCTURE_DISPLAY,
-            structViewer.getSelectedItem().toString());
-    Cache.setOrRemove(CHIMERA_PATH, chimeraPath.getText());
+            viewer);
+    if (viewer.equals(ViewerType.CHIMERA.name()))
+    {
+      Cache.setOrRemove(CHIMERA_PATH, viewerPath);
+    }
+    else if (viewer.equals(ViewerType.CHIMERAX.name()))
+    {
+      Cache.setOrRemove(CHIMERAX_PATH, viewerPath);
+    }
+    else if (viewer.equals(ViewerType.PYMOL.name()))
+    {
+      Cache.setOrRemove(PYMOL_PATH, viewerPath);
+    }
     Cache.applicationProperties.setProperty("MAP_WITH_SIFTS",
             Boolean.toString(siftsMapping.isSelected()));
     SiftsSettings.setMapWithSifts(siftsMapping.isSelected());
@@ -889,7 +943,7 @@ public class Preferences extends GPreferences
   @Override
   protected boolean validateStructure()
   {
-    return validateChimeraPath();
+    return validateViewerPath();
 
   }
 
@@ -1213,19 +1267,20 @@ public class Preferences extends GPreferences
   }
 
   /**
-   * Returns true if chimera path is to a valid executable, else show an error
-   * dialog.
+   * Returns true if structure viewer path is to a valid executable, else shows
+   * an error dialog. Does nothing if the path is empty, as is the case for Jmol
+   * (built in to Jalview) or when Jalview is left to try default paths.
    */
-  private boolean validateChimeraPath()
+  private boolean validateViewerPath()
   {
-    if (chimeraPath.getText().trim().length() > 0)
+    if (structureViewerPath.getText().trim().length() > 0)
     {
-      File f = new File(chimeraPath.getText());
+      File f = new File(structureViewerPath.getText());
       if (!f.canExecute())
       {
         JvOptionPane.showInternalMessageDialog(Desktop.desktop,
-                MessageManager.getString("label.invalid_chimera_path"),
-                MessageManager.getString("label.invalid_name"),
+                MessageManager.getString("label.invalid_viewer_path"),
+                MessageManager.getString("label.invalid_viewer_path"),
                 JvOptionPane.ERROR_MESSAGE);
         return false;
       }
@@ -1234,23 +1289,57 @@ public class Preferences extends GPreferences
   }
 
   /**
-   * If Chimera is selected, check it can be found on default or user-specified
-   * path, if not show a warning/help dialog.
+   * If Chimera or ChimeraX or Pymol is selected, check it can be found on
+   * default or user-specified path, if not show a warning/help dialog
    */
   @Override
   protected void structureViewer_actionPerformed(String selectedItem)
   {
-    if (!selectedItem.equals(ViewerType.CHIMERA.name()))
+    if (selectedItem.equals(ViewerType.JMOL.name()))
     {
+      structureViewerPath.setEnabled(false);
+      structureViewerPathLabel.setEnabled(false);
       return;
     }
     boolean found = false;
+    structureViewerPath.setEnabled(true);
+    structureViewerPathLabel.setEnabled(true);
+    structureViewerPathLabel.setText(MessageManager
+            .formatMessage("label.viewer_path", selectedItem));
 
     /*
-     * Try user-specified and standard paths for Chimera executable.
+     * Try user-specified and standard paths for structure viewer executable
      */
-    List<String> paths = StructureManager.getChimeraPaths();
-    paths.add(0, chimeraPath.getText());
+    String viewerPath = "";
+    List<String> paths = null;
+    try
+    {
+      ViewerType viewerType = ViewerType.valueOf(selectedItem);
+      switch (viewerType)
+      {
+      case JMOL:
+        // dealt with above
+        break;
+      case CHIMERA:
+        viewerPath = Cache.getDefault(CHIMERA_PATH, "");
+        paths = StructureManager.getChimeraPaths(false);
+        break;
+      case CHIMERAX:
+        viewerPath = Cache.getDefault(CHIMERAX_PATH, "");
+        paths = StructureManager.getChimeraPaths(true);
+        break;
+      case PYMOL:
+        viewerPath = Cache.getDefault(PYMOL_PATH, "");
+        paths = PymolManager.getPymolPaths();
+        break;
+      }
+    } catch (IllegalArgumentException e)
+    {
+      // only valid entries should be in the drop-down
+    }
+    structureViewerPath.setText(viewerPath);
+
+    paths.add(0, structureViewerPath.getText());
     for (String path : paths)
     {
       if (new File(path.trim()).canExecute())
@@ -1259,12 +1348,13 @@ public class Preferences extends GPreferences
         break;
       }
     }
+
     if (!found)
     {
       String[] options = { "OK", "Help" };
       int showHelp = JvOptionPane.showInternalOptionDialog(Desktop.desktop,
               JvSwingUtils.wrapTooltip(true,
-                      MessageManager.getString("label.chimera_missing")),
+                      MessageManager.getString("label.viewer_missing")),
               "", JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
               null, options, options[0]);
       if (showHelp == JvOptionPane.NO_OPTION)
diff --git a/src/jalview/gui/PymolBindingModel.java b/src/jalview/gui/PymolBindingModel.java
new file mode 100644 (file)
index 0000000..538b101
--- /dev/null
@@ -0,0 +1,232 @@
+package jalview.gui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.ext.pymol.PymolCommands;
+import jalview.ext.pymol.PymolManager;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.structure.AtomSpec;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureSelectionManager;
+import jalview.structures.models.AAStructureBindingModel;
+
+public class PymolBindingModel extends AAStructureBindingModel
+{
+  /*
+   * format for labels shown on structures when mousing over sequence;
+   * see https://pymolwiki.org/index.php/Label#examples
+   * left not final so customisable e.g. with a Groovy script
+   */
+  private static String LABEL_FORMAT = "\"%s %s\" % (resn,resi)";
+
+  private PymolManager pymolManager;
+
+  /*
+   * full paths to structure files opened in PyMOL
+   */
+  List<String> structureFiles = new ArrayList<>();
+
+  /*
+   * lookup from file path to PyMOL object name
+   */
+  Map<String, String> pymolObjects = new HashMap<>();
+
+  private String lastLabelSpec;
+
+  /**
+   * Constructor
+   * 
+   * @param viewer
+   * @param ssm
+   * @param pdbentry
+   * @param sequenceIs
+   */
+  public PymolBindingModel(StructureViewerBase viewer,
+          StructureSelectionManager ssm, PDBEntry[] pdbentry,
+          SequenceI[][] sequenceIs)
+  {
+    super(ssm, pdbentry, sequenceIs, null);
+    pymolManager = new PymolManager();
+    setStructureCommands(new PymolCommands());
+    setViewer(viewer);
+  }
+
+  @Override
+  public String[] getStructureFiles()
+  {
+    return structureFiles.toArray(new String[structureFiles.size()]);
+  }
+
+  @Override
+  public void highlightAtoms(List<AtomSpec> atoms)
+  {
+    /*
+     * https://pymolwiki.org/index.php/Label#examples
+     */
+    StringBuilder sb = new StringBuilder();
+    for (AtomSpec atom : atoms)
+    {
+      // todo promote to StructureCommandsI.showLabel()
+      // todo handle CA|P correctly
+      String modelId = getModelIdForFile(atom.getPdbFile());
+      sb.append(String.format(" %s//%s/%d/CA", modelId,
+              atom.getChain(),
+              atom.getPdbResNum()));
+    }
+    String labelSpec = sb.toString();
+    if (labelSpec.equals(lastLabelSpec))
+    {
+      return;
+    }
+    StructureCommandI command = new StructureCommand("label", labelSpec, LABEL_FORMAT);
+    executeCommand(command, false);
+
+    /*
+     * and remove the label(s) previously shown
+     */
+    if (lastLabelSpec != null)
+    {
+      command = new StructureCommand("label", lastLabelSpec, "");
+      executeCommand(command, false);
+    }
+
+    lastLabelSpec = labelSpec;
+  }
+
+  @Override
+  public SequenceRenderer getSequenceRenderer(AlignmentViewPanel avp)
+  {
+    return new SequenceRenderer(avp.getAlignViewport());
+  }
+
+  @Override
+  protected List<String> executeCommand(StructureCommandI command,
+          boolean getReply)
+  {
+    // System.out.println(command.toString()); // debug
+    return pymolManager.sendCommand(command, getReply);
+  }
+
+  @Override
+  protected String getModelIdForFile(String file)
+  {
+    return pymolObjects.containsKey(file) ? pymolObjects.get(file) : "";
+  }
+
+  @Override
+  protected ViewerType getViewerType()
+  {
+    return ViewerType.PYMOL;
+  }
+
+  @Override
+  public boolean isViewerRunning()
+  {
+    return pymolManager.isPymolLaunched();
+  }
+
+  @Override
+  public void closeViewer(boolean closePymol)
+  {
+    super.closeViewer(closePymol);
+    pymolManager = null;
+  }
+
+  public boolean launchPymol()
+  {
+    if (pymolManager.isPymolLaunched())
+    {
+      return true;
+    }
+
+    Process pymol = pymolManager.launchPymol();
+    if (pymol != null)
+    {
+      // start listening for PyMOL selections - how??
+      startExternalViewerMonitor(pymol);
+    }
+    else
+    {
+      Cache.log.error("Failed to launch PyMOL!");
+    }
+    return pymol != null;
+  }
+
+  public void openFile(PDBEntry pe)
+  {
+    // todo : check not already open, remap / rename, etc
+    String file = pe.getFile();
+    StructureCommandI cmd = getCommandGenerator().loadFile(file);
+
+    /*
+     * a second parameter sets the pdbid as the loaded PyMOL object name
+     */
+    String pdbId = pe.getId();
+    cmd.addParameter(pdbId);
+
+    executeCommand(cmd, false);
+
+    pymolObjects.put(file, pdbId);
+    if (!structureFiles.contains(file))
+    {
+      structureFiles.add(file);
+    }
+    if (getSsm() != null)
+    {
+      getSsm().addStructureViewerListener(this);
+    }
+
+  }
+
+  @Override
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return file;
+  }
+
+  /**
+   * Returns the file extension to use for a saved viewer session file (.pse)
+   * 
+   * @return
+   * @see https://pymolwiki.org/index.php/Save
+   */
+  @Override
+  public String getSessionFileExtension()
+  {
+    return ".pse";
+  }
+
+  @Override
+  public String getHelpURL()
+  {
+    return "https://pymolwiki.org/";
+  }
+
+  /**
+   * Constructs and sends commands to set atom properties for visible Jalview
+   * features on residues mapped to structure
+   * 
+   * @param avp
+   * @return
+   */
+  public int sendFeaturesToViewer(AlignmentViewPanel avp)
+  {
+    // todo pull up this and JalviewChimeraBinding variant
+    Map<String, Map<Object, AtomSpecModel>> featureValues = buildFeaturesMap(
+            avp);
+    List<StructureCommandI> commands = getCommandGenerator()
+            .setAttributes(featureValues);
+    executeCommands(commands, false, null);
+    return commands.size();
+  }
+
+}
diff --git a/src/jalview/gui/PymolViewer.java b/src/jalview/gui/PymolViewer.java
new file mode 100644 (file)
index 0000000..c5a4c9a
--- /dev/null
@@ -0,0 +1,382 @@
+package jalview.gui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JInternalFrame;
+import javax.swing.JMenuItem;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.StructureViewerModel;
+import jalview.datamodel.StructureViewerModel.StructureData;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
+import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.MessageManager;
+
+public class PymolViewer extends StructureViewerBase
+{
+  private static final int myWidth = 500;
+
+  private static final int myHeight = 150;
+
+  private PymolBindingModel binding;
+
+  private String pymolSessionFile;
+
+  public PymolViewer()
+  {
+    super();
+
+    /*
+     * closeViewer will decide whether or not to close this frame
+     * depending on whether user chooses to Cancel or not
+     */
+    setDefaultCloseOperation(JInternalFrame.DO_NOTHING_ON_CLOSE);
+  }
+
+  public PymolViewer(PDBEntry pdb, SequenceI[] seqs, Object object,
+          AlignmentPanel ap)
+  {
+    this();
+    openNewPymol(ap, new PDBEntry[] { pdb },
+            new SequenceI[][]
+            { seqs });
+  }
+
+  public PymolViewer(PDBEntry[] pe, boolean alignAdded, SequenceI[][] seqs,
+          AlignmentPanel ap)
+  {
+    this();
+    setAlignAddedStructures(alignAdded);
+    openNewPymol(ap, pe, seqs);
+  }
+
+  /**
+   * Constructor given a session file to be restored
+   * 
+   * @param sessionFile
+   * @param alignPanel
+   * @param pdbArray
+   * @param seqsArray
+   * @param colourByPymol
+   * @param colourBySequence
+   * @param newViewId
+   */
+  public PymolViewer(StructureViewerModel viewerModel,
+          AlignmentPanel alignPanel, String sessionFile, String vid)
+  {
+    // TODO convert to base/factory class method
+    this();
+    setViewId(vid);
+    this.pymolSessionFile = sessionFile;
+    Map<File, StructureData> pdbData = viewerModel.getFileData();
+    PDBEntry[] pdbArray = new PDBEntry[pdbData.size()];
+    SequenceI[][] seqsArray = new SequenceI[pdbData.size()][];
+    int i = 0;
+    for (StructureData data : pdbData.values())
+    {
+      PDBEntry pdbentry = new PDBEntry(data.getPdbId(), null,
+              PDBEntry.Type.PDB, data.getFilePath());
+      pdbArray[i] = pdbentry;
+      List<SequenceI> sequencesForPdb = data.getSeqList();
+      seqsArray[i] = sequencesForPdb
+              .toArray(new SequenceI[sequencesForPdb.size()]);
+      i++;
+    }
+
+    openNewPymol(alignPanel, pdbArray, seqsArray);
+    if (viewerModel.isColourByViewer())
+    {
+      binding.setColourBySequence(false);
+      seqColour.setSelected(false);
+      viewerColour.setSelected(true);
+    }
+    else if (viewerModel.isColourWithAlignPanel())
+    {
+      binding.setColourBySequence(true);
+      seqColour.setSelected(true);
+      viewerColour.setSelected(false);
+    }
+  }
+
+  private void openNewPymol(AlignmentPanel ap, PDBEntry[] pe,
+          SequenceI[][] seqs)
+  {
+    createProgressBar();
+    binding = new PymolBindingModel(this, ap.getStructureSelectionManager(),
+            pe, seqs);
+    addAlignmentPanel(ap);
+    useAlignmentPanelForColourbyseq(ap);
+
+    if (pe.length > 1)
+    {
+      useAlignmentPanelForSuperposition(ap);
+    }
+    binding.setColourBySequence(true);
+    setSize(myWidth, myHeight);
+    initMenus();
+    viewerActionMenu.setText("PyMOL");
+    updateTitleAndMenus();
+
+    addingStructures = false;
+    worker = new Thread(this);
+    worker.start();
+
+    this.addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosing(
+              InternalFrameEvent internalFrameEvent)
+      {
+        closeViewer(false);
+      }
+    });
+
+  }
+
+  /**
+   * Create a helper to manage progress bar display
+   */
+  protected void createProgressBar()
+  {
+    if (getProgressIndicator() == null)
+    {
+      setProgressIndicator(new ProgressBar(statusPanel, statusBar));
+    }
+  }
+
+  @Override
+  public void run()
+  {
+    // todo pull up much of this
+  
+    StringBuilder errormsgs = new StringBuilder(128);
+    List<PDBEntry> filePDB = new ArrayList<>();
+    List<Integer> filePDBpos = new ArrayList<>();
+    String[] curfiles = binding.getStructureFiles(); // files currently in viewer
+    for (int pi = 0; pi < binding.getPdbCount(); pi++)
+    {
+      String file = null;
+      PDBEntry thePdbEntry = binding.getPdbEntry(pi);
+      if (thePdbEntry.getFile() == null)
+      {
+        /*
+         * Retrieve PDB data, save to file, attach to PDBEntry
+         */
+        file = fetchPdbFile(thePdbEntry);
+        if (file == null)
+        {
+          errormsgs.append("'" + thePdbEntry.getId() + "' ");
+        }
+      }
+      else
+      {
+        /*
+         * got file already
+         */
+        file = new File(thePdbEntry.getFile()).getAbsoluteFile()
+                .getPath();
+        // todo - skip if already loaded in PyMOL
+      }
+      if (file != null)
+      {
+        filePDB.add(thePdbEntry);
+        filePDBpos.add(Integer.valueOf(pi));
+      }
+    }
+        
+    if (!filePDB.isEmpty())
+    {
+      /*
+       * at least one structure to add to viewer
+       */
+      binding.setFinishedInit(false);
+      if (!addingStructures)
+      {
+        try
+        {
+          initPymol();
+        } catch (Exception ex)
+        {
+          Cache.log.error("Couldn't open PyMOL viewer!", ex);
+        }
+      }
+      int num = -1;
+      for (PDBEntry pe : filePDB)
+      {
+        num++;
+        if (pe.getFile() != null)
+        {
+          try
+          {
+            int pos = filePDBpos.get(num).intValue();
+            long startTime = startProgressBar(getViewerName() + " "
+                    + MessageManager.getString("status.opening_file_for")
+                    + " " + pe.getId());
+            binding.openFile(pe);
+            binding.addSequence(pos, binding.getSequence()[pos]);
+            File fl = new File(pe.getFile());
+            DataSourceType protocol = DataSourceType.URL;
+            try
+            {
+              if (fl.exists())
+              {
+                protocol = DataSourceType.FILE;
+              }
+            } catch (Throwable e)
+            {
+            } finally
+            {
+              stopProgressBar("", startTime);
+            }
+
+            StructureFile pdb = binding.getSsm().setMapping(
+                    binding.getSequence()[pos], binding.getChains()[pos],
+                    pe.getFile(), protocol,
+                    getProgressIndicator());
+            binding.stashFoundChains(pdb, pe.getFile());
+          } catch (Exception ex)
+          {
+            Cache.log.error(
+                    "Couldn't open " + pe.getFile() + " in Chimera viewer!",
+                    ex);
+          } finally
+          {
+            // Cache.log.debug("File locations are " + files);
+          }
+        }
+      }
+
+      binding.refreshGUI();
+      binding.setFinishedInit(true);
+      binding.setLoadingFromArchive(false);
+
+      /*
+       * ensure that any newly discovered features (e.g. RESNUM)
+       * are added to any open feature settings dialog
+       */
+      FeatureRenderer fr = getBinding().getFeatureRenderer(null);
+      if (fr != null)
+      {
+        fr.featuresAdded();
+      }
+
+      // refresh the sequence colours for the new structure(s)
+      for (AlignmentViewPanel ap : _colourwith)
+      {
+        binding.updateColours(ap);
+      }
+      // do superposition if asked to
+      if (alignAddedStructures)
+      {
+        new Thread(new Runnable()
+        {
+          @Override
+          public void run()
+          {
+            alignStructsWithAllAlignPanels();
+          }
+        }).start();
+      }
+      addingStructures = false;
+    }
+    _started = false;
+    worker = null;
+
+  }
+
+  /**
+   * Launch PyMOL. If we have a session file name, send PyMOL the command to
+   * open its saved session file.
+   */
+  void initPymol()
+  {
+    Desktop.addInternalFrame(this,
+            binding.getViewerTitle(getViewerName(), true),
+            getBounds().width, getBounds().height);
+
+    if (!binding.launchPymol())
+    {
+      JvOptionPane.showMessageDialog(Desktop.desktop,
+              MessageManager.formatMessage("label.open_viewer_failed",
+                      getViewerName()),
+              MessageManager.getString("label.error_loading_file"),
+              JvOptionPane.ERROR_MESSAGE);
+      this.dispose();
+      return;
+    }
+
+    if (this.pymolSessionFile != null)
+    {
+      boolean opened = binding.openSession(pymolSessionFile);
+      if (!opened)
+      {
+        Cache.log.error(
+                "An error occurred opening PyMOL session file "
+                + pymolSessionFile);
+      }
+    }
+    // binding.startPymolListener();
+  }
+
+  @Override
+  public AAStructureBindingModel getBinding()
+  {
+    return binding;
+  }
+
+  @Override
+  public ViewerType getViewerType()
+  {
+    return ViewerType.PYMOL;
+  }
+
+  @Override
+  protected String getViewerName()
+  {
+    return "PyMOL";
+  }
+
+  @Override
+  protected void initMenus()
+  {
+    super.initMenus();
+
+    savemenu.setVisible(false); // not yet implemented
+    viewMenu.add(fitToWindow);
+
+    JMenuItem writeFeatures = new JMenuItem(
+            MessageManager.getString("label.create_viewer_attributes"));
+    writeFeatures.setToolTipText(MessageManager
+            .getString("label.create_viewer_attributes_tip"));
+    writeFeatures.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        sendFeaturesToPymol();
+      }
+    });
+    viewerActionMenu.add(writeFeatures);
+  }
+
+  protected void sendFeaturesToPymol()
+  {
+    int count = binding.sendFeaturesToViewer(getAlignmentPanel());
+    statusBar.setText(
+            MessageManager.formatMessage("label.attributes_set", count));
+  }
+
+}
index 2ad5bea..8a49092 100644 (file)
@@ -212,8 +212,6 @@ public class SeqPanel extends JPanel
 
   StringBuffer keyboardNo2;
 
-  java.net.URL linkImageURL;
-
   private final SequenceAnnotationReport seqARep;
 
   /*
@@ -244,8 +242,7 @@ public class SeqPanel extends JPanel
    */
   public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
-    linkImageURL = getClass().getResource("/images/link.gif");
-    seqARep = new SequenceAnnotationReport(linkImageURL.toString());
+    seqARep = new SequenceAnnotationReport(true);
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
@@ -1067,9 +1064,9 @@ public class SeqPanel extends JPanel
     {
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
-      unshownFeatures = seqARep.appendFeaturesLengthLimit(tooltipText, pos,
-              features,
-              this.ap.getSeqPanel().seqCanvas.fr, MAX_TOOLTIP_LENGTH);
+      unshownFeatures = seqARep.appendFeatures(tooltipText, pos,
+              features, this.ap.getSeqPanel().seqCanvas.fr,
+              MAX_TOOLTIP_LENGTH);
 
       /*
        * add features in CDS/protein complement at the corresponding
@@ -1087,9 +1084,8 @@ public class SeqPanel extends JPanel
                   pos);
           if (mf != null)
           {
-            unshownFeatures = seqARep.appendFeaturesLengthLimit(
-                    tooltipText, pos, mf, fr2,
-                    MAX_TOOLTIP_LENGTH);
+            unshownFeatures += seqARep.appendFeatures(tooltipText,
+                    pos, mf, fr2, MAX_TOOLTIP_LENGTH);
           }
         }
       }
@@ -1245,7 +1241,7 @@ public class SeqPanel extends JPanel
   {
     char sequenceChar = sequence.getCharAt(column);
     int pos = sequence.findPosition(column);
-    setStatusMessage(sequence, seqIndex, sequenceChar, pos);
+    setStatusMessage(sequence.getName(), seqIndex, sequenceChar, pos);
 
     return pos;
   }
@@ -1261,7 +1257,7 @@ public class SeqPanel extends JPanel
    * Sequence 6 ID: O.niloticus.3 Nucleotide: Uracil (2)
    * </pre>
    * 
-   * @param sequence
+   * @param seqName
    * @param seqIndex
    *          sequence position in the alignment (1..)
    * @param sequenceChar
@@ -1269,7 +1265,7 @@ public class SeqPanel extends JPanel
    * @param residuePos
    *          the sequence residue position (if not over a gap)
    */
-  protected void setStatusMessage(SequenceI sequence, int seqIndex,
+  protected void setStatusMessage(String seqName, int seqIndex,
           char sequenceChar, int residuePos)
   {
     StringBuilder text = new StringBuilder(32);
@@ -1279,7 +1275,7 @@ public class SeqPanel extends JPanel
      */
     String seqno = seqIndex == -1 ? "" : " " + (seqIndex + 1);
     text.append("Sequence").append(seqno).append(" ID: ")
-            .append(sequence.getName());
+            .append(seqName);
 
     String residue = null;
 
@@ -1324,7 +1320,8 @@ public class SeqPanel extends JPanel
     {
       return;
     }
-    SequenceI ds = al.getSequenceAt(sequenceIndex).getDatasetSequence();
+    SequenceI alignedSeq = al.getSequenceAt(sequenceIndex);
+    SequenceI ds = alignedSeq.getDatasetSequence();
     for (SearchResultMatchI m : results.getResults())
     {
       SequenceI seq = m.getSequence();
@@ -1336,8 +1333,8 @@ public class SeqPanel extends JPanel
       if (seq == ds)
       {
         int start = m.getStart();
-        setStatusMessage(seq, sequenceIndex, seq.getCharAt(start - 1),
-                start);
+        setStatusMessage(alignedSeq.getName(), sequenceIndex,
+                seq.getCharAt(start - 1), start);
         return;
       }
     }
diff --git a/src/jalview/gui/Slider.java b/src/jalview/gui/Slider.java
new file mode 100644 (file)
index 0000000..7f18461
--- /dev/null
@@ -0,0 +1,113 @@
+package jalview.gui;
+
+import javax.swing.JSlider;
+
+/**
+ * A modified {@code javax.swing.JSlider} that
+ * <ul>
+ * <li>supports float valued numbers (by scaling up integer values)</li>
+ * <li>rescales 'true' value range to avoid negative values, as these are not
+ * rendered correctly by some look and feel libraries</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ */
+@SuppressWarnings("serial")
+public class Slider extends JSlider
+{
+  /*
+   * 'true' value corresponding to zero on the slider
+   */
+  private float trueMin;
+
+  /*
+   * 'true' value corresponding to slider maximum
+   */
+  private float trueMax;
+
+  /*
+   * scaleFactor applied to true value range to give a
+   * slider range of 0 - 100
+   */
+  private float sliderScaleFactor;
+
+  /**
+   * Constructor that rescales min - max to 0 - 100 for the slider
+   * 
+   * @param min
+   * @param max
+   * @param value
+   */
+  public Slider(float min, float max, float value)
+  {
+    super();
+    setSliderModel(min, max, value);
+  }
+
+  /**
+   * Sets the min-max range and current value of the slider, with rescaling from
+   * true values to slider range as required
+   * 
+   * @param min
+   * @param max
+   * @param value
+   */
+  public void setSliderModel(float min, float max, float value)
+  {
+    trueMin = min;
+    trueMax = max;
+    setMinimum(0);
+    sliderScaleFactor = 100f / (max - min);
+    int sliderMax = (int) ((max - min) * sliderScaleFactor);
+    setMaximum(sliderMax);
+    setSliderValue(value);
+  }
+
+  /**
+   * Answers the value of the slider position (descaled to 'true' value)
+   * 
+   * @return
+   */
+  public float getSliderValue()
+  {
+    /*
+     * convert slider max to 'true max' in case of rounding errors
+     */
+    int value = getValue();
+    return value == getMaximum() ? trueMax
+            : value / sliderScaleFactor + trueMin;
+  }
+
+  /**
+   * Sets the slider value (scaled from the true value to the slider range)
+   * 
+   * @param value
+   */
+  public void setSliderValue(float value)
+  {
+    setValue(Math.round((value - trueMin) * sliderScaleFactor));
+  }
+
+  /**
+   * Answers the value of the slider position as a percentage between minimum and
+   * maximum of its range
+   * 
+   * @return
+   */
+  public float getSliderPercentageValue()
+  {
+    return (getValue() - getMinimum()) * 100f
+            / (getMaximum() - getMinimum());
+  }
+
+  /**
+   * Sets the slider position for a given percentage value of its min-max range
+   * 
+   * @param pct
+   */
+  public void setSliderPercentageValue(float pct)
+  {
+    float pc = pct / 100f * getMaximum();
+    setValue((int) pc);
+  }
+}
index 0c8354b..617706a 100644 (file)
  */
 package jalview.gui;
 
-import jalview.api.structures.JalviewStructureDisplayI;
-import jalview.bin.Cache;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.datamodel.StructureViewerModel;
-import jalview.structure.StructureSelectionManager;
-
-import java.awt.Rectangle;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -35,6 +27,13 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
+import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.bin.Cache;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.StructureViewerModel;
+import jalview.structure.StructureSelectionManager;
+
 /**
  * A proxy for handling structure viewers, that orchestrates adding selected
  * structures, associated with sequences in Jalview, to an existing viewer, or
@@ -56,7 +55,7 @@ public class StructureViewer
 
   public enum ViewerType
   {
-    JMOL, CHIMERA
+    JMOL, CHIMERA, CHIMERAX, PYMOL
   };
 
   /**
@@ -165,6 +164,15 @@ public class StructureViewer
       sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs,
               ap);
     }
+    else if (viewerType.equals(ViewerType.CHIMERAX))
+    {
+      sview = new ChimeraXViewFrame(pdbsForFile, superposeAdded, theSeqs,
+              ap);
+    }
+    else if (viewerType.equals(ViewerType.PYMOL))
+    {
+      sview = new PymolViewer(pdbsForFile, superposeAdded, theSeqs, ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
@@ -305,6 +313,14 @@ public class StructureViewer
     {
       sview = new ChimeraViewFrame(pdb, seqsForPdb, null, ap);
     }
+    else if (viewerType.equals(ViewerType.CHIMERAX))
+    {
+      sview = new ChimeraXViewFrame(pdb, seqsForPdb, null, ap);
+    }
+    else if (viewerType.equals(ViewerType.PYMOL))
+    {
+      sview = new PymolViewer(pdb, seqsForPdb, null, ap);
+    }
     else
     {
       Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
@@ -313,42 +329,41 @@ public class StructureViewer
   }
 
   /**
-   * Create a new panel controlling a structure viewer.
+   * Creates a new panel controlling a structure viewer
    * 
    * @param type
-   * @param pdbf
-   * @param id
-   * @param sq
    * @param alignPanel
    * @param viewerData
-   * @param fileloc
-   * @param rect
+   * @param sessionFile
    * @param vid
    * @return
    */
-  public JalviewStructureDisplayI createView(ViewerType type, String[] pdbf,
-          String[] id, SequenceI[][] sq, AlignmentPanel alignPanel,
-          StructureViewerModel viewerData, String fileloc, Rectangle rect,
-          String vid)
+  public static JalviewStructureDisplayI createView(ViewerType type,
+          AlignmentPanel alignPanel, StructureViewerModel viewerData,
+          String sessionFile, String vid)
   {
-    final boolean useinViewerSuperpos = viewerData.isAlignWithPanel();
-    final boolean usetoColourbyseq = viewerData.isColourWithAlignPanel();
-    final boolean viewerColouring = viewerData.isColourByViewer();
-
+    JalviewStructureDisplayI viewer = null;
     switch (type)
     {
     case JMOL:
-      sview = new AppJmol(pdbf, id, sq, alignPanel, usetoColourbyseq,
-              useinViewerSuperpos, viewerColouring, fileloc, rect, vid);
+      viewer = new AppJmol(viewerData, alignPanel, sessionFile, vid);
+      // todo or construct and then openSession(sessionFile)?
       break;
     case CHIMERA:
-      Cache.log.error(
-              "Unsupported structure viewer type " + type.toString());
+      viewer = new ChimeraViewFrame(viewerData, alignPanel, sessionFile,
+              vid);
+      break;
+    case CHIMERAX:
+      viewer = new ChimeraXViewFrame(viewerData, alignPanel, sessionFile,
+              vid);
+      break;
+    case PYMOL:
+      viewer = new PymolViewer(viewerData, alignPanel, sessionFile, vid);
       break;
     default:
       Cache.log.error(UNKNOWN_VIEWER_TYPE + type.toString());
     }
-    return sview;
+    return viewer;
   }
 
   public boolean isBusy()
index 418a84d..a0b199b 100644 (file)
  */
 package jalview.gui;
 
-import jalview.api.AlignmentViewPanel;
-import jalview.bin.Cache;
-import jalview.datamodel.Alignment;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-import jalview.gui.JalviewColourChooser.ColourChooserListener;
-import jalview.gui.StructureViewer.ViewerType;
-import jalview.gui.ViewSelectionMenu.ViewSetProvider;
-import jalview.io.DataSourceType;
-import jalview.io.JalviewFileChooser;
-import jalview.io.JalviewFileView;
-import jalview.jbgui.GStructureViewer;
-import jalview.schemes.ColourSchemeI;
-import jalview.schemes.ColourSchemes;
-import jalview.structure.StructureMapping;
-import jalview.structures.models.AAStructureBindingModel;
-import jalview.util.MessageManager;
-
 import java.awt.Color;
 import java.awt.Component;
 import java.awt.event.ActionEvent;
@@ -54,6 +34,7 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Random;
 import java.util.Vector;
 
 import javax.swing.ButtonGroup;
@@ -64,6 +45,26 @@ import javax.swing.JRadioButtonMenuItem;
 import javax.swing.event.MenuEvent;
 import javax.swing.event.MenuListener;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.gui.ViewSelectionMenu.ViewSetProvider;
+import jalview.io.DataSourceType;
+import jalview.io.JalviewFileChooser;
+import jalview.io.JalviewFileView;
+import jalview.jbgui.GStructureViewer;
+import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ColourSchemes;
+import jalview.structure.StructureMapping;
+import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.BrowserLauncher;
+import jalview.util.MessageManager;
+import jalview.ws.dbsources.Pdb;
+
 /**
  * Base class with common functionality for JMol, Chimera or other structure
  * viewers.
@@ -90,13 +91,13 @@ public abstract class StructureViewerBase extends GStructureViewer
   /**
    * list of alignment panels to use for superposition
    */
-  protected Vector<AlignmentPanel> _alignwith = new Vector<>();
+  protected Vector<AlignmentViewPanel> _alignwith = new Vector<>();
 
   /**
    * list of alignment panels that are used for colouring structures by aligned
    * sequences
    */
-  protected Vector<AlignmentPanel> _colourwith = new Vector<>();
+  protected Vector<AlignmentViewPanel> _colourwith = new Vector<>();
 
   private String viewId = null;
 
@@ -121,6 +122,10 @@ public abstract class StructureViewerBase extends GStructureViewer
    */
   protected volatile boolean seqColoursApplied = false;
 
+  private IProgressIndicator progressBar = null;
+
+  private Random random = new Random();
+
   /**
    * Default constructor
    */
@@ -159,13 +164,14 @@ public abstract class StructureViewerBase extends GStructureViewer
     return _aps.contains(ap2.av.getSequenceSetId());
   }
 
-  public boolean isUsedforaligment(AlignmentPanel ap2)
+  public boolean isUsedforaligment(AlignmentViewPanel ap2)
   {
 
     return (_alignwith != null) && _alignwith.contains(ap2);
   }
 
-  public boolean isUsedforcolourby(AlignmentPanel ap2)
+  @Override
+  public boolean isUsedForColourBy(AlignmentViewPanel ap2)
   {
     return (_colourwith != null) && _colourwith.contains(ap2);
   }
@@ -193,8 +199,6 @@ public abstract class StructureViewerBase extends GStructureViewer
     this.viewId = viewId;
   }
 
-  public abstract String getStateInfo();
-
   protected void buildActionMenu()
   {
     if (_alignwith == null)
@@ -215,6 +219,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     }
   }
 
+  @Override
   public AlignmentPanel getAlignmentPanel()
   {
     return ap;
@@ -267,7 +272,8 @@ public abstract class StructureViewerBase extends GStructureViewer
    * 
    * @param nap
    */
-  public void removeAlignmentPanel(AlignmentPanel nap)
+  @Override
+  public void removeAlignmentPanel(AlignmentViewPanel nap)
   {
     try
     {
@@ -339,8 +345,6 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   public abstract ViewerType getViewerType();
 
-  protected abstract IProgressIndicator getIProgressIndicator();
-
   /**
    * add a new structure (with associated sequences and chains) to this viewer,
    * retrieving it if necessary first.
@@ -449,7 +453,7 @@ public abstract class StructureViewerBase extends GStructureViewer
      * create the mappings
      */
     apanel.getStructureSelectionManager().setMapping(seq, chains,
-            pdbFilename, DataSourceType.FILE, getIProgressIndicator());
+            pdbFilename, DataSourceType.FILE, getProgressIndicator());
 
     /*
      * alert the FeatureRenderer to show new (PDB RESNUM) features
@@ -554,8 +558,6 @@ public abstract class StructureViewerBase extends GStructureViewer
     }
   }
 
-  abstract void showSelectedChains();
-
   /**
    * Action on selecting one of Jalview's registered colour schemes
    */
@@ -566,7 +568,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     ColourSchemeI cs = ColourSchemes.getInstance()
             .getColourScheme(colourSchemeName, getAlignmentPanel().av, al,
                     null);
-    getBinding().setJalviewColourScheme(cs);
+    getBinding().colourByJalviewColourScheme(cs);
   }
 
   /**
@@ -600,7 +602,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        viewerColour_actionPerformed(actionEvent);
+        viewerColour_actionPerformed();
       }
     });
     colourMenu.add(viewerColour);
@@ -616,7 +618,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        background_actionPerformed(actionEvent);
+        background_actionPerformed();
       }
     });
     colourMenu.add(backGround);
@@ -647,7 +649,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        seqColour_actionPerformed(actionEvent);
+        seqColour_actionPerformed();
       }
     });
 
@@ -659,7 +661,7 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        chainColour_actionPerformed(actionEvent);
+        chainColour_actionPerformed();
       }
     });
 
@@ -671,12 +673,15 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        chargeColour_actionPerformed(actionEvent);
+        chargeColour_actionPerformed();
       }
     });
 
     viewerColour = new JRadioButtonMenuItem();
-    // text is set in overrides of this method
+    viewerColour
+            .setText(MessageManager.getString("label.colour_with_viewer"));
+    viewerColour.setToolTipText(MessageManager
+            .getString("label.let_viewer_manage_structure_colours"));
     viewerColour.setName(ViewerColour.ByViewer.name());
     viewerColour.setSelected(!binding.isColourBySequence());
 
@@ -702,8 +707,8 @@ public abstract class StructureViewerBase extends GStructureViewer
                 }
                 else
                 {
-                  // update the Chimera display now.
-                  seqColour_actionPerformed(null);
+                  // update the viewer display now.
+                  seqColour_actionPerformed();
                 }
               }
             });
@@ -714,10 +719,18 @@ public abstract class StructureViewerBase extends GStructureViewer
       @Override
       public void itemStateChanged(ItemEvent e)
       {
-        alignStructs.setEnabled(!_alignwith.isEmpty());
-        alignStructs.setToolTipText(MessageManager.formatMessage(
-                "label.align_structures_using_linked_alignment_views",
-                _alignwith.size()));
+        if (_alignwith.isEmpty())
+        {
+          alignStructs.setEnabled(false);
+          alignStructs.setToolTipText(null);
+        }
+        else
+        {
+          alignStructs.setEnabled(true);
+          alignStructs.setToolTipText(MessageManager.formatMessage(
+                  "label.align_structures_using_linked_alignment_views",
+                  _alignwith.size()));
+        }
       }
     };
     viewSelectionMenu = new ViewSelectionMenu(
@@ -744,13 +757,11 @@ public abstract class StructureViewerBase extends GStructureViewer
       }
     });
 
-    buildColourMenu();
-  }
+    viewerActionMenu.setText(getViewerName());
+    helpItem.setText(MessageManager.formatMessage("label.viewer_help",
+            getViewerName()));
 
-  @Override
-  public void setJalviewColourScheme(ColourSchemeI cs)
-  {
-    getBinding().setJalviewColourScheme(cs);
+    buildColourMenu();
   }
 
   /**
@@ -759,12 +770,7 @@ public abstract class StructureViewerBase extends GStructureViewer
    * the operation.
    */
   @Override
-  protected String alignStructs_actionPerformed(ActionEvent actionEvent)
-  {
-    return alignStructs_withAllAlignPanels();
-  }
-
-  protected String alignStructs_withAllAlignPanels()
+  protected String alignStructsWithAllAlignPanels()
   {
     if (getAlignmentPanel() == null)
     {
@@ -779,19 +785,8 @@ public abstract class StructureViewerBase extends GStructureViewer
     String reply = null;
     try
     {
-      AlignmentI[] als = new Alignment[_alignwith.size()];
-      HiddenColumns[] alc = new HiddenColumns[_alignwith.size()];
-      int[] alm = new int[_alignwith.size()];
-      int a = 0;
-
-      for (AlignmentPanel alignPanel : _alignwith)
-      {
-        als[a] = alignPanel.av.getAlignment();
-        alm[a] = -1;
-        alc[a++] = alignPanel.av.getAlignment().getHiddenColumns();
-      }
-      reply = getBinding().superposeStructures(als, alm, alc);
-      if (reply != null)
+      reply = getBinding().superposeStructures(_alignwith);
+      if (reply != null && !reply.isEmpty())
       {
         String text = MessageManager
                 .formatMessage("error.superposition_failed", reply);
@@ -800,9 +795,9 @@ public abstract class StructureViewerBase extends GStructureViewer
     } catch (Exception e)
     {
       StringBuffer sp = new StringBuffer();
-      for (AlignmentPanel alignPanel : _alignwith)
+      for (AlignmentViewPanel alignPanel : _alignwith)
       {
-        sp.append("'" + alignPanel.alignFrame.getTitle() + "' ");
+        sp.append("'" + alignPanel.getViewName() + "' ");
       }
       Cache.log.info("Couldn't align structures with the " + sp.toString()
               + "associated alignment panels.", e);
@@ -815,7 +810,7 @@ public abstract class StructureViewerBase extends GStructureViewer
    * background of the structure viewer
    */
   @Override
-  public void background_actionPerformed(ActionEvent actionEvent)
+  public void background_actionPerformed()
   {
     String ttl = MessageManager.getString("label.select_background_colour");
     ColourChooserListener listener = new ColourChooserListener()
@@ -830,7 +825,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
-  public void viewerColour_actionPerformed(ActionEvent actionEvent)
+  public void viewerColour_actionPerformed()
   {
     if (viewerColour.isSelected())
     {
@@ -840,21 +835,21 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
-  public void chainColour_actionPerformed(ActionEvent actionEvent)
+  public void chainColour_actionPerformed()
   {
     chainColour.setSelected(true);
     getBinding().colourByChain();
   }
 
   @Override
-  public void chargeColour_actionPerformed(ActionEvent actionEvent)
+  public void chargeColour_actionPerformed()
   {
     chargeColour.setSelected(true);
     getBinding().colourByCharge();
   }
 
   @Override
-  public void seqColour_actionPerformed(ActionEvent actionEvent)
+  public void seqColour_actionPerformed()
   {
     AAStructureBindingModel binding = getBinding();
     binding.setColourBySequence(seqColour.isSelected());
@@ -873,7 +868,7 @@ public abstract class StructureViewerBase extends GStructureViewer
         }
       }
       // Set the colour using the current view for the associated alignframe
-      for (AlignmentPanel alignPanel : _colourwith)
+      for (AlignmentViewPanel alignPanel : _colourwith)
       {
         binding.colourBySequence(alignPanel);
       }
@@ -882,7 +877,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
-  public void pdbFile_actionPerformed(ActionEvent actionEvent)
+  public void pdbFile_actionPerformed()
   {
     // TODO: JAL-3048 not needed for Jalview-JS - save PDB file
     JalviewFileChooser chooser = new JalviewFileChooser(
@@ -934,7 +929,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   @Override
-  public void viewMapping_actionPerformed(ActionEvent actionEvent)
+  public void viewMapping_actionPerformed()
   {
     CutAndPasteTransfer cap = new CutAndPasteTransfer();
     try
@@ -975,7 +970,7 @@ public abstract class StructureViewerBase extends GStructureViewer
      * enable 'Superpose with' if more than one mapped structure
      */
     viewSelectionMenu.setEnabled(false);
-    if (getBinding().getStructureFiles().length > 1
+    if (getBinding().getMappedStructureCount() > 1
             && getBinding().getSequence().length > 1)
     {
       viewSelectionMenu.setEnabled(true);
@@ -996,7 +991,7 @@ public abstract class StructureViewerBase extends GStructureViewer
 
     if (!binding.isLoadingFromArchive())
     {
-      seqColour_actionPerformed(null);
+      seqColour_actionPerformed();
     }
   }
 
@@ -1049,4 +1044,202 @@ public abstract class StructureViewerBase extends GStructureViewer
     toFront();
   }
 
+  @Override
+  public long startProgressBar(String msg)
+  {
+    // TODO would rather have startProgress/stopProgress as the
+    // IProgressIndicator interface
+    long tm = random.nextLong();
+    if (progressBar != null)
+    {
+      progressBar.setProgressBar(msg, tm);
+    }
+    return tm;
+  }
+
+  @Override
+  public void stopProgressBar(String msg, long handle)
+  {
+    if (progressBar != null)
+    {
+      progressBar.setProgressBar(msg, handle);
+    }
+  }
+
+  protected IProgressIndicator getProgressIndicator()
+  {
+    return progressBar;
+  }
+
+  protected void setProgressIndicator(IProgressIndicator pi)
+  {
+    progressBar = pi;
+  }
+
+  protected void setProgressMessage(String message, long id)
+  {
+    if (progressBar != null)
+    {
+      progressBar.setProgressBar(message, id);
+    }
+  }
+
+  @Override
+  public void showConsole(boolean show)
+  {
+    // default does nothing
+  }
+
+  /**
+   * Show only the selected chain(s) in the viewer
+   */
+  protected void showSelectedChains()
+  {
+    List<String> toshow = new ArrayList<>();
+    for (int i = 0; i < chainMenu.getItemCount(); i++)
+    {
+      if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
+      {
+        JCheckBoxMenuItem item = (JCheckBoxMenuItem) chainMenu.getItem(i);
+        if (item.isSelected())
+        {
+          toshow.add(item.getText());
+        }
+      }
+    }
+    getBinding().showChains(toshow);
+  }
+
+  /**
+   * Tries to fetch a PDB file and save to a temporary local file. Returns the
+   * saved file path if successful, or null if not.
+   * 
+   * @param processingEntry
+   * @return
+   */
+  protected String fetchPdbFile(PDBEntry processingEntry)
+  {
+    String filePath = null;
+    Pdb pdbclient = new Pdb();
+    AlignmentI pdbseq = null;
+    String pdbid = processingEntry.getId();
+    long handle = System.currentTimeMillis()
+            + Thread.currentThread().hashCode();
+  
+    /*
+     * Write 'fetching PDB' progress on AlignFrame as we are not yet visible
+     */
+    String msg = MessageManager.formatMessage("status.fetching_pdb",
+            new Object[]
+            { pdbid });
+    getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
+    // long hdl = startProgressBar(MessageManager.formatMessage(
+    // "status.fetching_pdb", new Object[]
+    // { pdbid }));
+    try
+    {
+      pdbseq = pdbclient.getSequenceRecords(pdbid);
+    } catch (Exception e)
+    {
+      System.err.println(
+              "Error retrieving PDB id " + pdbid + ": " + e.getMessage());
+    } finally
+    {
+      msg = pdbid + " " + MessageManager.getString("label.state_completed");
+      getAlignmentPanel().alignFrame.setProgressBar(msg, handle);
+      // stopProgressBar(msg, hdl);
+    }
+    /*
+     * If PDB data were saved and are not invalid (empty alignment), return the
+     * file path.
+     */
+    if (pdbseq != null && pdbseq.getHeight() > 0)
+    {
+      // just use the file name from the first sequence's first PDBEntry
+      filePath = new File(pdbseq.getSequenceAt(0).getAllPDBEntries()
+              .elementAt(0).getFile()).getAbsolutePath();
+      processingEntry.setFile(filePath);
+    }
+    return filePath;
+  }
+
+  /**
+   * If supported, saves the state of the structure viewer to a temporary file
+   * and returns the file, else returns null
+   * 
+   * @return
+   */
+  public File saveSession()
+  {
+    // TODO: a wait loop to ensure the file is written fully before returning?
+    return getBinding() == null ? null : getBinding().saveSession();
+  }
+
+  /**
+   * Close down this instance of Jalview's Chimera viewer, giving the user the
+   * option to close the associated Chimera window (process). They may wish to
+   * keep it open until they have had an opportunity to save any work.
+   * 
+   * @param forceClose
+   *          if true, close any linked Chimera process; if false, prompt first
+   */
+  @Override
+  public void closeViewer(boolean forceClose)
+  {
+    AAStructureBindingModel binding = getBinding();
+    if (binding != null && binding.isViewerRunning())
+    {
+      if (!forceClose)
+      {
+        String viewerName = getViewerName();
+        String prompt = MessageManager
+                .formatMessage("label.confirm_close_viewer", new Object[]
+                { binding.getViewerTitle(viewerName, false), viewerName });
+        prompt = JvSwingUtils.wrapTooltip(true, prompt);
+        int confirm = JvOptionPane.showConfirmDialog(this, prompt,
+                MessageManager.getString("label.close_viewer"),
+                JvOptionPane.YES_NO_CANCEL_OPTION);
+        /*
+         * abort closure if user hits escape or Cancel
+         */
+        if (confirm == JvOptionPane.CANCEL_OPTION
+                || confirm == JvOptionPane.CLOSED_OPTION)
+        {
+          return;
+        }
+        forceClose = confirm == JvOptionPane.YES_OPTION;
+      }
+    }
+    if (binding != null)
+    {
+      binding.closeViewer(forceClose);
+    }
+    setAlignmentPanel(null);
+    _aps.clear();
+    _alignwith.clear();
+    _colourwith.clear();
+    // TODO: check for memory leaks where instance isn't finalised because jmb
+    // holds a reference to the window
+    // jmb = null;
+    dispose();
+  }
+
+  @Override
+  public void showHelp_actionPerformed()
+  {
+    try
+    {
+      String url = getBinding().getHelpURL();
+      if (url != null)
+      {
+        BrowserLauncher.openURL(url);
+      }
+    } catch (IOException ex)
+    {
+      System.err
+              .println("Show " + getViewerName() + " failed with: "
+                      + ex.getMessage());
+    }
+  }
+
 }
index 0848a4d..19ede62 100644 (file)
@@ -264,8 +264,8 @@ public class VamsasApplication implements SelectionSource, VamsasSource
   {
     if (!inSession())
     {
-      throw new Error(MessageManager.getString(
-              "error.implementation_error_vamsas_operation_not_init"));
+      throw new Error(
+              "Implementation error! Vamsas Operations when client not initialised and connected");
     }
     addDocumentUpdateHandler();
     addStoreDocumentHandler();
@@ -351,8 +351,7 @@ public class VamsasApplication implements SelectionSource, VamsasSource
   {
     if (!inSession())
     {
-      throw new Error(MessageManager
-              .getString("error.jalview_no_connected_vamsas_session"));
+      throw new Error("Jalview not connected to Vamsas session");
     }
     Cache.log.info("Jalview disconnecting from the Vamsas Session.");
     try
@@ -722,8 +721,8 @@ public class VamsasApplication implements SelectionSource, VamsasSource
         return;
       }
 
-      throw new Error(MessageManager.getString(
-              "error.implementation_error_cannot_recover_vamsas_object_mappings"));
+      throw new Error(
+              "IMPLEMENTATION ERROR: Cannot recover vamsas object mappings - no backup was made");
     }
     jv2vobj.clear();
     Iterator el = _backup_jv2vobj.entrySet().iterator();
index 2a7743a..a1529fc 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import jalview.api.AlignmentViewPanel;
 import jalview.util.MessageManager;
 
 import java.awt.Component;
@@ -56,7 +57,7 @@ public class ViewSelectionMenu extends JMenu
 
   private ViewSetProvider _allviews;
 
-  private List<AlignmentPanel> _selectedviews;
+  private List<AlignmentViewPanel> _selectedviews;
 
   private ItemListener _handler;
 
@@ -79,7 +80,7 @@ public class ViewSelectionMenu extends JMenu
    *          selection/deselection state
    */
   public ViewSelectionMenu(String title, final ViewSetProvider allviews,
-          final List<AlignmentPanel> selectedviews,
+          final List<AlignmentViewPanel> selectedviews,
           final ItemListener handler)
   {
     super(title);
index 573040f..b8a721e 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io;
 
 import java.io.File;
index 1504404..339cefa 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io;
 
 import java.io.File;
index 06c2fc9..0d5f92b 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io;
 
 import jalview.bin.Cache;
index 4face29..69130d0 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io;
 
 import jalview.bin.Cache;
index d2a9b0a..c08c84e 100755 (executable)
  */
 package jalview.io;
 
+import java.awt.Color;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.SequenceIdMatcher;
 import jalview.api.AlignViewportI;
@@ -44,18 +56,6 @@ import jalview.util.MapList;
 import jalview.util.ParseHtmlBodyAndLinks;
 import jalview.util.StringUtils;
 
-import java.awt.Color;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TreeMap;
-
 /**
  * Parses and writes features files, which may be in Jalview, GFF2 or GFF3
  * format. These are tab-delimited formats but with differences in the use of
@@ -736,7 +736,6 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       if (mf != null)
       {
-        MapList mapping = mf.mapping.getMap();
         for (SequenceFeature sf : mf.features)
         {
           /*
@@ -752,9 +751,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
             found.add(sf);
             int begin = sf.getBegin();
             int end = sf.getEnd();
-            int[] range = mf.mapping.getTo() == seq.getDatasetSequence()
-                    ? mapping.locateInTo(begin, end)
-                    : mapping.locateInFrom(begin, end);
+            int[] range = mf.getMappedPositions(begin, end);
             SequenceFeature sf2 = new SequenceFeature(sf, range[0],
                     range[1], group, sf.getScore());
             complementary.add(sf2);
index a5a4e36..39d8ad4 100755 (executable)
  */
 package jalview.io;
 
+import jalview.api.AlignExportSettingsI;
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureSettingsModelI;
+import jalview.util.MessageManager;
+
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -30,15 +37,12 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.io.StringReader;
+import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.net.URLConnection;
 import java.util.zip.GZIPInputStream;
 
-import jalview.api.AlignExportSettingsI;
-import jalview.api.AlignViewportI;
-import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureSettingsModelI;
-import jalview.util.MessageManager;
 import jalview.util.Platform;
 
 /**
@@ -64,6 +68,7 @@ public class FileParse
   {
     return bytes;
   }
+
   /**
    * a viewport associated with the current file operation. May be null. May
    * move to different object.
@@ -190,29 +195,73 @@ public class FileParse
     }
     if (!error)
     {
-      if (fileStr.toLowerCase().endsWith(".gz"))
+      try
       {
-        try
-        {
-          dataIn = tryAsGzipSource(new FileInputStream(fileStr));
-          dataName = fileStr;
-          return error;
-        } catch (Exception x)
-        {
-          warningMessage = "Failed  to resolve as a GZ stream ("
-                  + x.getMessage() + ")";
-          // x.printStackTrace();
-        }
-        ;
+        dataIn = checkForGzipStream(new FileInputStream(fileStr));
+        dataName = fileStr;
+      } catch (Exception x)
+      {
+        warningMessage = "Failed to resolve " + fileStr
+                + " as a data source. (" + x.getMessage() + ")";
+        // x.printStackTrace();
+        error = true;
       }
-
-      dataIn = new BufferedReader(new FileReader(fileStr));
-      dataName = fileStr;
+      ;
     }
     return error;
   }
+  
+  /**
+   * Recognise the 2-byte magic header for gzip streams
+   * 
+   * https://recalll.co/ask/v/topic/java-How-to-check-if-InputStream-is-Gzipped/555aadd62bd27354438b90f6
+   * 
+   * @param bytes - at least two bytes 
+   * @return 
+   */
+  private static boolean isGzipStream(byte[] bytes) {
+    int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
+    return (GZIPInputStream.GZIP_MAGIC == head);
+  }
 
-  private BufferedReader tryAsGzipSource(InputStream inputStream)
+  /**
+   * Returns a Reader for the given input after wrapping it in a buffered input
+   * stream, and then checking if it needs to be wrapped by a GZipInputStream
+   * 
+   * @param input
+   * @return
+   */
+  private BufferedReader checkForGzipStream(InputStream input) throws Exception {
+
+    // NB: stackoverflow https://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped
+    // could use a PushBackInputStream rather than a BufferedInputStream
+    
+    BufferedInputStream bufinput;
+    if (!input.markSupported()) {
+       bufinput= new BufferedInputStream(input,16);
+       input = bufinput;
+    }
+    input.mark(4);
+    byte[] bytes=input.readNBytes(2);
+    input.reset();
+    if (bytes.length==2 && isGzipStream(bytes)) {
+      return getGzipReader(input);
+    }
+    // return a buffered reader for the stream.
+    InputStreamReader isReader= new InputStreamReader(input);
+    BufferedReader toReadFrom=new BufferedReader(isReader);
+    return toReadFrom;
+  }
+  /**
+   * Returns a {@code BufferedReader} which wraps the input stream with a
+   * GZIPInputStream. Throws a {@code ZipException} if a GZIP format error
+   * occurs or the compression method used is unsupported.
+   * 
+   * @param inputStream
+   * @return
+   * @throws Exception
+   */
+  private BufferedReader getGzipReader(InputStream inputStream)
           throws Exception
   {
     BufferedReader inData = new BufferedReader(
@@ -223,44 +272,74 @@ public class FileParse
     return inData;
   }
 
-  private boolean checkURLSource(String fileStr)
+  /**
+   * Tries to read from the given URL. If successful, saves a reader to the
+   * response in field {@code dataIn}, otherwise (on exception, or HTTP response
+   * status not 200), throws an exception.
+   * <p>
+   * If the response status includes
+   * 
+   * <pre>
+   * Content-Type : application/x-gzip
+   * </pre>
+   * 
+   * then tries to read as gzipped content.
+   * 
+   * @param urlStr
+   * @throws IOException
+   * @throws MalformedURLException
+   */
+  private void checkURLSource(String urlStr)
           throws IOException, MalformedURLException
   {
     errormessage = "URL NOT FOUND";
-    URL url = new URL(fileStr);
-    //
-    // GZIPInputStream code borrowed from Aquaria (soon to be open sourced) via
-    // Kenny Sabir
-    Exception e = null;
-    if (fileStr.toLowerCase().endsWith(".gz"))
+    URL url = new URL(urlStr);
+    URLConnection _conn = url.openConnection();
+    if (_conn instanceof HttpURLConnection)
     {
-      try
+      HttpURLConnection conn = (HttpURLConnection) _conn;
+      int rc = conn.getResponseCode();
+      if (rc != HttpURLConnection.HTTP_OK)
       {
-        InputStream inputStream = url.openStream();
-        dataIn = tryAsGzipSource(inputStream);
-        dataName = fileStr;
-        return false;
+        throw new IOException(
+                "Response status from " + urlStr + " was " + rc);
+      }
+    } else {
+      try {
+      dataIn = checkForGzipStream(_conn.getInputStream());
+      dataName=urlStr;
+      } catch (IOException ex)
+      {
+        throw new IOException("Failed to handle non-HTTP URI stream",ex);
       } catch (Exception ex)
       {
-        e = ex;
+        throw new IOException("Failed to determine type of input stream for given URI",ex);
       }
+      return;
     }
-
-    try
-    {
-      dataIn = new BufferedReader(new InputStreamReader(url.openStream()));
-    } catch (IOException q)
+    String encoding = _conn.getContentEncoding();
+    String contentType = _conn.getContentType();
+    boolean isgzipped = "application/x-gzip".equalsIgnoreCase(contentType)
+            || "gzip".equals(encoding);
+    Exception e = null;
+    InputStream inputStream = _conn.getInputStream();
+    if (isgzipped)
     {
-      if (e != null)
+      try
+      {
+        dataIn = getGzipReader(inputStream);
+        dataName = urlStr;
+      } catch (Exception e1)
       {
         throw new IOException(MessageManager
                 .getString("exception.failed_to_resolve_gzip_stream"), e);
       }
-      throw q;
+      return;
     }
-    // record URL as name of datasource.
-    dataName = fileStr;
-    return false;
+
+    dataIn = new BufferedReader(new InputStreamReader(inputStream));
+    dataName = urlStr;
+    return;
   }
 
   /**
@@ -345,7 +424,8 @@ public class FileParse
       {
         // this will be from JavaScript
         inFile = file;
-        dataIn = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
+        dataIn = new BufferedReader(
+                new InputStreamReader(new ByteArrayInputStream(bytes)));
         dataName = fileStr;
       }
       else if (checkFileSource(fileStr))
@@ -453,8 +533,7 @@ public class FileParse
     {
       // pass up the reason why we have no source to read from
       throw new IOException(MessageManager.formatMessage(
-              "exception.failed_to_read_data_from_source",
-              new String[]
+              "exception.failed_to_read_data_from_source", new String[]
               { errormessage }));
     }
     error = false;
index 084dbc5..77a9a47 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io;
 
 public class IntKeyStringValueEntry
index 9d7fcfc..8e4e783 100644 (file)
@@ -55,9 +55,9 @@ public class SequenceAnnotationReport
 
   private static final int MAX_SOURCES = 40;
 
- // public static final String[][] PRIMARY_SOURCES  moved to DBRefSource.java
+  private static String linkImageURL;
 
-  final String linkImageURL;
+ // public static final String[][] PRIMARY_SOURCES  moved to DBRefSource.java
 
   /*
    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
@@ -117,14 +117,30 @@ public class SequenceAnnotationReport
 //    }
   };
 
-  public SequenceAnnotationReport(String linkURL)
+  private boolean forTooltip;
+
+  /**
+   * Constructor given a flag which affects behaviour
+   * <ul>
+   * <li>if true, generates feature details suitable to show in a tooltip</li>
+   * <li>if false, generates feature details in a form suitable for the sequence
+   * details report</li>
+   * </ul>
+   * 
+   * @param isForTooltip
+   */
+  public SequenceAnnotationReport(boolean isForTooltip)
   {
-    this.linkImageURL = linkURL;
+    this.forTooltip = isForTooltip;
+    if (linkImageURL == null)
+    {
+      linkImageURL = getClass().getResource("/images/link.gif").toString();
+    }
   }
 
   /**
-   * Append text for the list of features to the tooltip Returns number of
-   * features left if maxlength limit is (or would have been) reached
+   * Append text for the list of features to the tooltip. Returns the number of
+   * features not added if maxlength limit is (or would have been) reached.
    * 
    * @param sb
    * @param residuePos
@@ -132,7 +148,7 @@ public class SequenceAnnotationReport
    * @param minmax
    * @param maxlength
    */
-  public int appendFeaturesLengthLimit(final StringBuilder sb,
+  public int appendFeatures(final StringBuilder sb,
           int residuePos, List<SequenceFeature> features,
           FeatureRendererModel fr, int maxlength)
   {
@@ -147,16 +163,10 @@ public class SequenceAnnotationReport
     return 0;
   }
 
-  public void appendFeatures(final StringBuilder sb, int residuePos,
-          List<SequenceFeature> features, FeatureRendererModel fr)
-  {
-    appendFeaturesLengthLimit(sb, residuePos, features, fr, 0);
-  }
-
   /**
-   * Appends text for mapped features (e.g. CDS feature for peptide or vice versa)
-   * Returns number of features left if maxlength limit is (or would have been)
-   * reached
+   * Appends text for mapped features (e.g. CDS feature for peptide or vice
+   * versa) Returns number of features left if maxlength limit is (or would have
+   * been) reached.
    * 
    * @param sb
    * @param residuePos
@@ -164,7 +174,7 @@ public class SequenceAnnotationReport
    * @param fr
    * @param maxlength
    */
-  public int appendFeaturesLengthLimit(StringBuilder sb, int residuePos,
+  public int appendFeatures(StringBuilder sb, int residuePos,
           MappedFeatures mf, FeatureRendererModel fr, int maxlength)
   {
     for (int i = 0; i < mf.features.size(); i++)
@@ -178,12 +188,6 @@ public class SequenceAnnotationReport
     return 0;
   }
 
-  public void appendFeatures(StringBuilder sb, int residuePos,
-          MappedFeatures mf, FeatureRendererModel fr)
-  {
-    appendFeaturesLengthLimit(sb, residuePos, mf, fr, 0);
-  }
-
   /**
    * Appends the feature at rpos to the given buffer
    * 
@@ -196,19 +200,49 @@ public class SequenceAnnotationReport
           FeatureRendererModel fr, SequenceFeature feature,
           MappedFeatures mf, int maxlength)
   {
+    int begin = feature.getBegin();
+    int end = feature.getEnd();
+
+    /*
+     * if this is a virtual features, convert begin/end to the
+     * coordinates of the sequence it is mapped to
+     */
+    int[] beginRange = null;
+    int[] endRange = null;
+    if (mf != null)
+    {
+      beginRange = mf.getMappedPositions(begin, begin);
+      endRange = mf.getMappedPositions(end, end);
+      if (beginRange == null || endRange == null)
+      {
+        // something went wrong
+        return false;
+      }
+      begin = beginRange[0];
+      end = endRange[endRange.length - 1];
+    }
+
     StringBuilder sb = new StringBuilder();
     if (feature.isContactFeature())
     {
-      if (feature.getBegin() == rpos || feature.getEnd() == rpos)
+      /*
+       * include if rpos is at start or end position of [mapped] feature
+       */
+      boolean showContact = (mf == null) && (rpos == begin || rpos == end);
+      boolean showMappedContact = (mf != null) && ((rpos >= beginRange[0]
+              && rpos <= beginRange[beginRange.length - 1])
+              || (rpos >= endRange[0]
+                      && rpos <= endRange[endRange.length - 1]));
+      if (showContact || showMappedContact)
       {
         if (sb0.length() > 6)
         {
           sb.append("<br/>");
         }
-        sb.append(feature.getType()).append(" ").append(feature.getBegin())
-                .append(":").append(feature.getEnd());
+        sb.append(feature.getType()).append(" ").append(begin).append(":")
+                .append(end);
       }
-      return appendTextMaxLengthReached(sb0, sb, maxlength);
+      return appendText(sb0, sb, maxlength);
     }
 
     if (sb0.length() > 6)
@@ -223,11 +257,11 @@ public class SequenceAnnotationReport
       if (rpos != 0)
       {
         // we are marking a positional feature
-        sb.append(feature.begin);
-      }
-      if (feature.begin != feature.end)
-      {
-        sb.append(" ").append(feature.end);
+        sb.append(begin);
+        if (begin != end)
+        {
+          sb.append(" ").append(end);
+        }
       }
 
       String description = feature.getDescription();
@@ -288,29 +322,21 @@ public class SequenceAnnotationReport
         }
       }
     }
-    return appendTextMaxLengthReached(sb0, sb, maxlength);
-  }
-
-  void appendFeature(final StringBuilder sb, int rpos,
-          FeatureRendererModel fr, SequenceFeature feature,
-          MappedFeatures mf)
-  {
-    appendFeature(sb, rpos, fr, feature, mf, 0);
+    return appendText(sb0, sb, maxlength);
   }
 
   /**
-   * Appends {@code sb} to {@code sb0}, and returns false, unless
-   * {@code maxlength} is not zero and appending would make the total length
-   * greater than {@code maxlength}, in which case the text is not appended, and
-   * the method returns true.
+   * Appends sb to sb0, and returns false, unless maxlength is not zero and
+   * appending would make the result longer than or equal to maxlength, in which
+   * case the append is not done and returns true
    * 
    * @param sb0
    * @param sb
    * @param maxlength
    * @return
    */
-  private static boolean appendTextMaxLengthReached(StringBuilder sb0,
-          StringBuilder sb, int maxlength)
+  private static boolean appendText(StringBuilder sb0, StringBuilder sb,
+          int maxlength)
   {
     if (maxlength == 0 || sb0.length() + sb.length() < maxlength)
     {
@@ -472,7 +498,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, fr, sf, null);
+        appendFeature(sb, 0, fr, sf, null, 0);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
index 84e629e..8b26757 100644 (file)
  */
 package jalview.io;
 
-import jalview.analysis.Rna;
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.Annotation;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.Mapping;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.schemes.ResidueProperties;
-import jalview.util.Comparison;
-import jalview.util.Format;
-import jalview.util.MessageManager;
-
 import java.io.BufferedReader;
 import java.io.FileReader;
 import java.io.IOException;
@@ -53,6 +39,21 @@ import com.stevesoft.pat.Regex;
 import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
 import fr.orsay.lri.varna.factories.RNAFactory;
 import fr.orsay.lri.varna.models.rna.RNA;
+import jalview.analysis.Rna;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.schemes.ResidueProperties;
+import jalview.util.Comparison;
+import jalview.util.DBRefUtils;
+import jalview.util.Format;
+import jalview.util.MessageManager;
 
 // import org.apache.log4j.*;
 
@@ -332,17 +333,14 @@ public class StockholmFile extends AlignFile
 
           if (accAnnotations != null && accAnnotations.containsKey("AC"))
           {
-            if (dbsource != null)
+            String dbr = (String) accAnnotations.get("AC");
+            if (dbr != null)
             {
-              String dbr = (String) accAnnotations.get("AC");
-              if (dbr != null)
-              {
-                // we could get very clever here - but for now - just try to
-                // guess accession type from source of alignment plus structure
-                // of accession
-                guessDatabaseFor(seqO, dbr, dbsource);
-
-              }
+              // we could get very clever here - but for now - just try to
+              // guess accession type from type of sequence, source of alignment plus
+              // structure
+              // of accession
+              guessDatabaseFor(seqO, dbr, dbsource);
             }
             // else - do what ? add the data anyway and prompt the user to
             // specify what references these are ?
@@ -527,6 +525,9 @@ public class StockholmFile extends AlignFile
               treeName = an.stringMatched(2);
               treeString = new StringBuffer();
             }
+            // TODO: JAL-3532 - this is where GF comments and database references are lost
+            // suggest overriding this method for Stockholm files to catch and properly
+            // process CC, DR etc into multivalued properties
             setAlignmentProperty(an.stringMatched(1), an.stringMatched(2));
           }
         }
@@ -755,6 +756,12 @@ public class StockholmFile extends AlignFile
         st = -1;
       }
     }
+    if (dbsource == null)
+    {
+      // make up an origin based on whether the sequence looks like it is nucleotide
+      // or protein
+      dbsource = (seqO.isProtein()) ? "PFAM" : "RFAM";
+    }
     if (dbsource.equals("PFAM"))
     {
       seqdb = "UNIPROT";
@@ -930,6 +937,11 @@ public class StockholmFile extends AlignFile
     return annot;
   }
 
+  private String dbref_to_ac_record(DBRefEntry ref)
+  {
+    return ref.getSource().toString() + " ; "
+            + ref.getAccessionId().toString();
+  }
   @Override
   public String print(SequenceI[] s, boolean jvSuffix)
   {
@@ -944,6 +956,7 @@ public class StockholmFile extends AlignFile
     int slen = s.length;
     SequenceI seq;
     Hashtable<String, String> dataRef = null;
+    boolean isAA = s[in].isProtein();
     while ((in < slen) && ((seq = s[in]) != null))
     {
       String tmp = printId(seq, jvSuffix);
@@ -961,14 +974,29 @@ public class StockholmFile extends AlignFile
         {
           dataRef = new Hashtable<>();
         }
-        for (int idb = 0; idb < ndb; idb++)
+        List<DBRefEntry> primrefs = seq.getPrimaryDBRefs();
+        if (primrefs.size() >= 1)
         {
-
-          DBRefEntry ref = seqrefs.get(idb);
-          String datAs1 = ref.getSource().toString()
-                  + " ; "
-                  + ref.getAccessionId().toString();
-          dataRef.put(tmp, datAs1);
+          dataRef.put(tmp, dbref_to_ac_record(primrefs.get(0)));
+        }
+        else
+        {
+          for (int idb = 0; idb < seq.getDBRefs().size(); idb++)
+          {
+            DBRefEntry dbref = seq.getDBRefs().get(idb);
+            dataRef.put(tmp, dbref_to_ac_record(dbref));
+            // if we put in a uniprot or EMBL record then we're done:
+            if (isAA && DBRefSource.UNIPROT
+                    .equals(DBRefUtils.getCanonicalName(dbref.getSource())))
+            {
+              break;
+            }
+            if (!isAA && DBRefSource.EMBL
+                    .equals(DBRefUtils.getCanonicalName(dbref.getSource())))
+            {
+              break;
+            }
+          }
         }
       }
       in++;
@@ -998,10 +1026,11 @@ public class StockholmFile extends AlignFile
       while (en.hasMoreElements())
       {
         Object idd = en.nextElement();
-        String type = (String) dataRef.remove(idd);
+        String type = dataRef.remove(idd);
         out.append(new Format("%-" + (maxid - 2) + "s")
                 .form("#=GS " + idd.toString() + " "));
-        if (type.contains("PFAM") || type.contains("RFAM"))
+        if (isAA && type.contains("UNIPROT")
+                || (!isAA && type.contains("EMBL")))
         {
 
           out.append(" AC " + type.substring(type.indexOf(";") + 1));
index 387dbfa..2fc526b 100644 (file)
@@ -338,10 +338,8 @@ public class VamsasAppDatastore
                 if (vbound.getV_parent() != null
                         && dataset != vbound.getV_parent())
                 {
-                  throw new Error(MessageManager.getString(
-                          "error.implementation_error_cannot_map_alignment_sequences"));
-                  // This occurs because the dataset for the alignment we are
-                  // trying to
+                  throw new Error(
+                          "IMPLEMENTATION ERROR: Cannot map an alignment of sequences from different datasets into a single alignment in the vamsas document.");
                 }
               }
             }
index b4580f0..0fed7d7 100644 (file)
@@ -305,8 +305,8 @@ public abstract class Rangetype extends DatastoreItem
   {
     if (ml == null)
     {
-      throw new Error(MessageManager
-              .getString("error.implementation_error_maplist_is_null"));
+      throw new Error(
+              "Implementation error. MapList is null for initMapType.");
     }
     maprange.setLocal(new Local());
     maprange.setMapped(new Mapped());
index 168f1c6..f4ffc0c 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io.vcf;
 
 import jalview.analysis.Dna;
@@ -892,7 +912,7 @@ public class VCFLoader
          * RuntimeException throwable by htsjdk
          */
         String msg = String.format("Error reading VCF for %s:%d-%d: %s ",
-                map.chromosome, vcfStart, vcfEnd);
+                map.chromosome, vcfStart, vcfEnd,e.getLocalizedMessage());
         Cache.log.error(msg);
       }
     }
index 6071933..8d83e75 100644 (file)
@@ -30,7 +30,6 @@ import jalview.ext.jmol.JmolCommands;
 import jalview.structure.AtomSpec;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.HttpUtils;
 
@@ -220,21 +219,22 @@ public class MouseOverStructureListener extends JSFunctionExec
 
       // Form a colour command from the given alignment panel for each distinct
       // structure
-      ArrayList<String[]> ccomands = new ArrayList<String[]>();
-      ArrayList<String> pdbfn = new ArrayList<String>();
-      StructureMappingcommandSet[] colcommands = JmolCommands
-              .getColourBySequenceCommand(ssm, modelSet, sequence, sr,
+      ArrayList<String[]> ccomands = new ArrayList<>();
+      ArrayList<String> pdbfn = new ArrayList<>();
+      String[] colcommands = new JmolCommands()
+              .colourBySequence(ssm, modelSet, sequence, sr,
                       (AlignmentViewPanel) source);
       if (colcommands == null)
       {
         return;
       }
       int sz = 0;
-      for (jalview.structure.StructureMappingcommandSet ccset : colcommands)
+      // for (jalview.structure.StructureMappingcommandSet ccset : colcommands)
+      for (String command : colcommands)
       {
-        sz += ccset.commands.length;
-        ccomands.add(ccset.commands);
-        pdbfn.add(ccset.mapping);
+        // sz += ccset.commands.length;
+        // ccomands.add(command); // ccset.commands);
+        // pdbfn.add(ccset.mapping);
       }
 
       String mclass, mhandle;
index 6de3888..ae6727a 100755 (executable)
  */
 package jalview.jbgui;
 
-import jalview.bin.Cache;
-import jalview.fts.core.FTSDataColumnPreferences;
-import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
-import jalview.fts.service.pdb.PDBFTSRestClient;
-import jalview.gui.Desktop;
-import jalview.gui.JalviewBooleanRadioButtons;
-import jalview.gui.JvOptionPane;
-import jalview.gui.JvSwingUtils;
-import jalview.gui.StructureViewer.ViewerType;
-import jalview.io.BackupFilenameParts;
-import jalview.io.BackupFiles;
-import jalview.io.BackupFilesPresetEntry;
-import jalview.io.IntKeyStringValueEntry;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Component;
@@ -87,6 +71,22 @@ import javax.swing.event.ChangeListener;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
 
+import jalview.bin.Cache;
+import jalview.fts.core.FTSDataColumnPreferences;
+import jalview.fts.core.FTSDataColumnPreferences.PreferenceSource;
+import jalview.fts.service.pdb.PDBFTSRestClient;
+import jalview.gui.Desktop;
+import jalview.gui.JalviewBooleanRadioButtons;
+import jalview.gui.JvOptionPane;
+import jalview.gui.JvSwingUtils;
+import jalview.gui.StructureViewer.ViewerType;
+import jalview.io.BackupFilenameParts;
+import jalview.io.BackupFiles;
+import jalview.io.BackupFilesPresetEntry;
+import jalview.io.IntKeyStringValueEntry;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+
 /**
  * Base class for the Preferences panel.
  * 
@@ -180,7 +180,9 @@ public class GPreferences extends JPanel
 
   protected JComboBox<String> structViewer = new JComboBox<>();
 
-  protected JTextField chimeraPath = new JTextField();
+  protected JLabel structureViewerPathLabel;
+
+  protected JTextField structureViewerPath = new JTextField();
 
   protected ButtonGroup mappingMethod = new ButtonGroup();
 
@@ -1233,7 +1235,7 @@ public class GPreferences extends JPanel
     structureTab.setBorder(new TitledBorder(
             MessageManager.getString("label.structure_options")));
     structureTab.setLayout(null);
-    final int width = 400;
+    final int width = 420;
     final int height = 22;
     final int lineSpacing = 25;
     int ypos = 15;
@@ -1281,13 +1283,19 @@ public class GPreferences extends JPanel
     viewerLabel.setFont(LABEL_FONT);
     viewerLabel.setHorizontalAlignment(SwingConstants.LEFT);
     viewerLabel.setText(MessageManager.getString("label.structure_viewer"));
-    viewerLabel.setBounds(new Rectangle(10, ypos, 200, height));
+    viewerLabel.setBounds(new Rectangle(10, ypos, 220, height));
     structureTab.add(viewerLabel);
 
+    /*
+     * add all external viewers as options here - check 
+     * when selected whether the program is installed
+     */
     structViewer.setFont(LABEL_FONT);
-    structViewer.setBounds(new Rectangle(160, ypos, 120, height));
+    structViewer.setBounds(new Rectangle(190, ypos, 120, height));
     structViewer.addItem(ViewerType.JMOL.name());
     structViewer.addItem(ViewerType.CHIMERA.name());
+    structViewer.addItem(ViewerType.CHIMERAX.name());
+    structViewer.addItem(ViewerType.PYMOL.name());
     structViewer.addActionListener(new ActionListener()
     {
       @Override
@@ -1300,35 +1308,38 @@ public class GPreferences extends JPanel
     structureTab.add(structViewer);
 
     ypos += lineSpacing;
-    JLabel pathLabel = new JLabel();
-    pathLabel.setFont(new java.awt.Font("SansSerif", 0, 11));
-    pathLabel.setHorizontalAlignment(SwingConstants.LEFT);
-    pathLabel.setText(MessageManager.getString("label.chimera_path"));
-    pathLabel.setBounds(new Rectangle(10, ypos, 140, height));
-    structureTab.add(pathLabel);
-
-    chimeraPath.setFont(LABEL_FONT);
-    chimeraPath.setText("");
+    structureViewerPathLabel = new JLabel();
+    structureViewerPathLabel.setFont(LABEL_FONT);// new Font("SansSerif", 0, 11));
+    structureViewerPathLabel.setHorizontalAlignment(SwingConstants.LEFT);
+    structureViewerPathLabel.setText(MessageManager
+            .formatMessage("label.viewer_path", "Chimera(X)"));
+    structureViewerPathLabel.setBounds(new Rectangle(10, ypos, 170, height));
+    structureViewerPathLabel.setEnabled(false);
+    structureTab.add(structureViewerPathLabel);
+
+    structureViewerPath.setFont(LABEL_FONT);
+    structureViewerPath.setText("");
+    structureViewerPath.setEnabled(false);
     final String tooltip = JvSwingUtils.wrapTooltip(true,
-            MessageManager.getString("label.chimera_path_tip"));
-    chimeraPath.setToolTipText(tooltip);
-    chimeraPath.setBounds(new Rectangle(160, ypos, 300, height));
-    chimeraPath.addMouseListener(new MouseAdapter()
+            MessageManager.getString("label.viewer_path_tip"));
+    structureViewerPath.setToolTipText(tooltip);
+    structureViewerPath.setBounds(new Rectangle(190, ypos, 290, height));
+    structureViewerPath.addMouseListener(new MouseAdapter()
     {
       @Override
       public void mouseClicked(MouseEvent e)
       {
-        if (e.getClickCount() == 2)
+        if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
         {
           String chosen = openFileChooser();
           if (chosen != null)
           {
-            chimeraPath.setText(chosen);
+            structureViewerPath.setText(chosen);
           }
         }
       }
     });
-    structureTab.add(chimeraPath);
+    structureTab.add(structureViewerPath);
 
     ypos += lineSpacing;
     nwMapping.setFont(LABEL_FONT);
@@ -1343,7 +1354,7 @@ public class GPreferences extends JPanel
             MessageManager.getString("label.mapping_method"));
     mmTitledBorder.setTitleFont(LABEL_FONT);
     mappingPanel.setBorder(mmTitledBorder);
-    mappingPanel.setBounds(new Rectangle(10, ypos, 452, 45));
+    mappingPanel.setBounds(new Rectangle(10, ypos, 472, 45));
     // GridLayout mappingLayout = new GridLayout();
     mappingPanel.setLayout(new GridLayout());
     mappingPanel.add(nwMapping);
@@ -1354,7 +1365,7 @@ public class GPreferences extends JPanel
     ypos += lineSpacing;
     FTSDataColumnPreferences docFieldPref = new FTSDataColumnPreferences(
             PreferenceSource.PREFERENCES, PDBFTSRestClient.getInstance());
-    docFieldPref.setBounds(new Rectangle(10, ypos, 450, 120));
+    docFieldPref.setBounds(new Rectangle(10, ypos, 470, 120));
     structureTab.add(docFieldPref);
 
     /*
@@ -1362,8 +1373,8 @@ public class GPreferences extends JPanel
      */
     if (Platform.isJS()) 
     {
-      pathLabel.setVisible(false);
-      chimeraPath.setVisible(false);
+      structureViewerPathLabel.setVisible(false);
+      structureViewerPath.setVisible(false);
       viewerLabel.setVisible(false);
       structViewer.setVisible(false);
     }
index dfee3e2..4e13032 100644 (file)
  */
 package jalview.jbgui;
 
-import jalview.api.structures.JalviewStructureDisplayI;
-import jalview.gui.ColourMenuHelper.ColourChangeListener;
-import jalview.util.ImageMaker.TYPE;
-import jalview.util.MessageManager;
-
 import java.awt.BorderLayout;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
@@ -38,6 +33,11 @@ import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JRadioButtonMenuItem;
 
+import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.gui.ColourMenuHelper.ColourChangeListener;
+import jalview.util.ImageMaker.TYPE;
+import jalview.util.MessageManager;
+
 @SuppressWarnings("serial")
 public abstract class GStructureViewer extends JInternalFrame
         implements JalviewStructureDisplayI, ColourChangeListener
@@ -109,7 +109,7 @@ public abstract class GStructureViewer extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        pdbFile_actionPerformed(actionEvent);
+        pdbFile_actionPerformed();
       }
     });
 
@@ -142,7 +142,7 @@ public abstract class GStructureViewer extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        viewMapping_actionPerformed(actionEvent);
+        viewMapping_actionPerformed();
       }
     });
 
@@ -166,13 +166,12 @@ public abstract class GStructureViewer extends JInternalFrame
     JMenu helpMenu = new JMenu();
     helpMenu.setText(MessageManager.getString("action.help"));
     helpItem = new JMenuItem();
-    helpItem.setText(MessageManager.getString("label.jmol_help"));
     helpItem.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        showHelp_actionPerformed(actionEvent);
+        showHelp_actionPerformed();
       }
     });
     alignStructs = new JMenuItem();
@@ -183,7 +182,7 @@ public abstract class GStructureViewer extends JInternalFrame
       @Override
       public void actionPerformed(ActionEvent actionEvent)
       {
-        alignStructs_actionPerformed(actionEvent);
+        alignStructsWithAllAlignPanels();
       }
     });
 
@@ -215,20 +214,20 @@ public abstract class GStructureViewer extends JInternalFrame
 
   protected void fitToWindow_actionPerformed()
   {
+    getBinding().focusView();
   }
 
   protected void highlightSelection_actionPerformed()
   {
   }
 
-  protected void viewerColour_actionPerformed(ActionEvent actionEvent)
+  protected void viewerColour_actionPerformed()
   {
   }
 
-  protected abstract String alignStructs_actionPerformed(
-          ActionEvent actionEvent);
+  protected abstract String alignStructsWithAllAlignPanels();
 
-  public void pdbFile_actionPerformed(ActionEvent actionEvent)
+  public void pdbFile_actionPerformed()
   {
 
   }
@@ -238,32 +237,32 @@ public abstract class GStructureViewer extends JInternalFrame
 
   }
 
-  public void viewMapping_actionPerformed(ActionEvent actionEvent)
+  public void viewMapping_actionPerformed()
   {
 
   }
 
-  public void seqColour_actionPerformed(ActionEvent actionEvent)
+  public void seqColour_actionPerformed()
   {
 
   }
 
-  public void chainColour_actionPerformed(ActionEvent actionEvent)
+  public void chainColour_actionPerformed()
   {
 
   }
 
-  public void chargeColour_actionPerformed(ActionEvent actionEvent)
+  public void chargeColour_actionPerformed()
   {
 
   }
 
-  public void background_actionPerformed(ActionEvent actionEvent)
+  public void background_actionPerformed()
   {
 
   }
 
-  public void showHelp_actionPerformed(ActionEvent actionEvent)
+  public void showHelp_actionPerformed()
   {
 
   }
index 6340e64..ccd9ab0 100644 (file)
@@ -24,6 +24,55 @@ import static jalview.math.RotatableMatrix.Axis.X;
 import static jalview.math.RotatableMatrix.Axis.Y;
 import static jalview.math.RotatableMatrix.Axis.Z;
 
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Rectangle;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Vector;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+import javax.swing.JInternalFrame;
+import javax.swing.SwingUtilities;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.Marshaller;
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+
 import jalview.analysis.Conservation;
 import jalview.analysis.PCA;
 import jalview.analysis.scoremodels.ScoreModels;
@@ -58,7 +107,6 @@ import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.AppVarna;
-import jalview.gui.ChimeraViewFrame;
 import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
 import jalview.gui.OOMWarning;
@@ -150,55 +198,6 @@ import jalview.xml.binding.jalview.SequenceSet.SequenceSetProperties;
 import jalview.xml.binding.jalview.ThresholdType;
 import jalview.xml.binding.jalview.VAMSAS;
 
-import java.awt.Color;
-import java.awt.Font;
-import java.awt.Rectangle;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.lang.reflect.InvocationTargetException;
-import java.math.BigInteger;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.GregorianCalendar;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.Vector;
-import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
-import java.util.jar.JarOutputStream;
-
-import javax.swing.JInternalFrame;
-import javax.swing.SwingUtilities;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.Marshaller;
-import javax.xml.datatype.DatatypeConfigurationException;
-import javax.xml.datatype.DatatypeFactory;
-import javax.xml.datatype.XMLGregorianCalendar;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamReader;
-
 /**
  * Write out the current jalview desktop state as a Jalview XML stream.
  * 
@@ -1089,25 +1088,28 @@ public class Jalview2XML
             if (frames[f] instanceof StructureViewerBase)
             {
               StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
-              matchedFile = saveStructureState(ap, jds, pdb, entry, viewIds,
+              matchedFile = saveStructureViewer(ap, jds, pdb, entry, viewIds,
                       matchedFile, viewFrame);
               /*
                * Only store each structure viewer's state once in the project
                * jar. First time through only (storeDS==false)
                */
               String viewId = viewFrame.getViewId();
+              String viewerType = viewFrame.getViewerType().toString();
               if (!storeDS && !viewIds.contains(viewId))
               {
                 viewIds.add(viewId);
-                try
+                File viewerState = viewFrame.saveSession();
+                if (viewerState != null)
                 {
-                  String viewerState = viewFrame.getStateInfo();
-                  writeJarEntry(jout, getViewerJarEntryName(viewId),
-                          viewerState.getBytes());
-                } catch (IOException e)
+                  copyFileToJar(jout, viewerState.getPath(),
+                          getViewerJarEntryName(viewId), viewerType);
+                }
+                else
                 {
-                  System.err.println(
-                          "Error saving viewer state: " + e.getMessage());
+                  Cache.log.error("Failed to save viewer state for "
+                          +
+                          viewerType);
                 }
               }
             }
@@ -1129,7 +1131,7 @@ public class Jalview2XML
             if (!pdbfiles.contains(pdbId))
             {
               pdbfiles.add(pdbId);
-              copyFileToJar(jout, matchedFile, pdbId);
+              copyFileToJar(jout, matchedFile, pdbId, pdbId);
             }
           }
 
@@ -1676,7 +1678,7 @@ public class Jalview2XML
       // using save and then load
       try
       {
-       fileName = fileName.replace('\\', '/');
+        fileName = fileName.replace('\\', '/');
         System.out.println("Writing jar entry " + fileName);
         JarEntry entry = new JarEntry(fileName);
         jout.putNextEntry(entry);
@@ -1977,7 +1979,7 @@ public class Jalview2XML
 
                 String varnaStateFile = varna.getStateInfo(model.rna);
                 jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
-                copyFileToJar(jout, varnaStateFile, jarEntryName);
+                copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
                 rnaSessions.put(model, jarEntryName);
               }
               SecondaryStructure ss = new SecondaryStructure();
@@ -2001,59 +2003,48 @@ public class Jalview2XML
    * @param jout
    * @param infilePath
    * @param jarEntryName
+   * @param msg
+   *          additional identifying info to log to the console
    */
   protected void copyFileToJar(JarOutputStream jout, String infilePath,
-          String jarEntryName)
+          String jarEntryName, String msg)
   {
-    DataInputStream dis = null;
-    try
+    try (InputStream is = new FileInputStream(infilePath))
     {
       File file = new File(infilePath);
       if (file.exists() && jout != null)
       {
-        dis = new DataInputStream(new FileInputStream(file));
-        byte[] data = new byte[(int) file.length()];
-        dis.readFully(data);
-        writeJarEntry(jout, jarEntryName, data);
+        System.out.println(
+                "Writing jar entry " + jarEntryName + " (" + msg + ")");
+        jout.putNextEntry(new JarEntry(jarEntryName));
+        copyAll(is, jout);
+        jout.closeEntry();
+        // dis = new DataInputStream(new FileInputStream(file));
+        // byte[] data = new byte[(int) file.length()];
+        // dis.readFully(data);
+        // writeJarEntry(jout, jarEntryName, data);
       }
     } catch (Exception ex)
     {
       ex.printStackTrace();
-    } finally
-    {
-      if (dis != null)
-      {
-        try
-        {
-          dis.close();
-        } catch (IOException e)
-        {
-          // ignore
-        }
-      }
     }
   }
 
   /**
-   * Write the data to a new entry of given name in the output jar file
+   * Copies input to output, in 4K buffers; handles any data (text or binary)
    * 
-   * @param jout
-   * @param jarEntryName
-   * @param data
+   * @param in
+   * @param out
    * @throws IOException
    */
-  protected void writeJarEntry(JarOutputStream jout, String jarEntryName,
-          byte[] data) throws IOException
+  protected void copyAll(InputStream in, OutputStream out)
+          throws IOException
   {
-    if (jout != null)
+    byte[] buffer = new byte[4096];
+    int bytesRead = 0;
+    while ((bytesRead = in.read(buffer)) != -1)
     {
-      jarEntryName = jarEntryName.replace('\\','/');
-      System.out.println("Writing jar entry " + jarEntryName);
-      jout.putNextEntry(new JarEntry(jarEntryName));
-      DataOutputStream dout = new DataOutputStream(jout);
-      dout.write(data, 0, data.length);
-      dout.flush();
-      jout.closeEntry();
+      out.write(buffer, 0, bytesRead);
     }
   }
 
@@ -2070,7 +2061,7 @@ public class Jalview2XML
    * @param viewFrame
    * @return
    */
-  protected String saveStructureState(AlignmentPanel ap, SequenceI jds,
+  protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
           Pdbids pdb, PDBEntry entry, List<String> viewIds,
           String matchedFile, StructureViewerBase viewFrame)
   {
@@ -2124,7 +2115,7 @@ public class Jalview2XML
           final String viewId = viewFrame.getViewId();
           state.setViewId(viewId);
           state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
-          state.setColourwithAlignPanel(viewFrame.isUsedforcolourby(ap));
+          state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
           state.setColourByJmol(viewFrame.isColouredByViewer());
           state.setType(viewFrame.getViewerType().toString());
           // pdb.addStructureState(state);
@@ -3185,8 +3176,6 @@ public class Jalview2XML
   protected String copyJarEntry(jarInputStreamProvider jprovider,
           String jarEntryName, String prefix, String suffixModel)
   {
-    BufferedReader in = null;
-    PrintWriter out = null;
     String suffix = ".tmp";
     if (suffixModel == null)
     {
@@ -3197,33 +3186,24 @@ public class Jalview2XML
     {
       suffix = "." + suffixModel.substring(sfpos + 1);
     }
-    try
-    {
-      JarInputStream jin = jprovider.getJarInputStream();
-      /*
-       * if (jprovider.startsWith("http://")) { jin = new JarInputStream(new
-       * URL(jprovider).openStream()); } else { jin = new JarInputStream(new
-       * FileInputStream(jprovider)); }
-       */
 
+    try (JarInputStream jin = jprovider.getJarInputStream())
+    {
       JarEntry entry = null;
       do
       {
         entry = jin.getNextJarEntry();
       } while (entry != null && !entry.getName().equals(jarEntryName));
+
       if (entry != null)
       {
-        in = new BufferedReader(new InputStreamReader(jin, UTF_8));
+        // in = new BufferedReader(new InputStreamReader(jin, UTF_8));
         File outFile = File.createTempFile(prefix, suffix);
         outFile.deleteOnExit();
-        out = new PrintWriter(new FileOutputStream(outFile));
-        String data;
-
-        while ((data = in.readLine()) != null)
+        try (OutputStream os = new FileOutputStream(outFile))
         {
-          out.println(data);
+          copyAll(jin, os);
         }
-        out.flush();
         String t = outFile.getAbsolutePath();
         return t;
       }
@@ -3234,22 +3214,6 @@ public class Jalview2XML
     } catch (Exception ex)
     {
       ex.printStackTrace();
-    } finally
-    {
-      if (in != null)
-      {
-        try
-        {
-          in.close();
-        } catch (IOException e)
-        {
-          // ignore
-        }
-      }
-      if (out != null)
-      {
-        out.close();
-      }
     }
 
     return null;
@@ -4320,10 +4284,15 @@ public class Jalview2XML
             }
             if (!structureViewers.containsKey(sviewid))
             {
+              String viewerType = structureState.getType();
+              if (viewerType == null) // pre Jalview 2.9
+              {
+                viewerType = ViewerType.JMOL.toString();
+              }
               structureViewers.put(sviewid,
                       new StructureViewerModel(x, y, width, height, false,
                               false, true, structureState.getViewId(),
-                              structureState.getType()));
+                              viewerType));
               // Legacy pre-2.7 conversion JAL-823 :
               // do not assume any view has to be linked for colour by
               // sequence
@@ -4424,246 +4393,17 @@ public class Jalview2XML
       return;
     }
 
-    /*
-     * From 2.9: stateData.type contains JMOL or CHIMERA, data is in jar entry
-     * "viewer_"+stateData.viewId
-     */
-    if (ViewerType.CHIMERA.toString().equals(stateData.getType()))
-    {
-      createChimeraViewer(viewerData, af, jprovider);
-    }
-    else
-    {
-      /*
-       * else Jmol (if pre-2.9, stateData contains JMOL state string)
-       */
-      createJmolViewer(viewerData, af, jprovider);
-    }
-  }
-
-  /**
-   * Create a new Chimera viewer.
-   * 
-   * @param data
-   * @param af
-   * @param jprovider
-   */
-  protected void createChimeraViewer(
-          Entry<String, StructureViewerModel> viewerData, AlignFrame af,
-          jarInputStreamProvider jprovider)
-  {
-    StructureViewerModel data = viewerData.getValue();
-    String chimeraSessionFile = data.getStateData();
-
-    /*
-     * Copy Chimera session from jar entry "viewer_"+viewId to a temporary file
-     * 
-     * NB this is the 'saved' viewId as in the project file XML, _not_ the
-     * 'uniquified' sviewid used to reconstruct the viewer here
-     */
-    String viewerJarEntryName = getViewerJarEntryName(data.getViewId());
-    chimeraSessionFile = copyJarEntry(jprovider, viewerJarEntryName,
-            "chimera", ".py");
-
-    Set<Entry<File, StructureData>> fileData = data.getFileData()
-            .entrySet();
-    List<PDBEntry> pdbs = new ArrayList<>();
-    List<SequenceI[]> allseqs = new ArrayList<>();
-    for (Entry<File, StructureData> pdb : fileData)
-    {
-      String filePath = pdb.getValue().getFilePath();
-      String pdbId = pdb.getValue().getPdbId();
-      // pdbs.add(new PDBEntry(filePath, pdbId));
-      pdbs.add(new PDBEntry(pdbId, null, PDBEntry.Type.PDB, filePath));
-      final List<SequenceI> seqList = pdb.getValue().getSeqList();
-      SequenceI[] seqs = seqList.toArray(new SequenceI[seqList.size()]);
-      allseqs.add(seqs);
-    }
-
-    boolean colourByChimera = data.isColourByViewer();
-    boolean colourBySequence = data.isColourWithAlignPanel();
-
-    // TODO use StructureViewer as a factory here, see JAL-1761
-    final PDBEntry[] pdbArray = pdbs.toArray(new PDBEntry[pdbs.size()]);
-    final SequenceI[][] seqsArray = allseqs
-            .toArray(new SequenceI[allseqs.size()][]);
-    String newViewId = viewerData.getKey();
-
-    ChimeraViewFrame cvf = new ChimeraViewFrame(chimeraSessionFile,
-            af.alignPanel, pdbArray, seqsArray, colourByChimera,
-            colourBySequence, newViewId);
-    cvf.setSize(data.getWidth(), data.getHeight());
-    cvf.setLocation(data.getX(), data.getY());
-  }
-
-  /**
-   * Create a new Jmol window. First parse the Jmol state to translate filenames
-   * loaded into the view, and record the order in which files are shown in the
-   * Jmol view, so we can add the sequence mappings in same order.
-   * 
-   * @param viewerData
-   * @param af
-   * @param jprovider
-   */
-  protected void createJmolViewer(
-          final Entry<String, StructureViewerModel> viewerData,
-          AlignFrame af, jarInputStreamProvider jprovider)
-  {
-    final StructureViewerModel svattrib = viewerData.getValue();
-    String state = svattrib.getStateData();
-
-    /*
-     * Pre-2.9: state element value is the Jmol state string
-     * 
-     * 2.9+: @type is "JMOL", state data is in a Jar file member named "viewer_"
-     * + viewId
-     */
-    if (ViewerType.JMOL.toString().equals(svattrib.getType()))
-    {
-      state = readJarEntry(jprovider,
-              getViewerJarEntryName(svattrib.getViewId()));
-    }
-
-    List<String> pdbfilenames = new ArrayList<>();
-    List<SequenceI[]> seqmaps = new ArrayList<>();
-    List<String> pdbids = new ArrayList<>();
-    StringBuilder newFileLoc = new StringBuilder(64);
-    int cp = 0, ncp, ecp;
-    Map<File, StructureData> oldFiles = svattrib.getFileData();
-    while ((ncp = state.indexOf("load ", cp)) > -1)
-    {
-      do
-      {
-        // look for next filename in load statement
-        newFileLoc.append(state.substring(cp,
-                ncp = (state.indexOf("\"", ncp + 1) + 1)));
-        String oldfilenam = state.substring(ncp,
-                ecp = state.indexOf("\"", ncp));
-        // recover the new mapping data for this old filename
-        // have to normalize filename - since Jmol and jalview do
-        // filename
-        // translation differently.
-        StructureData filedat = oldFiles.get(new File(oldfilenam));
-        if (filedat == null)
-        {
-          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
-          filedat = oldFiles.get(new File(reformatedOldFilename));
-        }
-        newFileLoc.append(Platform.escapeBackslashes(filedat.getFilePath()));
-        pdbfilenames.add(filedat.getFilePath());
-        pdbids.add(filedat.getPdbId());
-        seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-        newFileLoc.append("\"");
-        cp = ecp + 1; // advance beyond last \" and set cursor so we can
-                      // look for next file statement.
-      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
-    }
-    if (cp > 0)
-    {
-      // just append rest of state
-      newFileLoc.append(state.substring(cp));
-    }
-    else
-    {
-      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
-      newFileLoc = new StringBuilder(state);
-      newFileLoc.append("; load append ");
-      for (File id : oldFiles.keySet())
-      {
-        // add this and any other pdb files that should be present in
-        // the viewer
-        StructureData filedat = oldFiles.get(id);
-        newFileLoc.append(filedat.getFilePath());
-        pdbfilenames.add(filedat.getFilePath());
-        pdbids.add(filedat.getPdbId());
-        seqmaps.add(filedat.getSeqList().toArray(new SequenceI[0]));
-        newFileLoc.append(" \"");
-        newFileLoc.append(filedat.getFilePath());
-        newFileLoc.append("\"");
-
-      }
-      newFileLoc.append(";");
-    }
-
-    if (newFileLoc.length() == 0)
-    {
-      return;
-    }
-    int histbug = newFileLoc.indexOf("history = ");
-    if (histbug > -1)
-    {
-      /*
-       * change "history = [true|false];" to "history = [1|0];"
-       */
-      histbug += 10;
-      int diff = histbug == -1 ? -1 : newFileLoc.indexOf(";", histbug);
-      String val = (diff == -1) ? null
-              : newFileLoc.substring(histbug, diff);
-      if (val != null && val.length() >= 4)
-      {
-        if (val.contains("e")) // eh? what can it be?
-        {
-          if (val.trim().equals("true"))
-          {
-            val = "1";
-          }
-          else
-          {
-            val = "0";
-          }
-          newFileLoc.replace(histbug, diff, val);
-        }
-      }
-    }
-
-    final String[] pdbf = pdbfilenames
-            .toArray(new String[pdbfilenames.size()]);
-    final String[] id = pdbids.toArray(new String[pdbids.size()]);
-    final SequenceI[][] sq = seqmaps
-            .toArray(new SequenceI[seqmaps.size()][]);
-    final String fileloc = newFileLoc.toString();
-    final String sviewid = viewerData.getKey();
-    final AlignFrame alf = af;
-    final Rectangle rect = new Rectangle(svattrib.getX(), svattrib.getY(),
-            svattrib.getWidth(), svattrib.getHeight());
+    String type = stateData.getType();
     try
     {
-      javax.swing.SwingUtilities.invokeAndWait(new Runnable()
-      {
-        @Override
-        public void run()
-        {
-          JalviewStructureDisplayI sview = null;
-          try
-          {
-            sview = new StructureViewer(
-                    alf.alignPanel.getStructureSelectionManager())
-                            .createView(StructureViewer.ViewerType.JMOL,
-                                    pdbf, id, sq, alf.alignPanel, svattrib,
-                                    fileloc, rect, sviewid);
-            addNewStructureViewer(sview);
-          } catch (OutOfMemoryError ex)
-          {
-            new OOMWarning("restoring structure view for PDB id " + id,
-                    (OutOfMemoryError) ex.getCause());
-            if (sview != null && sview.isVisible())
-            {
-              sview.closeViewer(false);
-              sview.setVisible(false);
-              sview.dispose();
-            }
-          }
-        }
-      });
-    } catch (InvocationTargetException ex)
-    {
-      warn("Unexpected error when opening Jmol view.", ex);
-
-    } catch (InterruptedException e)
+      ViewerType viewerType = ViewerType.valueOf(type);
+      createStructureViewer(viewerType, viewerData, af, jprovider);
+    } catch (IllegalArgumentException | NullPointerException e)
     {
-      // e.printStackTrace();
+      // TODO JAL-3619 show error dialog / offer an alternative viewer
+      Cache.log.error(
+              "Invalid structure viewer type: " + type);
     }
-
   }
 
   /**
@@ -5520,7 +5260,7 @@ public class Jalview2XML
         addDatasetRef(vamsasSet.getDatasetId(), ds);
       }
     }
-    Vector dseqs = null;
+    Vector<SequenceI> dseqs = null;
     if (!ignoreUnrefed)
     {
       // recovering an alignment View
@@ -5548,7 +5288,7 @@ public class Jalview2XML
       // try even harder to restore dataset
       AlignmentI xtantDS = checkIfHasDataset(vamsasSet.getSequence());
       // create a list of new dataset sequences
-      dseqs = new Vector();
+      dseqs = new Vector<>();
     }
     for (int i = 0, iSize = vamsasSet.getSequence().size(); i < iSize; i++)
     {
@@ -5636,7 +5376,8 @@ public class Jalview2XML
    *          vamsasSeq array ordering, to preserve ordering of dataset
    */
   private void ensureJalviewDatasetSequence(Sequence vamsasSeq,
-          AlignmentI ds, Vector dseqs, boolean ignoreUnrefed, int vseqpos)
+          AlignmentI ds, Vector<SequenceI> dseqs, boolean ignoreUnrefed,
+          int vseqpos)
   {
     // JBP TODO: Check this is called for AlCodonFrames to support recovery of
     // xRef Codon Maps
@@ -6360,6 +6101,177 @@ public class Jalview2XML
   }
 
   /**
+   * Creates a new structure viewer window
+   * 
+   * @param viewerType
+   * @param viewerData
+   * @param af
+   * @param jprovider
+   */
+  protected void createStructureViewer(
+          ViewerType viewerType, final Entry<String, StructureViewerModel> viewerData,
+          AlignFrame af, jarInputStreamProvider jprovider)
+  {
+    final StructureViewerModel viewerModel = viewerData.getValue();
+    String sessionFilePath = null;
+
+    if (viewerType == ViewerType.JMOL)
+    {
+      sessionFilePath = rewriteJmolSession(viewerModel, jprovider);
+    }
+    else
+    {
+      String viewerJarEntryName = getViewerJarEntryName(
+              viewerModel.getViewId());
+      sessionFilePath = copyJarEntry(jprovider,
+              viewerJarEntryName,
+              "viewerSession", ".tmp");
+    }
+    final String sessionPath = sessionFilePath;
+    final String sviewid = viewerData.getKey();
+    try
+    {
+      SwingUtilities.invokeAndWait(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+          JalviewStructureDisplayI sview = null;
+          try
+          {
+            sview = StructureViewer.createView(viewerType, af.alignPanel,
+                    viewerModel, sessionPath, sviewid);
+            addNewStructureViewer(sview);
+          } catch (OutOfMemoryError ex)
+          {
+            new OOMWarning("Restoring structure view for "
+                    + viewerType,
+                    (OutOfMemoryError) ex.getCause());
+            if (sview != null && sview.isVisible())
+            {
+              sview.closeViewer(false);
+              sview.setVisible(false);
+              sview.dispose();
+            }
+          }
+        }
+      });
+    } catch (InvocationTargetException | InterruptedException ex)
+    {
+      warn("Unexpected error when opening " + viewerType
+              + " structure viewer", ex);
+    }
+  }
+
+  /**
+   * Rewrites a Jmol session script, saves it to a temporary file, and returns
+   * the path of the file. "load file" commands are rewritten to change the
+   * original PDB file names to those created as the Jalview project is loaded.
+   * 
+   * @param svattrib
+   * @param jprovider
+   * @return
+   */
+  private String rewriteJmolSession(StructureViewerModel svattrib,
+          jarInputStreamProvider jprovider)
+  {
+    String state = svattrib.getStateData(); // Jalview < 2.9
+    if (state == null || state.isEmpty()) // Jalview >= 2.9
+    {
+      String jarEntryName = getViewerJarEntryName(svattrib.getViewId());
+      state = readJarEntry(jprovider, jarEntryName);
+    }
+    // TODO or simpler? for each key in oldFiles,
+    // replace key.getPath() in state with oldFiles.get(key).getFilePath()
+    // (allowing for different path escapings)
+    StringBuilder rewritten = new StringBuilder(state.length());
+    int cp = 0, ncp, ecp;
+    Map<File, StructureData> oldFiles = svattrib.getFileData();
+    while ((ncp = state.indexOf("load ", cp)) > -1)
+    {
+      do
+      {
+        // look for next filename in load statement
+        rewritten.append(state.substring(cp,
+                ncp = (state.indexOf("\"", ncp + 1) + 1)));
+        String oldfilenam = state.substring(ncp,
+                ecp = state.indexOf("\"", ncp));
+        // recover the new mapping data for this old filename
+        // have to normalize filename - since Jmol and jalview do
+        // filename translation differently.
+        StructureData filedat = oldFiles.get(new File(oldfilenam));
+        if (filedat == null)
+        {
+          String reformatedOldFilename = oldfilenam.replaceAll("/", "\\\\");
+          filedat = oldFiles.get(new File(reformatedOldFilename));
+        }
+        rewritten
+                .append(Platform.escapeBackslashes(filedat.getFilePath()));
+        rewritten.append("\"");
+        cp = ecp + 1; // advance beyond last \" and set cursor so we can
+                      // look for next file statement.
+      } while ((ncp = state.indexOf("/*file*/", cp)) > -1);
+    }
+    if (cp > 0)
+    {
+      // just append rest of state
+      rewritten.append(state.substring(cp));
+    }
+    else
+    {
+      System.err.print("Ignoring incomplete Jmol state for PDB ids: ");
+      rewritten = new StringBuilder(state);
+      rewritten.append("; load append ");
+      for (File id : oldFiles.keySet())
+      {
+        // add pdb files that should be present in the viewer
+        StructureData filedat = oldFiles.get(id);
+        rewritten.append(" \"").append(filedat.getFilePath()).append("\"");
+      }
+      rewritten.append(";");
+    }
+
+    if (rewritten.length() == 0)
+    {
+      return null;
+    }
+    final String history = "history = ";
+    int historyIndex = rewritten.indexOf(history);
+    if (historyIndex > -1)
+    {
+      /*
+       * change "history = [true|false];" to "history = [1|0];"
+       */
+      historyIndex += history.length();
+      String val = rewritten.substring(historyIndex, historyIndex + 5);
+      if (val.startsWith("true"))
+      {
+        rewritten.replace(historyIndex, historyIndex + 4, "1");
+      }
+      else if (val.startsWith("false"))
+      {
+        rewritten.replace(historyIndex, historyIndex + 5, "0");
+      }
+    }
+
+    try
+    {
+      File tmp = File.createTempFile("viewerSession", ".tmp");
+      try (OutputStream os = new FileOutputStream(tmp))
+      {
+        InputStream is = new ByteArrayInputStream(
+                rewritten.toString().getBytes());
+        copyAll(is, os);
+        return tmp.getAbsolutePath();
+      }
+    } catch (IOException e)
+    {
+      Cache.log.error("Error restoring Jmol session: " + e.toString());
+    }
+    return null;
+  }
+
+  /**
    * Populates an XML model of the feature colour scheme for one feature type
    * 
    * @param featureType
index 1fccfa2..aa3328b 100644 (file)
@@ -112,6 +112,10 @@ public class FeatureRenderer extends FeatureRendererModel
         continue;
       }
 
+      /*
+       * JAL-3045 text is always drawn over features, even if
+       * 'Show Text' is unchecked in the format menu
+       */
       g.setColor(Color.white);
       int charOffset = (charWidth - fm.charWidth(s)) / 2;
       g.drawString(String.valueOf(s),
index f20cd31..f404d35 100644 (file)
@@ -43,52 +43,64 @@ public class AtomSpec
    * Parses a Chimera atomspec e.g. #1:12.A to construct an AtomSpec model (with
    * null pdb file name)
    * 
+   * <pre>
+   * Chimera format: 
+   *    #1.2:12-20.A     model 1, submodel 2, chain A, atoms 12-20
+   * </pre>
+   * 
    * @param spec
    * @return
    * @throw IllegalArgumentException if the spec cannot be parsed, or represents
    *        more than one residue
+   * @see https://www.cgl.ucsf.edu/chimera/current/docs/UsersGuide/midas/frameatom_spec.html
    */
   public static AtomSpec fromChimeraAtomspec(String spec)
   {
-    int colonPos = spec.indexOf(":");
-    if (colonPos == -1)
+    int modelSeparatorPos = spec.indexOf(":");
+    if (modelSeparatorPos == -1)
     {
       throw new IllegalArgumentException(spec);
     }
 
     int hashPos = spec.indexOf("#");
-    if (hashPos == -1 && colonPos != 0)
+    if (hashPos == -1 && modelSeparatorPos != 0)
     {
       // # is missing but something precedes : - reject
       throw new IllegalArgumentException(spec);
     }
 
-    String modelSubmodel = spec.substring(hashPos + 1, colonPos);
-    int dotPos = modelSubmodel.indexOf(".");
+    String modelSubmodel = spec.substring(hashPos + 1, modelSeparatorPos);
     int modelId = 0;
     try
     {
-      modelId = Integer.valueOf(dotPos == -1 ? modelSubmodel
-              : modelSubmodel.substring(0, dotPos));
+      int subModelPos = modelSubmodel.indexOf(".");
+      modelId = Integer.valueOf(
+              subModelPos > 0 ? modelSubmodel.substring(0, subModelPos)
+                      : modelSubmodel);
     } catch (NumberFormatException e)
     {
       // ignore, default to model 0
     }
 
-    String residueChain = spec.substring(colonPos + 1);
-    dotPos = residueChain.indexOf(".");
+    /*
+     * now process what follows the model, either
+     * Chimera:  atoms.chain
+     * ChimeraX: chain:atoms
+     */
+    String atomsAndChain = spec.substring(modelSeparatorPos + 1);
+    String[] tokens = atomsAndChain.split("\\.");
+    String atoms = tokens.length == 1 ? atomsAndChain : (tokens[0]);
     int resNum = 0;
     try
     {
-      resNum = Integer.parseInt(dotPos == -1 ? residueChain
-              : residueChain.substring(0, dotPos));
+      resNum = Integer.parseInt(atoms);
     } catch (NumberFormatException e)
     {
       // could be a range e.g. #1:4-7.B
       throw new IllegalArgumentException(spec);
     }
 
-    String chainId = dotPos == -1 ? "" : residueChain.substring(dotPos + 1);
+    String chainId = tokens.length == 1 ? "" : (tokens[1]);
 
     return new AtomSpec(modelId, chainId, resNum, 0);
   }
@@ -161,4 +173,70 @@ public class AtomSpec
     return "pdbFile: " + pdbFile + ", chain: " + chain + ", res: "
             + pdbResNum + ", atom: " + atomIndex;
   }
+
+  /**
+   * Parses a ChimeraX atomspec to construct an AtomSpec model (with
+   * null pdb file name)
+   * 
+   * <pre>
+   * ChimeraX format:
+   *    #1.2/A:12-20     model 1, submodel 2, chain A, atoms 12-20
+   * </pre>
+   * 
+   * @param spec
+   * @return
+   * @throw IllegalArgumentException if the spec cannot be parsed, or represents
+   *        more than one residue
+   * @see http://rbvi.ucsf.edu/chimerax/docs/user/commands/atomspec.html
+   */
+  public static AtomSpec fromChimeraXAtomspec(String spec)
+  {
+    int modelSeparatorPos = spec.indexOf("/");
+    if (modelSeparatorPos == -1)
+    {
+      throw new IllegalArgumentException(spec);
+    }
+
+    int hashPos = spec.indexOf("#");
+    if (hashPos == -1 && modelSeparatorPos != 0)
+    {
+      // # is missing but something precedes : - reject
+      throw new IllegalArgumentException(spec);
+    }
+
+    String modelSubmodel = spec.substring(hashPos + 1, modelSeparatorPos);
+    int modelId = 0;
+    try
+    {
+      int subModelPos = modelSubmodel.indexOf(".");
+      modelId = Integer.valueOf(
+              subModelPos > 0 ? modelSubmodel.substring(0, subModelPos)
+                      : modelSubmodel);
+    } catch (NumberFormatException e)
+    {
+      // ignore, default to model 0
+    }
+
+    /*
+     * now process what follows the model, either
+     * Chimera:  atoms.chain
+     * ChimeraX: chain:atoms
+     */
+    String atomsAndChain = spec.substring(modelSeparatorPos + 1);
+    String[] tokens = atomsAndChain.split("\\:");
+    String atoms = tokens.length == 1 ? atomsAndChain : (tokens[1]);
+    int resNum = 0;
+    try
+    {
+      resNum = Integer.parseInt(atoms);
+    } catch (NumberFormatException e)
+    {
+      // could be a range e.g. #1:4-7.B
+      throw new IllegalArgumentException(spec);
+    }
+
+    String chainId = tokens.length == 1 ? "" : (tokens[0]);
+
+    return new AtomSpec(modelId, chainId, resNum, 0);
+  }
 }
diff --git a/src/jalview/structure/AtomSpecModel.java b/src/jalview/structure/AtomSpecModel.java
new file mode 100644 (file)
index 0000000..1ef653e
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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.structure;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * A class to model a set of models, chains and atom range positions
+ * 
+ */
+public class AtomSpecModel
+{
+  /*
+   * { modelId, {chainCode, List<from-to> ranges} }
+   */
+  private Map<String, Map<String, BitSet>> atomSpec;
+
+  /**
+   * Constructor
+   */
+  public AtomSpecModel()
+  {
+    atomSpec = new TreeMap<>();
+  }
+
+  /**
+   * Adds one contiguous range to this atom spec
+   * 
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public void addRange(String model, int startPos, int endPos, String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour and model
+     */
+    Map<String, BitSet> modelData = atomSpec.get(model);
+    if (modelData == null)
+    {
+      atomSpec.put(model, modelData = new TreeMap<>());
+    }
+
+    /*
+     * Get/initialize map of data for colour, model and chain
+     */
+    BitSet chainData = modelData.get(chain);
+    if (chainData == null)
+    {
+      chainData = new BitSet();
+      modelData.put(chain, chainData);
+    }
+
+    /*
+     * Add the start/end positions
+     */
+    chainData.set(startPos, endPos + 1);
+  }
+
+  public Iterable<String> getModels()
+  {
+    return atomSpec.keySet();
+  }
+
+  public int getModelCount()
+  {
+    return atomSpec.size();
+  }
+
+  public Iterable<String> getChains(String model)
+  {
+    return atomSpec.containsKey(model) ? atomSpec.get(model).keySet()
+            : null;
+  }
+
+  /**
+   * Returns a (possibly empty) ordered list of contiguous atom ranges for the
+   * given model and chain.
+   * 
+   * @param model
+   * @param chain
+   * @return
+   */
+  public List<int[]> getRanges(String model, String chain)
+  {
+    List<int[]> ranges = new ArrayList<>();
+    if (atomSpec.containsKey(model))
+    {
+      BitSet bs = atomSpec.get(model).get(chain);
+      int start = 0;
+      if (bs != null)
+      {
+        start = bs.nextSetBit(start);
+        int end = 0;
+        while (start != -1)
+        {
+          end = bs.nextClearBit(start);
+          ranges.add(new int[] { start, end - 1 });
+          start = bs.nextSetBit(end);
+        }
+      }
+    }
+    return ranges;
+  }
+}
diff --git a/src/jalview/structure/StructureCommand.java b/src/jalview/structure/StructureCommand.java
new file mode 100644 (file)
index 0000000..0a7000b
--- /dev/null
@@ -0,0 +1,126 @@
+package jalview.structure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StructureCommand implements StructureCommandI
+{
+  private String command;
+
+  private List<String> parameters;
+
+  public StructureCommand(String cmd, String... params)
+  {
+    command = cmd;
+    if (params != null)
+    {
+      for (String p : params)
+      {
+        addParameter(p);
+      }
+    }
+  }
+
+  @Override
+  public void addParameter(String param)
+  {
+    if (parameters == null)
+    {
+      parameters = new ArrayList<>();
+    }
+    parameters.add(param);
+  }
+
+  @Override
+  public String getCommand()
+  {
+    return command;
+  }
+
+  @Override
+  public List<String> getParameters()
+  {
+    return parameters;
+  }
+
+  @Override
+  public boolean hasParameters()
+  {
+    return parameters != null && !parameters.isEmpty();
+  }
+
+  @Override
+  public String toString()
+  {
+    if (!hasParameters()) 
+    {
+      return command;
+    }
+    StringBuilder sb = new StringBuilder(32);
+    sb.append(command).append("(");
+    boolean first = true;
+    for (String p : parameters)
+    {
+      if (!first)
+      {
+        sb.append(",");
+      }
+      first = false;
+      sb.append(p);
+    }
+    sb.append(")");
+    return sb.toString();
+  }
+
+  @Override
+  public int hashCode()
+  {
+    int h = command.hashCode();
+    if (parameters != null)
+    {
+      for (String p : parameters)
+      {
+        h = h * 37 + p.hashCode();
+      }
+    }
+    return h;
+  }
+
+  /**
+   * Answers true if {@code obj} is a {@code StructureCommand} with the same
+   * command and parameters as this one, else false
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj == null || !(obj instanceof StructureCommand))
+    {
+      return false;
+    }
+    StructureCommand sc = (StructureCommand) obj;
+
+    if (!command.equals(sc.command))
+    {
+      return false;
+    }
+    if (parameters == null || sc.parameters == null)
+    {
+      return (parameters == null) && (sc.parameters == null);
+    }
+
+    int j = parameters.size();
+    if (j != sc.parameters.size())
+    {
+      return false;
+    }
+    for (int i = 0; i < j; i++)
+    {
+      if (!parameters.get(i).equals(sc.parameters.get(i)))
+      {
+        return false;
+      }
+    }
+    return true;
+  }
+
+}
diff --git a/src/jalview/structure/StructureCommandI.java b/src/jalview/structure/StructureCommandI.java
new file mode 100644 (file)
index 0000000..e39bbba
--- /dev/null
@@ -0,0 +1,14 @@
+package jalview.structure;
+
+import java.util.List;
+
+public interface StructureCommandI
+{
+  String getCommand();
+
+  List<String> getParameters();
+
+  void addParameter(String param);
+
+  boolean hasParameters();
+}
diff --git a/src/jalview/structure/StructureCommandsBase.java b/src/jalview/structure/StructureCommandsBase.java
new file mode 100644 (file)
index 0000000..57544b7
--- /dev/null
@@ -0,0 +1,257 @@
+package jalview.structure;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A base class holding methods useful to all classes that implement commands
+ * for structure viewers
+ * 
+ * @author gmcarstairs
+ *
+ */
+public abstract class StructureCommandsBase implements StructureCommandsI
+{
+  public static final String NAMESPACE_PREFIX = "jv_";
+
+  private static final String CMD_SEPARATOR = ";";
+
+  /**
+   * Returns something that separates concatenated commands
+   * 
+   * @return
+   */
+  protected static String getCommandSeparator()
+  {
+    return CMD_SEPARATOR;
+  }
+
+  /**
+   * Returns the lowest model number used by the structure viewer
+   * 
+   * @return
+   */
+  @Override
+  public int getModelStartNo()
+  {
+    return 0;
+  }
+
+  /**
+   * Helper method to add one contiguous range to the AtomSpec model for the given
+   * value (creating the model if necessary). As used by Jalview, {@code value} is
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
+   * 
+   * @param map
+   * @param value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
+          Object value, String model, int startPos, int endPos,
+          String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour
+     */
+    AtomSpecModel atomSpec = map.get(value);
+    if (atomSpec == null)
+    {
+      atomSpec = new AtomSpecModel();
+      map.put(value, atomSpec);
+    }
+  
+    atomSpec.addRange(model, startPos, endPos, chain);
+  }
+
+  /**
+   * Makes a structure viewer attribute name for a Jalview feature type by
+   * prefixing it with "jv_", and replacing any non-alphanumeric characters with
+   * an underscore
+   * 
+   * @param featureType
+   * @return
+   */
+  protected String makeAttributeName(String featureType)
+  {
+    StringBuilder sb = new StringBuilder();
+    if (featureType != null)
+    {
+      for (char c : featureType.toCharArray())
+      {
+        sb.append(Character.isLetterOrDigit(c) ? c : '_');
+      }
+    }
+    String attName = NAMESPACE_PREFIX + sb.toString();
+    return attName;
+  }
+
+  /**
+   * Traverse the map of colours/models/chains/positions to construct a list of
+   * 'color' commands (one per distinct colour used). The format of each command
+   * is specific to the structure viewer.
+   * <p>
+   * The default implementation returns a single command containing one command
+   * per colour, concatenated.
+   * 
+   * @param colourMap
+   * @return
+   */
+  @Override
+  public List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap)
+  {
+    List<StructureCommandI> commands = new ArrayList<>();
+    StringBuilder sb = new StringBuilder(colourMap.size() * 20);
+    boolean first = true;
+    for (Object key : colourMap.keySet())
+    {
+      Color colour = (Color) key;
+      final AtomSpecModel colourData = colourMap.get(colour);
+      StructureCommandI command = getColourCommand(colourData, colour);
+      if (!first)
+      {
+        sb.append(getCommandSeparator());
+      }
+      first = false;
+      sb.append(command.getCommand());
+    }
+
+    commands.add(new StructureCommand(sb.toString()));
+    return commands;
+  }
+
+  /**
+   * Returns a command to colour the atoms represented by {@code atomSpecModel}
+   * with the colour specified by {@code colourCode}.
+   * 
+   * @param atomSpecModel
+   * @param colour
+   * @return
+   */
+  protected StructureCommandI getColourCommand(AtomSpecModel atomSpecModel,
+          Color colour)
+  {
+    String atomSpec = getAtomSpec(atomSpecModel, false);
+    return colourResidues(atomSpec, colour);
+  }
+
+  /**
+   * Returns a command to colour the atoms described (in viewer command syntax)
+   * by {@code atomSpec} with the colour specified by {@code colourCode}
+   * 
+   * @param atomSpec
+   * @param colour
+   * @return
+   */
+  protected abstract StructureCommandI colourResidues(String atomSpec,
+          Color colour);
+
+  @Override
+  public List<StructureCommandI> colourByResidues(
+          Map<String, Color> colours)
+  {
+    List<StructureCommandI> commands = new ArrayList<>();
+    for (Entry<String, Color> entry : colours.entrySet())
+    {
+      commands.add(colourResidue(entry.getKey(), entry.getValue()));
+    }
+    return commands;
+  }
+
+  private StructureCommandI colourResidue(String resName, Color col)
+  {
+    String atomSpec = getResidueSpec(resName);
+    return colourResidues(atomSpec, col);
+  }
+
+  /**
+   * Helper method to append one start-end range to an atomspec string
+   * 
+   * @param sb
+   * @param start
+   * @param end
+   * @param chain
+   * @param firstPositionForModel
+   */
+  protected void appendRange(StringBuilder sb, int start, int end,
+          String chain, boolean firstPositionForModel, boolean isChimeraX)
+  {
+    if (!firstPositionForModel)
+    {
+      sb.append(",");
+    }
+    if (end == start)
+    {
+      sb.append(start);
+    }
+    else
+    {
+      sb.append(start).append("-").append(end);
+    }
+
+    if (!isChimeraX)
+    {
+      sb.append(".");
+      if (!" ".equals(chain))
+      {
+        sb.append(chain);
+      }
+    }
+  }
+
+  /**
+   * Returns the atom specifier meaning all occurrences of the given residue
+   * 
+   * @param residue
+   * @return
+   */
+  protected abstract String getResidueSpec(String residue);
+
+  @Override
+  public List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureValues)
+  {
+    // default does nothing, override where this is implemented
+    return null;
+  }
+
+  @Override
+  public List<StructureCommandI> startNotifications(String uri)
+  {
+    return null;
+  }
+
+  @Override
+  public List<StructureCommandI> stopNotifications()
+  {
+    return null;
+  }
+
+  @Override
+  public StructureCommandI getSelectedResidues()
+  {
+    return null;
+  }
+
+  @Override
+  public StructureCommandI listResidueAttributes()
+  {
+    return null;
+  }
+
+  @Override
+  public StructureCommandI getResidueAttributes(String attName)
+  {
+    return null;
+  }
+}
diff --git a/src/jalview/structure/StructureCommandsI.java b/src/jalview/structure/StructureCommandsI.java
new file mode 100644 (file)
index 0000000..91e0494
--- /dev/null
@@ -0,0 +1,219 @@
+package jalview.structure;
+
+import java.awt.Color;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Methods that generate commands that can be sent to a molecular structure
+ * viewer program (e.g. Jmol, Chimera, ChimeraX)
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface StructureCommandsI
+{
+  /**
+   * Returns the command to colour by chain
+   * 
+   * @return
+   */
+  StructureCommandI colourByChain();
+
+  /**
+   * Returns the command to colour residues using a charge-based scheme:
+   * <ul>
+   * <li>Aspartic acid and Glutamic acid (negative charge) red</li>
+   * <li>Lysine and Arginine (positive charge) blue</li>
+   * <li>Cysteine - yellow</li>
+   * <li>all others - white</li>
+   * </ul>
+   * 
+   * @return
+   */
+  List<StructureCommandI> colourByCharge();
+
+  /**
+   * Returns the command to colour residues with the colours provided in the
+   * map, one per three letter residue code
+   * 
+   * @param colours
+   * @return
+   */
+  List<StructureCommandI> colourByResidues(Map<String, Color> colours);
+
+  /**
+   * Returns the command to set the background colour of the structure viewer
+   * 
+   * @param col
+   * @return
+   */
+  StructureCommandI setBackgroundColour(Color col);
+
+  /**
+   * Returns commands to colour mapped residues of structures according to
+   * Jalview's colouring (including feature colouring if applied). Parameter is
+   * a map from Color to a model of all residues assigned that colour.
+   * 
+   * @param colourMap
+   * @return
+   */
+
+  List<StructureCommandI> colourBySequence(
+          Map<Object, AtomSpecModel> colourMap);
+
+  /**
+   * Returns a command to centre the display in the structure viewer
+   * 
+   * @return
+   */
+  StructureCommandI focusView();
+
+  /**
+   * Returns a command to show only the selected chains. The items in the input
+   * list should be formatted as "modelid:chainid".
+   * 
+   * @param toShow
+   * @return
+   */
+  List<StructureCommandI> showChains(List<String> toShow);
+
+  /**
+   * Returns a command to superpose structures by closest positioning of
+   * residues in {@code atomSpec} to the corresponding residues in
+   * {@code refAtoms}. If wanted, this may include commands to visually
+   * highlight the residues that were used for the superposition.
+   * 
+   * @param refAtoms
+   * @param atomSpec
+   * @return
+   */
+  List<StructureCommandI> superposeStructures(AtomSpecModel refAtoms,
+          AtomSpecModel atomSpec);
+
+  /**
+   * Returns a command to open a file of commands at the given path
+   * 
+   * @param path
+   * @return
+   */
+  StructureCommandI openCommandFile(String path);
+
+  /**
+   * Returns a command to save the current viewer session state to the given
+   * file
+   * 
+   * @param filepath
+   * @return
+   */
+  StructureCommandI saveSession(String filepath);
+
+  /**
+   * Returns a representation of the atom set represented by the model, in
+   * viewer syntax format. If {@code alphaOnly} is true, this is restricted to
+   * Alpha Carbon (peptide) or Phosphorous (rna) only
+   * 
+   * @param model
+   * @param alphaOnly
+   * @return
+   */
+  String getAtomSpec(AtomSpecModel model, boolean alphaOnly);
+
+  /**
+   * Returns the lowest model number used by the structure viewer (likely 0 or
+   * 1)
+   * 
+   * @return
+   */
+  // TODO remove by refactoring so command generation is purely driven by
+  // AtomSpecModel objects derived in the binding classes?
+  int getModelStartNo();
+
+  /**
+   * Returns command(s) to show only the backbone of the peptide (cartoons in
+   * Jmol, chain in Chimera)
+   * 
+   * @return
+   */
+  List<StructureCommandI> showBackbone();
+
+  /**
+   * Returns a command to open a file at the given path
+   * 
+   * @param file
+   * @return
+   */
+  // refactor if needed to distinguish loading data or session files
+  StructureCommandI loadFile(String file);
+
+  /**
+   * Returns commands to set atom attributes or properties, given a map of
+   * Jalview features as {featureType, {featureValue, AtomSpecModel}}. The
+   * assumption is that one command can be constructed for each feature type and
+   * value combination, to apply it to one or more residues.
+   * 
+   * @param featureValues
+   * @return
+   */
+  List<StructureCommandI> setAttributes(
+          Map<String, Map<Object, AtomSpecModel>> featureValues);
+
+  /**
+   * Returns command to open a saved structure viewer session file, or null if
+   * not supported
+   * 
+   * @param filepath
+   * @return
+   */
+  StructureCommandI openSession(String filepath);
+
+  /**
+   * Returns a command to ask the viewer to close down
+   * 
+   * @return
+   */
+  StructureCommandI closeViewer();
+
+  /**
+   * Returns one or more commands to ask the viewer to notify model or selection
+   * changes to the given uri. Returns null if this is not supported by the
+   * structure viewer.
+   * 
+   * @param uri
+   * @return
+   */
+  List<StructureCommandI> startNotifications(String uri);
+
+  /**
+   * Returns one or more commands to ask the viewer to stop notifying model or
+   * selection changes. Returns null if this is not supported by the structure
+   * viewer.
+   * 
+   * @return
+   */
+  List<StructureCommandI> stopNotifications();
+
+  /**
+   * Returns a command to ask the viewer for its current residue selection, or
+   * null if no such command is supported
+   * 
+   * @return
+   */
+  StructureCommandI getSelectedResidues();
+
+  /**
+   * Returns a command to list the unique names of residue attributes, or null
+   * if no such command is supported
+   * 
+   * @return
+   */
+  StructureCommandI listResidueAttributes();
+
+  /**
+   * Returns a command to list residues with an attribute of the given name,
+   * with attribute value, or null if no such command is supported
+   * 
+   * @return
+   */
+  StructureCommandI getResidueAttributes(String attName);
+}
index 798b07a..53644e9 100644 (file)
  */
 package jalview.structure;
 
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
 import jalview.analysis.AlignSeq;
 import jalview.api.StructureSelectionManagerProvider;
+import jalview.bin.Cache;
 import jalview.commands.CommandI;
 import jalview.commands.EditCommand;
 import jalview.commands.OrderCommand;
@@ -45,18 +57,6 @@ import jalview.util.Platform;
 import jalview.ws.sifts.SiftsClient;
 import jalview.ws.sifts.SiftsException;
 import jalview.ws.sifts.SiftsSettings;
-
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Vector;
-
 import mc_view.Atom;
 import mc_view.PDBChain;
 import mc_view.PDBfile;
@@ -430,7 +430,8 @@ public class StructureSelectionManager
     } catch (SiftsException e)
     {
       isMapUsingSIFTs = false;
-      e.printStackTrace();
+      Cache.log.error("SIFTS mapping failed", e);
+      Cache.log.error("Falling back on Needleman & Wunsch alignment");
       siftsClient = null;
     }
 
@@ -537,15 +538,14 @@ public class StructureSelectionManager
                     pdb, maxChain, sqmpping, maxAlignseq, siftsClient);
             seqToStrucMapping.add(siftsMapping);
             maxChain.makeExactMapping(siftsMapping, seq);
-            maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS");// FIXME: is this
-                                                       // "IEA:SIFTS" ?
+            maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS");
             maxChain.transferResidueAnnotation(siftsMapping, null);
             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
 
           } catch (SiftsException e)
           {
             // fall back to NW alignment
-            System.err.println(e.getMessage());
+            Cache.log.error(e.getMessage());
             StructureMapping nwMapping = getNWMappings(seq, pdbFile,
                     targetChainId, maxChain, pdb, maxAlignseq);
             seqToStrucMapping.add(nwMapping);
index 2528286..16dd96c 100644 (file)
  */
 package jalview.structures.models;
 
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.SwingUtilities;
+
+import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.api.StructureSelectionManagerProvider;
 import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.MappedFeatures;
 import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.ext.rbvi.chimera.JalviewChimeraBinding;
+import jalview.gui.AlignmentPanel;
+import jalview.gui.Desktop;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.io.StructureFile;
+import jalview.renderer.seqfeatures.FeatureColourFinder;
 import jalview.schemes.ColourSchemeI;
+import jalview.schemes.ResidueProperties;
 import jalview.structure.AtomSpec;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureCommandsI;
 import jalview.structure.StructureListener;
 import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 import jalview.util.Comparison;
 import jalview.util.MessageManager;
 
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.List;
-
 /**
  * 
  * A base class to hold common function for protein structure model binding.
@@ -57,10 +78,71 @@ public abstract class AAStructureBindingModel
         extends SequenceStructureBindingModel
         implements StructureListener, StructureSelectionManagerProvider
 {
+  /**
+   * Data bean class to simplify parameterisation in superposeStructures
+   */
+  public static class SuperposeData
+  {
+    public String filename;
+
+    public String pdbId;
+
+    public String chain = "";
+
+    public boolean isRna;
+
+    /*
+     * The pdb residue number (if any) mapped to columns of the alignment
+     */
+    public int[] pdbResNo; // or use SparseIntArray?
+
+    public String modelId;
+
+    /**
+     * Constructor
+     * 
+     * @param width
+     *          width of alignment (number of columns that may potentially
+     *          participate in superposition)
+     * @param model
+     *          structure viewer model number
+     */
+    public SuperposeData(int width, String model)
+    {
+      pdbResNo = new int[width];
+      modelId = model;
+    }
+  }
+
+  private static final int MIN_POS_TO_SUPERPOSE = 4;
+
+  private static final String COLOURING_STRUCTURES = MessageManager
+          .getString("status.colouring_structures");
+
+  /*
+   * the Jalview panel through which the user interacts
+   * with the structure viewer
+   */
+  private JalviewStructureDisplayI viewer;
+
+  /*
+   * helper that generates command syntax
+   */
+  private StructureCommandsI commandGenerator;
 
   private StructureSelectionManager ssm;
 
   /*
+   * modelled chains, formatted as "pdbid:chainCode"
+   */
+  private List<String> chainNames;
+
+  /*
+   * lookup of pdb file name by key "pdbid:chainCode"
+   */
+  private Map<String, String> chainFile;
+
+  /*
    * distinct PDB entries (pdb files) associated
    * with sequences
    */
@@ -88,40 +170,13 @@ public abstract class AAStructureBindingModel
   private boolean finishedInit = false;
 
   /**
-   * current set of model filenames loaded in the Jmol instance
+   * current set of model filenames loaded in the viewer
    */
   protected String[] modelFileNames = null;
 
   public String fileLoadingError;
 
-  /**
-   * Data bean class to simplify parameterisation in superposeStructures
-   */
-  protected class SuperposeData
-  {
-    /**
-     * Constructor with alignment width argument
-     * 
-     * @param width
-     */
-    public SuperposeData(int width)
-    {
-      pdbResNo = new int[width];
-    }
-
-    public String filename;
-
-    public String pdbId;
-
-    public String chain = "";
-
-    public boolean isRna;
-
-    /*
-     * The pdb residue number (if any) mapped to each column of the alignment
-     */
-    public int[] pdbResNo;
-  }
+  protected Thread externalViewerMonitor;
 
   /**
    * Constructor
@@ -134,6 +189,8 @@ public abstract class AAStructureBindingModel
   {
     this.ssm = ssm;
     this.sequence = seqs;
+    chainNames = new ArrayList<>();
+    chainFile = new HashMap<>();
   }
 
   /**
@@ -148,8 +205,7 @@ public abstract class AAStructureBindingModel
           PDBEntry[] pdbentry, SequenceI[][] sequenceIs,
           DataSourceType protocol)
   {
-    this.ssm = ssm;
-    this.sequence = sequenceIs;
+    this(ssm, sequenceIs);
     this.nucleotide = Comparison.isNucleotide(sequenceIs);
     this.pdbEntry = pdbentry;
     this.protocol = protocol;
@@ -207,6 +263,7 @@ public abstract class AAStructureBindingModel
     chains = newchains;
     return chainmaps > 0;
   }
+
   public StructureSelectionManager getSsm()
   {
     return ssm;
@@ -331,7 +388,11 @@ public abstract class AAStructureBindingModel
    */
   protected void releaseUIResources()
   {
+  }
 
+  @Override
+  public void releaseReferences(Object svl)
+  {
   }
 
   public boolean isColourBySequence()
@@ -339,6 +400,25 @@ public abstract class AAStructureBindingModel
     return colourBySequence;
   }
 
+  /**
+   * Called when the binding thinks the UI needs to be refreshed after a
+   * structure viewer state change. This could be because structures were
+   * loaded, or because an error has occurred. Default does nothing, override as
+   * required.
+   */
+  public void refreshGUI()
+  {
+  }
+
+  /**
+   * Instruct the Jalview binding to update the pdbentries vector if necessary
+   * prior to matching the jmol view's contents to the list of structure files
+   * Jalview knows about. By default does nothing, override as required.
+   */
+  public void refreshPdbEntries()
+  {
+  }
+
   public void setColourBySequence(boolean colourBySequence)
   {
     this.colourBySequence = colourBySequence;
@@ -355,8 +435,8 @@ public abstract class AAStructureBindingModel
               { Integer.valueOf(pe).toString() }));
     }
     final String nullChain = "TheNullChain";
-    List<SequenceI> s = new ArrayList<SequenceI>();
-    List<String> c = new ArrayList<String>();
+    List<SequenceI> s = new ArrayList<>();
+    List<String> c = new ArrayList<>();
     if (getChains() == null)
     {
       setChains(new String[getPdbCount()][]);
@@ -425,8 +505,8 @@ public abstract class AAStructureBindingModel
   public synchronized PDBEntry[] addSequenceAndChain(PDBEntry[] pdbe,
           SequenceI[][] seq, String[][] chns)
   {
-    List<PDBEntry> v = new ArrayList<PDBEntry>();
-    List<int[]> rtn = new ArrayList<int[]>();
+    List<PDBEntry> v = new ArrayList<>();
+    List<int[]> rtn = new ArrayList<>();
     for (int i = 0; i < getPdbCount(); i++)
     {
       v.add(getPdbEntry(i));
@@ -581,7 +661,8 @@ public abstract class AAStructureBindingModel
    * @return
    */
   protected int findSuperposableResidues(AlignmentI alignment,
-          BitSet matched, SuperposeData[] structures)
+          BitSet matched,
+          AAStructureBindingModel.SuperposeData[] structures)
   {
     int refStructure = -1;
     String[] files = getStructureFiles();
@@ -639,7 +720,7 @@ public abstract class AAStructureBindingModel
              * for the same structure)
              */
             s = seqCountForPdbFile;
-            break;
+            break; // fixme break out of two loops here!
           }
         }
       }
@@ -732,11 +813,15 @@ public abstract class AAStructureBindingModel
   }
 
   /**
-   * Returns a list of chains mapped in this viewer.
+   * Returns a list of chains mapped in this viewer, formatted as
+   * "pdbid:chainCode"
    * 
    * @return
    */
-  public abstract List<String> getChainNames();
+  public List<String> getChainNames()
+  {
+    return chainNames;
+  }
 
   /**
    * Returns the Jalview panel hosting the structure viewer (if any)
@@ -745,34 +830,134 @@ public abstract class AAStructureBindingModel
    */
   public JalviewStructureDisplayI getViewer()
   {
-    return null;
+    return viewer;
   }
 
-  public abstract void setJalviewColourScheme(ColourSchemeI cs);
+  public void setViewer(JalviewStructureDisplayI v)
+  {
+    viewer = v;
+  }
 
   /**
    * Constructs and sends a command to align structures against a reference
    * structure, based on one or more sequence alignments. May optionally return
-   * an error or warning message for the alignment command.
-   * 
-   * @param alignments
-   *          an array of alignments to process
-   * @param structureIndices
-   *          an array of corresponding reference structures (index into pdb
-   *          file array); if a negative value is passed, the first PDB file
-   *          mapped to an alignment sequence is used as the reference for
-   *          superposition
-   * @param hiddenCols
-   *          an array of corresponding hidden columns for each alignment
+   * an error or warning message for the alignment command(s).
+   * 
+   * @param alignWith
+   *          an array of one or more alignment views to process
    * @return
    */
-  public abstract String superposeStructures(AlignmentI[] alignments,
-          int[] structureIndices, HiddenColumns[] hiddenCols);
+  public String superposeStructures(List<AlignmentViewPanel> alignWith)
+  {
+    String error = "";
+    String[] files = getStructureFiles();
+
+    if (!waitForFileLoad(files))
+    {
+      return null;
+    }
+    refreshPdbEntries();
+
+    for (AlignmentViewPanel view : alignWith)
+    {
+      AlignmentI alignment = view.getAlignment();
+      HiddenColumns hiddenCols = alignment.getHiddenColumns();
+
+      /*
+       * 'matched' bit i will be set for visible alignment columns i where
+       * all sequences have a residue with a mapping to their PDB structure
+       */
+      BitSet matched = new BitSet();
+      final int width = alignment.getWidth();
+      for (int m = 0; m < width; m++)
+      {
+        if (hiddenCols == null || hiddenCols.isVisible(m))
+        {
+          matched.set(m);
+        }
+      }
+
+      AAStructureBindingModel.SuperposeData[] structures = new AAStructureBindingModel.SuperposeData[files.length];
+      for (int f = 0; f < files.length; f++)
+      {
+        structures[f] = new AAStructureBindingModel.SuperposeData(width,
+                getModelIdForFile(files[f]));
+      }
+
+      /*
+       * Calculate the superposable alignment columns ('matched'), and the
+       * corresponding structure residue positions (structures.pdbResNo)
+       */
+      int refStructure = findSuperposableResidues(alignment, matched,
+              structures);
+
+      /*
+       * require at least 4 positions to be able to execute superposition
+       */
+      int nmatched = matched.cardinality();
+      if (nmatched < MIN_POS_TO_SUPERPOSE)
+      {
+        String msg = MessageManager
+                .formatMessage("label.insufficient_residues", nmatched);
+        error += view.getViewName() + ": " + msg + "; ";
+        continue;
+      }
+
+      /*
+       * get a model of the superposable residues in the reference structure 
+       */
+      AtomSpecModel refAtoms = getAtomSpec(structures[refStructure],
+              matched);
+
+      /*
+       * Show all as backbone before doing superposition(s)
+       * (residues used for matching will be shown as ribbon)
+       */
+      // todo better way to ensure synchronous than setting getReply true!!
+      executeCommands(commandGenerator.showBackbone(), true, null);
+
+      /*
+       * superpose each (other) structure to the reference in turn
+       */
+      for (int i = 0; i < structures.length; i++)
+      {
+        if (i != refStructure)
+        {
+          AtomSpecModel atomSpec = getAtomSpec(structures[i], matched);
+          List<StructureCommandI> commands = commandGenerator
+                  .superposeStructures(refAtoms, atomSpec);
+          List<String> replies = executeCommands(commands, true, null);
+          for (String reply : replies)
+          {
+            // return this error (Chimera only) to the user
+            if (reply.toLowerCase().contains("unequal numbers of atoms"))
+            {
+              error += "; " + reply;
+            }
+          }
+        }
+      }
+    }
+
+    return error;
+  }
 
-  public abstract void setBackgroundColour(Color col);
+  private AtomSpecModel getAtomSpec(
+          AAStructureBindingModel.SuperposeData superposeData,
+          BitSet matched)
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    int nextColumnMatch = matched.nextSetBit(0);
+    while (nextColumnMatch != -1)
+    {
+      int pdbResNum = superposeData.pdbResNo[nextColumnMatch];
+      model.addRange(superposeData.modelId, pdbResNum, pdbResNum,
+              superposeData.chain);
+      nextColumnMatch = matched.nextSetBit(nextColumnMatch + 1);
+    }
 
-  protected abstract StructureMappingcommandSet[] getColourBySequenceCommands(
-          String[] files, SequenceRenderer sr, AlignmentViewPanel avp);
+    return model;
+  }
 
   /**
    * returns the current sequenceRenderer that should be used to colour the
@@ -785,42 +970,974 @@ public abstract class AAStructureBindingModel
   public abstract SequenceRenderer getSequenceRenderer(
           AlignmentViewPanel alignment);
 
-  protected abstract void colourBySequence(
-          StructureMappingcommandSet[] colourBySequenceCommands);
+  /**
+   * Sends a command to the structure viewer to colour each chain with a
+   * distinct colour (to the extent supported by the viewer)
+   */
+  public void colourByChain()
+  {
+    colourBySequence = false;
 
-  public abstract void colourByChain();
+    // TODO: JAL-628 colour chains distinctly across all visible models
 
-  public abstract void colourByCharge();
+    executeCommand(false, COLOURING_STRUCTURES,
+            commandGenerator.colourByChain());
+  }
 
   /**
-   * colour any structures associated with sequences in the given alignment
-   * using the getFeatureRenderer() and getSequenceRenderer() renderers but only
-   * if colourBySequence is enabled.
+   * Sends a command to the structure viewer to colour each chain with a
+   * distinct colour (to the extent supported by the viewer)
    */
-  public void colourBySequence(AlignmentViewPanel alignmentv)
+  public void colourByCharge()
+  {
+    colourBySequence = false;
+
+    executeCommands(commandGenerator.colourByCharge(), false,
+            COLOURING_STRUCTURES);
+  }
+
+  /**
+   * Sends a command to the structure to apply a colour scheme (defined in
+   * Jalview but not necessarily applied to the alignment), which defines a
+   * colour per residue letter. More complex schemes (e.g. that depend on
+   * consensus) cannot be used here and are ignored.
+   * 
+   * @param cs
+   */
+  public void colourByJalviewColourScheme(ColourSchemeI cs)
   {
-    if (!colourBySequence || !isLoadingFinished())
+    colourBySequence = false;
+
+    if (cs == null || !cs.isSimple())
     {
       return;
     }
-    if (getSsm() == null)
+
+    /*
+     * build a map of {Residue3LetterCode, Color}
+     */
+    Map<String, Color> colours = new HashMap<>();
+    List<String> residues = ResidueProperties.getResidues(isNucleotide(),
+            false);
+    for (String resName : residues)
+    {
+      char res = resName.length() == 3
+              ? ResidueProperties.getSingleCharacterCode(resName)
+              : resName.charAt(0);
+      Color colour = cs.findColour(res, 0, null, null, 0f);
+      colours.put(resName, colour);
+    }
+
+    /*
+     * pass to the command constructor, and send the command
+     */
+    List<StructureCommandI> cmd = commandGenerator
+            .colourByResidues(colours);
+    executeCommands(cmd, false, COLOURING_STRUCTURES);
+  }
+
+  public void setBackgroundColour(Color col)
+  {
+    StructureCommandI cmd = commandGenerator.setBackgroundColour(col);
+    executeCommand(false, null, cmd);
+  }
+
+  /**
+   * Execute one structure viewer command. If {@code getReply} is true, may
+   * optionally return one or more reply messages, else returns null.
+   * 
+   * @param cmd
+   * @param getReply
+   */
+  protected abstract List<String> executeCommand(StructureCommandI cmd,
+          boolean getReply);
+
+  /**
+   * Executes one or more structure viewer commands
+   * 
+   * @param commands
+   * @param getReply
+   * @param msg
+   */
+  protected List<String> executeCommands(List<StructureCommandI> commands,
+          boolean getReply, String msg)
+  {
+    return executeCommand(getReply, msg,
+            commands.toArray(new StructureCommandI[commands.size()]));
+  }
+
+  /**
+   * Executes one or more structure viewer commands, optionally returning the
+   * reply, and optionally showing a status message while the command is being
+   * executed.
+   * <p>
+   * If a reply is wanted, the execution is done synchronously (waits),
+   * otherwise it is done in a separate thread (doesn't wait).
+   * 
+   * @param getReply
+   * @param msg
+   * @param cmds
+   * @return
+   */
+  protected List<String> executeCommand(boolean getReply, String msg,
+          StructureCommandI... cmds)
+  {
+    JalviewStructureDisplayI theViewer = getViewer();
+    final long handle = msg == null ? 0 : theViewer.startProgressBar(msg);
+
+    if (getReply)
+    {
+      /*
+       * execute and wait for reply
+       */
+      List<String> response = new ArrayList<>();
+      try
+      {
+        for (StructureCommandI cmd : cmds)
+        {
+          List<String> replies = executeCommand(cmd, true);
+          response.addAll(replies);
+        }
+        return response;
+      } finally
+      {
+        if (msg != null)
+        {
+          theViewer.stopProgressBar(null, handle);
+        }
+      }
+    }
+
+    /*
+     * fire and forget
+     */
+    String threadName = msg == null ? "StructureCommand" : msg;
+    new Thread(new Runnable()
+    {
+      @Override
+      public void run()
+      {
+        try
+        {
+          for (StructureCommandI cmd : cmds)
+          {
+            executeCommand(cmd, false);
+          }
+        } finally
+        {
+          if (msg != null)
+          {
+            SwingUtilities.invokeLater(new Runnable()
+            {
+              @Override
+              public void run()
+              {
+                theViewer.stopProgressBar(null, handle);
+              }
+            });
+          }
+        }
+      }
+    }, threadName).start();
+    return null;
+  }
+
+  /**
+   * Colours any structures associated with sequences in the given alignment as
+   * coloured in the alignment view, provided colourBySequence is enabled
+   */
+  public void colourBySequence(AlignmentViewPanel alignmentv)
+  {
+    if (!colourBySequence || !isLoadingFinished() || getSsm() == null)
     {
       return;
     }
-    String[] files = getStructureFiles();
+    Map<Object, AtomSpecModel> colourMap = buildColoursMap(ssm, sequence,
+            alignmentv);
+
+    List<StructureCommandI> colourBySequenceCommands = commandGenerator
+            .colourBySequence(colourMap);
+    executeCommands(colourBySequenceCommands, false, COLOURING_STRUCTURES);
+  }
+
+  /**
+   * Centre the display in the structure viewer
+   */
+  public void focusView()
+  {
+    executeCommand(false, null, commandGenerator.focusView());
+  }
 
-    SequenceRenderer sr = getSequenceRenderer(alignmentv);
+  /**
+   * Generates and executes a command to show only specified chains in the
+   * structure viewer. The list of chains to show should contain entries
+   * formatted as "pdbid:chaincode".
+   * 
+   * @param toShow
+   */
+  public void showChains(List<String> toShow)
+  {
+    // todo or reformat toShow list entries as modelNo:pdbId:chainCode ?
 
-    StructureMappingcommandSet[] colourBySequenceCommands = getColourBySequenceCommands(
-            files, sr, alignmentv);
-    colourBySequence(colourBySequenceCommands);
+    /*
+     * Reformat the pdbid:chainCode values as modelNo:chainCode
+     * since this is what is needed to construct the viewer command
+     * todo: find a less messy way to do this
+     */
+    List<String> showThese = new ArrayList<>();
+    for (String chainId : toShow)
+    {
+      String[] tokens = chainId.split("\\:");
+      if (tokens.length == 2)
+      {
+        String pdbFile = getFileForChain(chainId);
+        String model = getModelIdForFile(pdbFile);
+        showThese.add(model + ":" + tokens[1]);
+      }
+    }
+    executeCommands(commandGenerator.showChains(showThese), false, null);
   }
 
+  /**
+   * Answers the structure viewer's model id given a PDB file name. Returns an
+   * empty string if model id is not found.
+   * 
+   * @param chainId
+   * @return
+   */
+  protected abstract String getModelIdForFile(String chainId);
+
   public boolean hasFileLoadingError()
   {
     return fileLoadingError != null && fileLoadingError.length() > 0;
   }
 
-  public abstract jalview.api.FeatureRenderer getFeatureRenderer(
-          AlignmentViewPanel alignment);
+  /**
+   * Returns the FeatureRenderer for the given alignment view, or null if
+   * feature display is turned off in the view.
+   * 
+   * @param avp
+   * @return
+   */
+  public FeatureRenderer getFeatureRenderer(AlignmentViewPanel avp)
+  {
+    AlignmentViewPanel ap = (avp == null) ? getViewer().getAlignmentPanel()
+            : avp;
+    if (ap == null)
+    {
+      return null;
+    }
+    return ap.getAlignViewport().isShowSequenceFeatures()
+            ? ap.getFeatureRenderer()
+            : null;
+  }
+
+  protected void setStructureCommands(StructureCommandsI cmd)
+  {
+    commandGenerator = cmd;
+  }
+
+  /**
+   * Records association of one chain id (formatted as "pdbid:chainCode") with
+   * the corresponding PDB file name
+   * 
+   * @param chainId
+   * @param fileName
+   */
+  public void addChainFile(String chainId, String fileName)
+  {
+    chainFile.put(chainId, fileName);
+  }
+
+  /**
+   * Returns the PDB filename for the given chain id (formatted as
+   * "pdbid:chainCode"), or null if not found
+   * 
+   * @param chainId
+   * @return
+   */
+  protected String getFileForChain(String chainId)
+  {
+    return chainFile.get(chainId);
+  }
+
+  @Override
+  public void updateColours(Object source)
+  {
+    AlignmentViewPanel ap = (AlignmentViewPanel) source;
+    // ignore events from panels not used to colour this view
+    if (!getViewer().isUsedForColourBy(ap))
+    {
+      return;
+    }
+    if (!isLoadingFromArchive())
+    {
+      colourBySequence(ap);
+    }
+  }
+
+  public StructureCommandsI getCommandGenerator()
+  {
+    return commandGenerator;
+  }
+
+  protected abstract ViewerType getViewerType();
+
+  /**
+   * Builds a data structure which records mapped structure residues for each
+   * colour. From this we can easily generate the viewer commands for colour by
+   * sequence. Constructs and returns a map of {@code Color} to
+   * {@code AtomSpecModel}, where the atomspec model holds
+   * 
+   * <pre>
+   *   Model ids
+   *     Chains
+   *       Residue positions
+   * </pre>
+   * 
+   * Ordering is by order of addition (for colours), natural ordering (for
+   * models and chains)
+   * 
+   * @param ssm
+   * @param sequence
+   * @param viewPanel
+   * @return
+   */
+  protected Map<Object, AtomSpecModel> buildColoursMap(
+          StructureSelectionManager ssm, SequenceI[][] sequence,
+          AlignmentViewPanel viewPanel)
+  {
+    String[] files = getStructureFiles();
+    SequenceRenderer sr = getSequenceRenderer(viewPanel);
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    FeatureColourFinder finder = new FeatureColourFinder(fr);
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    HiddenColumns cs = viewport.getAlignment().getHiddenColumns();
+    AlignmentI al = viewport.getAlignment();
+    Map<Object, AtomSpecModel> colourMap = new LinkedHashMap<>();
+    Color lastColour = null;
+
+    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+    {
+      final String modelId = getModelIdForFile(files[pdbfnum]);
+      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+
+      if (mapping == null || mapping.length < 1)
+      {
+        continue;
+      }
+
+      int startPos = -1, lastPos = -1;
+      String lastChain = "";
+      for (int s = 0; s < sequence[pdbfnum].length; s++)
+      {
+        for (int sp, m = 0; m < mapping.length; m++)
+        {
+          final SequenceI seq = sequence[pdbfnum][s];
+          if (mapping[m].getSequence() == seq
+                  && (sp = al.findIndex(seq)) > -1)
+          {
+            SequenceI asp = al.getSequenceAt(sp);
+            for (int r = 0; r < asp.getLength(); r++)
+            {
+              // no mapping to gaps in sequence
+              if (Comparison.isGap(asp.getCharAt(r)))
+              {
+                continue;
+              }
+              int pos = mapping[m].getPDBResNum(asp.findPosition(r));
+
+              if (pos < 1 || pos == lastPos)
+              {
+                continue;
+              }
+
+              Color colour = sr.getResidueColour(seq, r, finder);
+
+              /*
+               * darker colour for hidden regions
+               */
+              if (!cs.isVisible(r))
+              {
+                colour = Color.GRAY;
+              }
+
+              final String chain = mapping[m].getChain();
+
+              /*
+               * Just keep incrementing the end position for this colour range
+               * _unless_ colour, PDB model or chain has changed, or there is a
+               * gap in the mapped residue sequence
+               */
+              final boolean newColour = !colour.equals(lastColour);
+              final boolean nonContig = lastPos + 1 != pos;
+              final boolean newChain = !chain.equals(lastChain);
+              if (newColour || nonContig || newChain)
+              {
+                if (startPos != -1)
+                {
+                  addAtomSpecRange(colourMap, lastColour, modelId, startPos,
+                          lastPos, lastChain);
+                }
+                startPos = pos;
+              }
+              lastColour = colour;
+              lastPos = pos;
+              lastChain = chain;
+            }
+            // final colour range
+            if (lastColour != null)
+            {
+              addAtomSpecRange(colourMap, lastColour, modelId, startPos,
+                      lastPos, lastChain);
+            }
+            // break;
+          }
+        }
+      }
+    }
+    return colourMap;
+  }
+
+  /**
+   * todo better refactoring (map lookup or similar to get viewer structure id)
+   * 
+   * @param pdbfnum
+   * @param file
+   * @return
+   */
+  protected String getModelId(int pdbfnum, String file)
+  {
+    return String.valueOf(pdbfnum);
+  }
+
+  /**
+   * Saves chains, formatted as "pdbId:chainCode", and lookups from this to the
+   * full PDB file path
+   * 
+   * @param pdb
+   * @param file
+   */
+  public void stashFoundChains(StructureFile pdb, String file)
+  {
+    for (int i = 0; i < pdb.getChains().size(); i++)
+    {
+      String chid = pdb.getId() + ":" + pdb.getChains().elementAt(i).id;
+      addChainFile(chid, file);
+      getChainNames().add(chid);
+    }
+  }
+
+  /**
+   * Helper method to add one contiguous range to the AtomSpec model for the
+   * given value (creating the model if necessary). As used by Jalview,
+   * {@code value} is
+   * <ul>
+   * <li>a colour, when building a 'colour structure by sequence' command</li>
+   * <li>a feature value, when building a 'set Chimera attributes from features'
+   * command</li>
+   * </ul>
+   * 
+   * @param map
+   * @param value
+   * @param model
+   * @param startPos
+   * @param endPos
+   * @param chain
+   */
+  public static final void addAtomSpecRange(Map<Object, AtomSpecModel> map,
+          Object value, String model, int startPos, int endPos,
+          String chain)
+  {
+    /*
+     * Get/initialize map of data for the colour
+     */
+    AtomSpecModel atomSpec = map.get(value);
+    if (atomSpec == null)
+    {
+      atomSpec = new AtomSpecModel();
+      map.put(value, atomSpec);
+    }
+
+    atomSpec.addRange(model, startPos, endPos, chain);
+  }
+
+  /**
+   * Returns the file extension (including '.' separator) to use for a saved
+   * viewer session file. Default is to return null (not supported), override as
+   * required.
+   * 
+   * @return
+   */
+  public String getSessionFileExtension()
+  {
+    return null;
+  }
+
+  /**
+   * If supported, saves the state of the structure viewer to a temporary file
+   * and returns the file. Returns null and logs an error on any failure.
+   * 
+   * @return
+   */
+  public File saveSession()
+  {
+    String prefix = getViewerType().toString();
+    String suffix = getSessionFileExtension();
+    File f = null;
+    try
+    {
+      f = File.createTempFile(prefix, suffix);
+      saveSession(f);
+    } catch (IOException e)
+    {
+      Cache.log.error(String.format("Error saving %s session: %s", prefix,
+              e.toString()));
+    }
+
+    return f;
+  }
+
+  /**
+   * Saves the structure viewer session to the given file
+   * 
+   * @param f
+   */
+  protected void saveSession(File f)
+  {
+    StructureCommandI cmd = commandGenerator.saveSession(f.getPath());
+    if (cmd != null)
+    {
+      executeCommand(cmd, false);
+    }
+  }
+
+  /**
+   * Returns true if the viewer is an external structure viewer for which the
+   * process is still alive, else false (for Jmol, or an external viewer which
+   * the user has independently closed)
+   * 
+   * @return
+   */
+  public boolean isViewerRunning()
+  {
+    return false;
+  }
+
+  /**
+   * Closes Jalview's structure viewer panel and releases associated resources.
+   * If it is managing an external viewer program, and {@code forceClose} is
+   * true, also asks that program to close.
+   * 
+   * @param forceClose
+   */
+  public void closeViewer(boolean forceClose)
+  {
+    getSsm().removeStructureViewerListener(this, this.getStructureFiles());
+    releaseUIResources();
+
+    /*
+     * end the thread that closes this panel if the external viewer closes
+     */
+    if (externalViewerMonitor != null)
+    {
+      externalViewerMonitor.interrupt();
+      externalViewerMonitor = null;
+    }
+
+    stopListening();
+
+    if (forceClose)
+    {
+      StructureCommandI cmd = getCommandGenerator().closeViewer();
+      if (cmd != null)
+      {
+        executeCommand(cmd, false);
+      }
+    }
+  }
+
+  /**
+   * Returns the URL of a help page for the structure viewer, or null if none is
+   * known
+   * 
+   * @return
+   */
+  public String getHelpURL()
+  {
+    return null;
+  }
+
+  /**
+   * <pre>
+   * Helper method to build a map of 
+   *   { featureType, { feature value, AtomSpecModel } }
+   * </pre>
+   * 
+   * @param viewPanel
+   * @return
+   */
+  protected Map<String, Map<Object, AtomSpecModel>> buildFeaturesMap(
+          AlignmentViewPanel viewPanel)
+  {
+    Map<String, Map<Object, AtomSpecModel>> theMap = new LinkedHashMap<>();
+    String[] files = getStructureFiles();
+    if (files == null)
+    {
+      return theMap;
+    }
+
+    FeatureRenderer fr = viewPanel.getFeatureRenderer();
+    if (fr == null)
+    {
+      return theMap;
+    }
+
+    AlignViewportI viewport = viewPanel.getAlignViewport();
+    List<String> visibleFeatures = fr.getDisplayedFeatureTypes();
+
+    /*
+     * if alignment is showing features from complement, we also transfer
+     * these features to the corresponding mapped structure residues
+     */
+    boolean showLinkedFeatures = viewport.isShowComplementFeatures();
+    List<String> complementFeatures = new ArrayList<>();
+    FeatureRenderer complementRenderer = null;
+    if (showLinkedFeatures)
+    {
+      AlignViewportI comp = fr.getViewport().getCodingComplement();
+      if (comp != null)
+      {
+        complementRenderer = Desktop.getAlignFrameFor(comp)
+                .getFeatureRenderer();
+        complementFeatures = complementRenderer.getDisplayedFeatureTypes();
+      }
+    }
+    if (visibleFeatures.isEmpty() && complementFeatures.isEmpty())
+    {
+      return theMap;
+    }
+
+    AlignmentI alignment = viewPanel.getAlignment();
+    SequenceI[][] seqs = getSequence();
+
+    for (int pdbfnum = 0; pdbfnum < files.length; pdbfnum++)
+    {
+      String modelId = getModelIdForFile(files[pdbfnum]);
+      StructureMapping[] mapping = ssm.getMapping(files[pdbfnum]);
+
+      if (mapping == null || mapping.length < 1)
+      {
+        continue;
+      }
+
+      for (int seqNo = 0; seqNo < seqs[pdbfnum].length; seqNo++)
+      {
+        for (int m = 0; m < mapping.length; m++)
+        {
+          final SequenceI seq = seqs[pdbfnum][seqNo];
+          int sp = alignment.findIndex(seq);
+          StructureMapping structureMapping = mapping[m];
+          if (structureMapping.getSequence() == seq && sp > -1)
+          {
+            /*
+             * found a sequence with a mapping to a structure;
+             * now scan its features
+             */
+            if (!visibleFeatures.isEmpty())
+            {
+              scanSequenceFeatures(visibleFeatures, structureMapping, seq,
+                      theMap, modelId);
+            }
+            if (showLinkedFeatures)
+            {
+              scanComplementFeatures(complementRenderer, structureMapping,
+                      seq, theMap, modelId);
+            }
+          }
+        }
+      }
+    }
+    return theMap;
+  }
+
+  /**
+   * Ask the structure viewer to open a session file. Returns true if
+   * successful, else false (or not supported).
+   * 
+   * @param filepath
+   * @return
+   */
+  public boolean openSession(String filepath)
+  {
+    StructureCommandI cmd = getCommandGenerator().openSession(filepath);
+    if (cmd == null)
+    {
+      return false;
+    }
+    executeCommand(cmd, true);
+    // todo: test for failure - how?
+    return true;
+  }
+
+  /**
+   * Scans visible features in mapped positions of the CDS/peptide complement,
+   * and adds any found to the map of attribute values/structure positions
+   * 
+   * @param complementRenderer
+   * @param structureMapping
+   * @param seq
+   * @param theMap
+   * @param modelNumber
+   */
+  protected static void scanComplementFeatures(
+          FeatureRenderer complementRenderer,
+          StructureMapping structureMapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap,
+          String modelNumber)
+  {
+    /*
+     * for each sequence residue mapped to a structure position...
+     */
+    for (int seqPos : structureMapping.getMapping().keySet())
+    {
+      /*
+       * find visible complementary features at mapped position(s)
+       */
+      MappedFeatures mf = complementRenderer
+              .findComplementFeaturesAtResidue(seq, seqPos);
+      if (mf != null)
+      {
+        for (SequenceFeature sf : mf.features)
+        {
+          String type = sf.getType();
+
+          /*
+           * Don't copy features which originated from Chimera
+           */
+          if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+                  .equals(sf.getFeatureGroup()))
+          {
+            continue;
+          }
+
+          /*
+           * record feature 'value' (score/description/type) as at the
+           * corresponding structure position
+           */
+          List<int[]> mappedRanges = structureMapping
+                  .getPDBResNumRanges(seqPos, seqPos);
+
+          if (!mappedRanges.isEmpty())
+          {
+            String value = sf.getDescription();
+            if (value == null || value.length() == 0)
+            {
+              value = type;
+            }
+            float score = sf.getScore();
+            if (score != 0f && !Float.isNaN(score))
+            {
+              value = Float.toString(score);
+            }
+            Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+            if (featureValues == null)
+            {
+              featureValues = new HashMap<>();
+              theMap.put(type, featureValues);
+            }
+            for (int[] range : mappedRanges)
+            {
+              addAtomSpecRange(featureValues, value, modelNumber, range[0],
+                      range[1], structureMapping.getChain());
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Inspect features on the sequence; for each feature that is visible,
+   * determine its mapped ranges in the structure (if any) according to the
+   * given mapping, and add them to the map.
+   * 
+   * @param visibleFeatures
+   * @param mapping
+   * @param seq
+   * @param theMap
+   * @param modelId
+   */
+  protected static void scanSequenceFeatures(List<String> visibleFeatures,
+          StructureMapping mapping, SequenceI seq,
+          Map<String, Map<Object, AtomSpecModel>> theMap, String modelId)
+  {
+    List<SequenceFeature> sfs = seq.getFeatures().getPositionalFeatures(
+            visibleFeatures.toArray(new String[visibleFeatures.size()]));
+    for (SequenceFeature sf : sfs)
+    {
+      String type = sf.getType();
+
+      /*
+       * Don't copy features which originated from Chimera
+       */
+      if (JalviewChimeraBinding.CHIMERA_FEATURE_GROUP
+              .equals(sf.getFeatureGroup()))
+      {
+        continue;
+      }
+
+      List<int[]> mappedRanges = mapping.getPDBResNumRanges(sf.getBegin(),
+              sf.getEnd());
+
+      if (!mappedRanges.isEmpty())
+      {
+        String value = sf.getDescription();
+        if (value == null || value.length() == 0)
+        {
+          value = type;
+        }
+        float score = sf.getScore();
+        if (score != 0f && !Float.isNaN(score))
+        {
+          value = Float.toString(score);
+        }
+        Map<Object, AtomSpecModel> featureValues = theMap.get(type);
+        if (featureValues == null)
+        {
+          featureValues = new HashMap<>();
+          theMap.put(type, featureValues);
+        }
+        for (int[] range : mappedRanges)
+        {
+          addAtomSpecRange(featureValues, value, modelId, range[0],
+                  range[1], mapping.getChain());
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns the number of structure files in the structure viewer and mapped to
+   * Jalview. This may be zero if the files are still in the process of loading
+   * in the viewer.
+   * 
+   * @return
+   */
+  public int getMappedStructureCount()
+  {
+    String[] files = getStructureFiles();
+    return files == null ? 0 : files.length;
+  }
+
+  /**
+   * Starts a thread that waits for the external viewer program process to
+   * finish, so that we can then close the associated resources. This avoids
+   * leaving orphaned viewer panels in Jalview if the user closes the external
+   * viewer.
+   * 
+   * @param p
+   */
+  protected void startExternalViewerMonitor(Process p)
+  {
+    externalViewerMonitor = new Thread(new Runnable()
+    {
+
+      @Override
+      public void run()
+      {
+        try
+        {
+          p.waitFor();
+          JalviewStructureDisplayI display = getViewer();
+          if (display != null)
+          {
+            display.closeViewer(false);
+          }
+        } catch (InterruptedException e)
+        {
+          // exit thread if Chimera Viewer is closed in Jalview
+        }
+      }
+    });
+    externalViewerMonitor.start();
+  }
+
+  /**
+   * If supported by the external structure viewer, sends it commands to notify
+   * model or selection changes to the specified URL (where Jalview has started
+   * a listener)
+   * 
+   * @param uri
+   */
+  protected void startListening(String uri)
+  {
+    List<StructureCommandI> commands = getCommandGenerator()
+            .startNotifications(uri);
+    if (commands != null)
+    {
+      executeCommands(commands, false, null);
+    }
+  }
+
+  /**
+   * If supported by the external structure viewer, sends it commands to stop
+   * notifying model or selection changes
+   */
+  protected void stopListening()
+  {
+    List<StructureCommandI> commands = getCommandGenerator()
+            .stopNotifications();
+    if (commands != null)
+    {
+      executeCommands(commands, false, null);
+    }
+  }
+
+  /**
+   * If supported by the structure viewer, queries it for all residue attributes
+   * with the given attribute name, and creates features on corresponding
+   * residues of the alignment. Returns the number of features added.
+   * 
+   * @param attName
+   * @param alignmentPanel
+   * @return
+   */
+  public int copyStructureAttributesToFeatures(String attName,
+          AlignmentPanel alignmentPanel)
+  {
+    StructureCommandI cmd = getCommandGenerator()
+            .getResidueAttributes(attName);
+    if (cmd == null)
+    {
+      return 0;
+    }
+    List<String> residueAttributes = executeCommand(cmd, true);
+
+    int featuresAdded = createFeaturesForAttributes(attName,
+            residueAttributes);
+    if (featuresAdded > 0)
+    {
+      alignmentPanel.getFeatureRenderer().featuresAdded();
+    }
+    return featuresAdded;
+  }
+
+  /**
+   * Parses {@code residueAttributes} and creates sequence features on any
+   * mapped alignment residues. Returns the number of features created.
+   * <p>
+   * {@code residueAttributes} is the reply from the structure viewer to a
+   * command to list any residue attributes for the given attribute name. Syntax
+   * and parsing of this is viewer-specific.
+   * 
+   * @param attName
+   * @param residueAttributes
+   * @return
+   */
+  protected int createFeaturesForAttributes(String attName,
+          List<String> residueAttributes)
+  {
+    return 0;
+  }
 }
index fb54bba..ae0243e 100755 (executable)
  */
 package jalview.util;
 
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.DBRefSource;
-import jalview.datamodel.Mapping;
-import jalview.datamodel.PDBEntry;
-import jalview.datamodel.SequenceI;
-
 import java.util.ArrayList;
 import java.util.BitSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import com.stevesoft.pat.Regex;
 
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
+
 /**
  * Utilities for handling DBRef objects and their collections.
  */
@@ -308,6 +307,75 @@ public class DBRefUtils
 
        };
 
+  /**
+   * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
+   * database is PDB.
+   * <p>
+   * Used by file parsers to generate DBRefs from annotation within file (eg
+   * Stockholm)
+   * 
+   * @param dbname
+   * @param version
+   * @param acn
+   * @param seq
+   *          where to annotate with reference
+   * @return parsed version of entry that was added to seq (if any)
+   */
+  public static DBRefEntry parseToDbRef(SequenceI seq, String dbname,
+          String version, String acn)
+  {
+    DBRefEntry ref = null;
+    if (dbname != null)
+    {
+      String locsrc = DBRefUtils.getCanonicalName(dbname);
+      if (locsrc.equals(DBRefSource.PDB))
+      {
+        /*
+         * Check for PFAM style stockhom PDB accession id citation e.g.
+         * "1WRI A; 7-80;"
+         */
+        Regex r = new com.stevesoft.pat.Regex(
+                "([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)");
+        if (r.search(acn.trim()))
+        {
+          String pdbid = r.stringMatched(1);
+          String chaincode = r.stringMatched(2);
+          if (chaincode == null)
+          {
+            chaincode = " ";
+          }
+          // String mapstart = r.stringMatched(3);
+          // String mapend = r.stringMatched(4);
+          if (chaincode.equals(" "))
+          {
+            chaincode = "_";
+          }
+          // construct pdb ref.
+          ref = new DBRefEntry(locsrc, version, pdbid + chaincode);
+          PDBEntry pdbr = new PDBEntry();
+          pdbr.setId(pdbid);
+          pdbr.setType(PDBEntry.Type.PDB);
+          pdbr.setChainCode(chaincode);
+          seq.addPDBId(pdbr);
+        }
+        else
+        {
+          System.err.println("Malformed PDB DR line:" + acn);
+        }
+      }
+      else
+      {
+        // default:
+        ref = new DBRefEntry(locsrc, version, acn.trim());
+      }
+    }
+    if (ref != null)
+    {
+      seq.addDBRef(ref);
+    }
+    return ref;
+  }
+
        /**
         * accession ID and DB must be identical. Version is ignored. Map is either not
         * defined or is a match (or is compatible?)
@@ -451,60 +519,6 @@ public class DBRefUtils
   }
 
        /**
-        * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
-        * database is PDB.
-        * <p>
-        * Used by file parsers to generate DBRefs from annotation within file (eg
-        * Stockholm)
-        * 
-        * @param dbname
-        * @param version
-        * @param acn
-        * @param seq     where to annotate with reference
-        * @return parsed version of entry that was added to seq (if any)
-        */
-       public static DBRefEntry parseToDbRef(SequenceI seq, String dbname, String version, String acn) {
-               DBRefEntry ref = null;
-               if (dbname != null) {
-                       String locsrc = DBRefUtils.getCanonicalName(dbname);
-                       if (locsrc.equals(DBRefSource.PDB)) {
-                               /*
-                                * Check for PFAM style stockhom PDB accession id citation e.g. "1WRI A; 7-80;"
-                                */
-                               Regex r = new com.stevesoft.pat.Regex("([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)");
-                               if (r.search(acn.trim())) {
-                                       String pdbid = r.stringMatched(1);
-                                       String chaincode = r.stringMatched(2);
-                                       if (chaincode == null) {
-                                               chaincode = " ";
-                                       }
-                                       // String mapstart = r.stringMatched(3);
-                                       // String mapend = r.stringMatched(4);
-                                       if (chaincode.equals(" ")) {
-                                               chaincode = "_";
-                                       }
-                                       // construct pdb ref.
-                                       ref = new DBRefEntry(locsrc, version, pdbid + chaincode);
-                                       PDBEntry pdbr = new PDBEntry();
-                                       pdbr.setId(pdbid);
-                                       pdbr.setType(PDBEntry.Type.PDB);
-                                       pdbr.setChainCode(chaincode);
-                                       seq.addPDBId(pdbr);
-                               } else {
-                                       System.err.println("Malformed PDB DR line:" + acn);
-                               }
-                       } else {
-                               // default:
-                               ref = new DBRefEntry(locsrc, version, acn);
-                       }
-               }
-               if (ref != null) {
-                       seq.addDBRef(ref);
-               }
-               return ref;
-       }
-
-       /**
         * Returns true if either object is null, or they are equal
         * 
         * @param o1
index dde1662..7a5fb15 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 import java.io.IOException;
index 72d46a2..ecbb6e1 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 public class MathUtils
index c83da4e..83330b9 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 import java.awt.event.MouseEvent;
index 7a3c8ca..9877b5d 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 import java.awt.GraphicsEnvironment;
index a7817bd..f1014af 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 import java.awt.GraphicsEnvironment;
index bf17259..74c565d 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util;
 
 import java.awt.event.MouseEvent;
index 8816a7f..f47b367 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util.matcher;
 
 import jalview.util.MessageManager;
index 0792509..51e1828 100644 (file)
@@ -1,39 +1,69 @@
+/*
+ * 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.util.matcher;
 
 import java.util.Objects;
-import java.util.regex.Pattern;
 
 /**
  * A bean to describe one attribute-based filter
  */
 public class Matcher implements MatcherI
 {
+  public enum PatternType
+  {
+    String, Integer, Float
+  }
+
   /*
    * the comparison condition
    */
-  Condition condition;
+  private final Condition condition;
 
   /*
-   * the string pattern as entered, or the regex, to compare to
-   * also holds the string form of float value if a numeric condition
+   * the string pattern as entered, to compare to
    */
-  String pattern;
+  private String pattern;
 
   /*
    * the pattern in upper case, for non-case-sensitive matching
    */
-  String uppercasePattern;
+  private final String uppercasePattern;
 
   /*
    * the compiled regex if using a pattern match condition
-   * (reserved for possible future enhancement)
+   * (possible future enhancement)
+   */
+  // private Pattern regexPattern;
+
+  /*
+   * the value to compare to for a numerical condition with a float pattern
    */
-  Pattern regexPattern;
+  private float floatValue = 0F;
 
   /*
-   * the value to compare to for a numerical condition
+   * the value to compare to for a numerical condition with an integer pattern
    */
-  float value;
+  private long longValue = 0L;
+
+  private PatternType patternType;
 
   /**
    * Constructor
@@ -51,30 +81,38 @@ public class Matcher implements MatcherI
   {
     Objects.requireNonNull(cond);
     condition = cond;
+
     if (cond.isNumeric())
     {
-      value = Float.valueOf(compareTo);
-      pattern = String.valueOf(value);
-      uppercasePattern = pattern;
+      try
+      {
+        longValue = Long.valueOf(compareTo);
+        pattern = String.valueOf(longValue);
+        patternType = PatternType.Integer;
+      } catch (NumberFormatException e)
+      {
+        floatValue = Float.valueOf(compareTo);
+        pattern = String.valueOf(floatValue);
+        patternType = PatternType.Float;
+      }
     }
     else
     {
       pattern = compareTo;
-      if (pattern != null)
-      {
-        uppercasePattern = pattern.toUpperCase();
-      }
+      patternType = PatternType.String;
     }
 
+    uppercasePattern = pattern == null ? null : pattern.toUpperCase();
+
     // if we add regex conditions (e.g. matchesPattern), then
     // pattern should hold the raw regex, and
     // regexPattern = Pattern.compile(compareTo);
   }
 
   /**
-   * Constructor for a numerical match condition. Note that if a string
-   * comparison condition is specified, this will be converted to a comparison
-   * with the float value as string
+   * Constructor for a float-valued numerical match condition. Note that if a
+   * string comparison condition is specified, this will be converted to a
+   * comparison with the float value as string
    * 
    * @param cond
    * @param compareTo
@@ -85,39 +123,56 @@ public class Matcher implements MatcherI
   }
 
   /**
+   * Constructor for an integer-valued numerical match condition. Note that if a
+   * string comparison condition is specified, this will be converted to a
+   * comparison with the integer value as string
+   * 
+   * @param cond
+   * @param compareTo
+   */
+  public Matcher(Condition cond, long compareTo)
+  {
+    this(cond, String.valueOf(compareTo));
+  }
+
+  /**
    * {@inheritDoc}
    */
-  @SuppressWarnings("incomplete-switch")
   @Override
-  public boolean matches(String val)
+  public boolean matches(String compareTo)
   {
-    if (condition.isNumeric())
+    if (compareTo == null)
     {
-      try
-      {
-        /*
-         * treat a null value (no such attribute) as
-         * failing any numerical filter condition
-         */
-        return val == null ? false : matches(Float.valueOf(val));
-      } catch (NumberFormatException e)
-      {
-        return false;
-      }
+      return matchesNull();
     }
-    
-    /*
-     * a null value matches a negative condition, fails a positive test
-     */
-    if (val == null)
+
+    boolean matched = false;
+    switch (patternType)
     {
-      return condition == Condition.NotContains
-              || condition == Condition.NotMatches 
-              || condition == Condition.NotPresent;
+    case Float:
+      matched = matchesFloat(compareTo, floatValue);
+      break;
+    case Integer:
+      matched = matchesLong(compareTo);
+      break;
+    default:
+      matched = matchesString(compareTo);
+      break;
     }
-    
-    String upper = val.toUpperCase().trim();
+    return matched;
+  }
+
+  /**
+   * Executes a non-case-sensitive string comparison to the given value, after
+   * trimming it. Returns true if the test passes, false if it fails.
+   * 
+   * @param compareTo
+   * @return
+   */
+  boolean matchesString(String compareTo)
+  {
     boolean matched = false;
+    String upper = compareTo.toUpperCase().trim();
     switch(condition) {
     case Matches:
       matched = upper.equals(uppercasePattern);
@@ -141,38 +196,48 @@ public class Matcher implements MatcherI
   }
 
   /**
-   * Applies a numerical comparison match condition
+   * Performs a numerical comparison match condition test against a float value
    * 
-   * @param f
+   * @param testee
+   * @param compareTo
    * @return
    */
-  @SuppressWarnings("incomplete-switch")
-  boolean matches(float f)
+  boolean matchesFloat(String testee, float compareTo)
   {
     if (!condition.isNumeric())
     {
-      return matches(String.valueOf(f));
+      // failsafe, shouldn't happen
+      return matches(testee);
+    }
+
+    float f = 0f;
+    try
+    {
+      f = Float.valueOf(testee);
+    } catch (NumberFormatException e)
+    {
+      return false;
     }
     
     boolean matched = false;
     switch (condition) {
     case LT:
-      matched = f < value;
+      matched = f < compareTo;
       break;
     case LE:
-      matched = f <= value;
+      matched = f <= compareTo;
       break;
     case EQ:
-      matched = f == value;
+      matched = f == compareTo;
       break;
     case NE:
-      matched = f != value;
+      matched = f != compareTo;
       break;
     case GT:
-      matched = f > value;
+      matched = f > compareTo;
       break;
     case GE:
-      matched = f >= value;
+      matched = f >= compareTo;
       break;
     default:
       break;
@@ -188,7 +253,7 @@ public class Matcher implements MatcherI
   @Override
   public int hashCode()
   {
-    return pattern.hashCode() + condition.hashCode() + (int) value;
+    return pattern.hashCode() + condition.hashCode() + (int) floatValue;
   }
 
   /**
@@ -203,7 +268,8 @@ public class Matcher implements MatcherI
       return false;
     }
     Matcher m = (Matcher) obj;
-    if (condition != m.condition || value != m.value)
+    if (condition != m.condition || floatValue != m.floatValue
+            || longValue != m.longValue)
     {
       return false;
     }
@@ -227,12 +293,6 @@ public class Matcher implements MatcherI
   }
 
   @Override
-  public float getFloatValue()
-  {
-    return value;
-  }
-
-  @Override
   public String toString()
   {
     StringBuilder sb = new StringBuilder();
@@ -248,4 +308,81 @@ public class Matcher implements MatcherI
 
     return sb.toString();
   }
+
+  /**
+   * Performs a numerical comparison match condition test against an integer
+   * value
+   * 
+   * @param compareTo
+   * @return
+   */
+  boolean matchesLong(String compareTo)
+  {
+    if (!condition.isNumeric())
+    {
+      // failsafe, shouldn't happen
+      return matches(String.valueOf(compareTo));
+    }
+
+    long val = 0L;
+    try
+    {
+      val = Long.valueOf(compareTo);
+    } catch (NumberFormatException e)
+    {
+      /*
+       * try the presented value as a float instead
+       */
+      return matchesFloat(compareTo, longValue);
+    }
+    
+    boolean matched = false;
+    switch (condition) {
+    case LT:
+      matched = val < longValue;
+      break;
+    case LE:
+      matched = val <= longValue;
+      break;
+    case EQ:
+      matched = val == longValue;
+      break;
+    case NE:
+      matched = val != longValue;
+      break;
+    case GT:
+      matched = val > longValue;
+      break;
+    case GE:
+      matched = val >= longValue;
+      break;
+    default:
+      break;
+    }
+  
+    return matched;
+  }
+
+  /**
+   * Tests whether a null value matches the condition. The rule is that any
+   * numeric condition is failed, and only 'negative' string conditions are
+   * matched. So for example <br>
+   * {@code null contains "damaging"}<br>
+   * fails, but <br>
+   * {@code null does not contain "damaging"}</br>
+   * passes.
+   */
+  boolean matchesNull()
+  {
+    if (condition.isNumeric())
+    {
+      return false;
+    }
+    else
+    {
+      return condition == Condition.NotContains
+              || condition == Condition.NotMatches
+              || condition == Condition.NotPresent;
+    }
+  }
 }
index ca6d44c..a951fae 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.util.matcher;
 
 public interface MatcherI
@@ -13,6 +33,4 @@ public interface MatcherI
   Condition getCondition();
 
   String getPattern();
-
-  float getFloatValue();
 }
index 68345b9..3608626 100644 (file)
@@ -1199,8 +1199,8 @@ public abstract class FeatureRendererModel
   }
 
   @Override
-  public MappedFeatures findComplementFeaturesAtResidue(SequenceI sequence,
-          int pos)
+  public MappedFeatures findComplementFeaturesAtResidue(
+          final SequenceI sequence, final int pos)
   {
     SequenceI ds = sequence.getDatasetSequence();
     if (ds == null)
@@ -1269,9 +1269,12 @@ public abstract class FeatureRendererModel
     }
 
     /*
-     * sort by renderorder, inefficiently
+     * sort by renderorder (inefficiently but ok for small scale);
+     * NB this sorts 'on top' feature to end, for rendering
      */
     List<SequenceFeature> result = new ArrayList<>();
+    final int toAdd = found.size();
+    int added = 0;
     for (String type : renderOrder)
     {
       for (SequenceFeature sf : found)
@@ -1279,11 +1282,11 @@ public abstract class FeatureRendererModel
         if (type.equals(sf.getType()))
         {
           result.add(sf);
-          if (result.size() == found.size())
-          {
-            return new MappedFeatures(mapping, mapFrom, pos, residue,
-                    result);
-          }
+          added++;
+        }
+        if (added == toAdd)
+        {
+          break;
         }
       }
     }
index 6b8843d..7daa7b4 100644 (file)
  */
 package jalview.ws;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
 import jalview.analysis.AlignSeq;
 import jalview.api.FeatureSettingsModelI;
 import jalview.bin.Cache;
@@ -36,17 +46,6 @@ import jalview.gui.OOMWarning;
 import jalview.util.DBRefUtils;
 import jalview.util.MessageManager;
 import jalview.ws.seqfetcher.DbSourceProxy;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.Vector;
-
 import uk.ac.ebi.picr.model.UPEntry;
 import uk.ac.ebi.www.picr.AccessionMappingService.AccessionMapperServiceLocator;
 
@@ -74,8 +73,6 @@ public class DBRefFetcher implements Runnable
 
   CutAndPasteTransfer output = new CutAndPasteTransfer();
 
-  boolean running = false;
-
   /**
    * picr client instance
    */
@@ -225,24 +222,13 @@ public class DBRefFetcher implements Runnable
    */
   public void fetchDBRefs(boolean waitTillFinished)
   {
-    // TODO can we not simply write
-    // if (waitTillFinished) { run(); } else { new Thread(this).start(); }
-
-    Thread thread = new Thread(this);
-    thread.start();
-    running = true;
-
     if (waitTillFinished)
     {
-      while (running)
-      {
-        try
-        {
-          Thread.sleep(500);
-        } catch (Exception ex)
-        {
-        }
-      }
+      run();
+    }
+    else
+    {
+      new Thread(this).start();
     }
   }
 
@@ -295,7 +281,6 @@ public class DBRefFetcher implements Runnable
       throw new Error(MessageManager
               .getString("error.implementation_error_must_init_dbsources"));
     }
-    running = true;
     long startTime = System.currentTimeMillis();
     if (progressWindow != null)
     {
@@ -498,7 +483,6 @@ public class DBRefFetcher implements Runnable
     {
       listener.finished();
     }
-    running = false;
   }
 
   /**
index b19d606..8f97226 100644 (file)
@@ -34,6 +34,7 @@ import org.apache.http.NameValuePair;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.mime.HttpMultipartMode;
 import org.apache.http.entity.mime.MultipartEntity;
@@ -167,4 +168,65 @@ public class HttpClientUtils
       return null;
     }
   }
+
+  /**
+   * do an HTTP GET with URL-Encoded parameters passed in the Query string
+   * 
+   * @param url
+   * @param vals
+   * @return Reader containing content, if any, or null if no entity returned.
+   * @throws IOException
+   * @throws ClientProtocolException
+   * @throws Exception
+   */
+  public static BufferedReader doHttpGet(String url,
+          List<NameValuePair> vals, int connectionTimeoutMs,
+          int readTimeoutMs) throws ClientProtocolException, IOException
+  {
+    // todo use HttpClient 4.3 or later and class RequestConfig
+    HttpParams params = new BasicHttpParams();
+    params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,
+            HttpVersion.HTTP_1_1);
+    if (connectionTimeoutMs > 0)
+    {
+      HttpConnectionParams.setConnectionTimeout(params,
+              connectionTimeoutMs);
+    }
+    if (readTimeoutMs > 0)
+    {
+      HttpConnectionParams.setSoTimeout(params, readTimeoutMs);
+    }
+    boolean first = true;
+    for (NameValuePair param : vals)
+    {
+      if (first)
+      {
+        url += "?";
+      }
+      else
+      {
+        url += "&";
+      }
+      url += param.getName();
+      url += "=";
+      url += param.getValue();
+    }
+    HttpClient httpclient = new DefaultHttpClient(params);
+    HttpGet httpGet = new HttpGet(url);
+    // UrlEncodedFormEntity ue = new UrlEncodedFormEntity(vals, "UTF-8");
+    // httpGet.setEntity(ue);
+    HttpResponse response = httpclient.execute(httpGet);
+    HttpEntity resEntity = response.getEntity();
+  
+    if (resEntity != null)
+    {
+      BufferedReader r = new BufferedReader(
+              new InputStreamReader(resEntity.getContent()));
+      return r;
+    }
+    else
+    {
+      return null;
+    }
+  }
 }
index 8877c34..47e66ac 100644 (file)
@@ -35,6 +35,11 @@ import com.stevesoft.pat.Regex;
  */
 abstract public class Pfam extends Xfam
 {
+  /*
+   * append to URLs to retrieve as a gzipped file
+   */
+  protected static final String GZIPPED = "/gzipped";
+
   static final String PFAM_BASEURL_KEY = "PFAM_BASEURL";
 
   private static final String DEFAULT_PFAM_BASEURL = "https://pfam.xfam.org";
index 0600427..d71892b 100644 (file)
@@ -34,7 +34,7 @@ public class PfamFull extends Pfam
   @Override
   public String getURLSuffix()
   {
-    return "/alignment/full";
+    return "/alignment/full" + GZIPPED;
   }
 
   /*
index dff8a17..f64d07f 100644 (file)
@@ -36,7 +36,7 @@ public class PfamSeed extends Pfam
   @Override
   public String getURLSuffix()
   {
-    return "/alignment/seed";
+    return "/alignment/seed" + GZIPPED;
   }
 
   /*
index 1d9d99a..c9ee7fc 100644 (file)
@@ -36,6 +36,11 @@ abstract public class Rfam extends Xfam
 
   private static final String DEFAULT_RFAM_BASEURL = "https://rfam.xfam.org";
 
+  /*
+   * append to URLs to retrieve as a gzipped file
+   */
+  protected static final String GZIPPED = "?gzip=1&download=1";
+
   @Override
   protected String getURLPrefix()
   {
index d815336..396511c 100644 (file)
@@ -36,7 +36,7 @@ public class RfamFull extends Rfam
   @Override
   public String getURLSuffix()
   {
-    return "/alignment/full";
+    return "/alignment/full" + GZIPPED;
   }
 
   /*
index a74e829..eaa574b 100644 (file)
@@ -36,8 +36,7 @@ public class RfamSeed extends Rfam
   @Override
   public String getURLSuffix()
   {
-    // to download gzipped file add '?gzip=1'
-    return "/alignment/stockholm";
+    return "/alignment/stockholm" + GZIPPED;
   }
 
   /*
index b83f558..f0cb14b 100644 (file)
@@ -36,7 +36,6 @@ import jalview.ws.seqfetcher.DbSourceProxyImpl;
  */
 public abstract class Xfam extends DbSourceProxyImpl
 {
-
   public Xfam()
   {
     super();
index d065666..4fb9ca9 100644 (file)
  */
 package jalview.ws.sifts;
 
-import jalview.analysis.AlignSeq;
-import jalview.analysis.scoremodels.ScoreMatrix;
-import jalview.analysis.scoremodels.ScoreModels;
-import jalview.api.DBRefEntryI;
-import jalview.api.SiftsClientI;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.DBRefSource;
-import jalview.datamodel.SequenceI;
-import jalview.io.StructureFile;
-import jalview.schemes.ResidueProperties;
-import jalview.structure.StructureMapping;
-import jalview.util.Comparison;
-import jalview.util.DBRefUtils;
-import jalview.util.Format;
-import jalview.xml.binding.sifts.Entry;
-import jalview.xml.binding.sifts.Entry.Entity;
-import jalview.xml.binding.sifts.Entry.Entity.Segment;
-import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
-import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
-import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
-import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.ResidueDetail;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -67,10 +45,33 @@ import java.util.TreeMap;
 import java.util.zip.GZIPInputStream;
 
 import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
 import javax.xml.bind.Unmarshaller;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamReader;
 
+import jalview.analysis.AlignSeq;
+import jalview.analysis.scoremodels.ScoreMatrix;
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.api.DBRefEntryI;
+import jalview.api.SiftsClientI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.SequenceI;
+import jalview.io.StructureFile;
+import jalview.schemes.ResidueProperties;
+import jalview.structure.StructureMapping;
+import jalview.util.Comparison;
+import jalview.util.DBRefUtils;
+import jalview.util.Format;
+import jalview.util.Platform;
+import jalview.xml.binding.sifts.Entry;
+import jalview.xml.binding.sifts.Entry.Entity;
+import jalview.xml.binding.sifts.Entry.Entity.Segment;
+import jalview.xml.binding.sifts.Entry.Entity.Segment.ListMapRegion.MapRegion;
+import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue;
+import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.CrossRefDb;
+import jalview.xml.binding.sifts.Entry.Entity.Segment.ListResidue.Residue.ResidueDetail;
 import mc_view.Atom;
 import mc_view.PDBChain;
 
@@ -187,7 +188,8 @@ public class SiftsClient implements SiftsClientI
       XMLStreamReader streamReader = XMLInputFactory.newInstance()
               .createXMLStreamReader(gzis);
       Unmarshaller um = jc.createUnmarshaller();
-      return (Entry) um.unmarshal(streamReader);
+      JAXBElement<Entry> jbe = um.unmarshal(streamReader, Entry.class);
+      return jbe.getValue();
     } catch (Exception e)
     {
       e.printStackTrace();
@@ -299,21 +301,36 @@ public class SiftsClient implements SiftsClientI
     }
     String siftFile = pdbId + ".xml.gz";
     String siftsFileFTPURL = SIFTS_FTP_BASE_URL + siftFile;
-    String downloadedSiftsFile = SiftsSettings.getSiftDownloadDirectory()
-            + siftFile;
-    File siftsDownloadDir = new File(
-            SiftsSettings.getSiftDownloadDirectory());
-    if (!siftsDownloadDir.exists())
+    
+    /*
+     * Download the file from URL to either
+     * Java: directory of cached downloaded SIFTS files
+     * Javascript: temporary 'file' (in-memory cache)
+     */
+    File downloadTo = null;
+    if (Platform.isJS())
+    {
+      downloadTo = File.createTempFile(siftFile, ".xml.gz");
+    }
+    else
     {
-      siftsDownloadDir.mkdirs();
+      downloadTo = new File(
+              SiftsSettings.getSiftDownloadDirectory() + siftFile);
+      File siftsDownloadDir = new File(
+              SiftsSettings.getSiftDownloadDirectory());
+      if (!siftsDownloadDir.exists())
+      {
+        siftsDownloadDir.mkdirs();
+      }
     }
+
     // System.out.println(">> Download ftp url : " + siftsFileFTPURL);
     // long now = System.currentTimeMillis();
     URL url = new URL(siftsFileFTPURL);
     URLConnection conn = url.openConnection();
     InputStream inputStream = conn.getInputStream();
     FileOutputStream outputStream = new FileOutputStream(
-            downloadedSiftsFile);
+            downloadTo);
     byte[] buffer = new byte[BUFFER_SIZE];
     int bytesRead = -1;
     while ((bytesRead = inputStream.read(buffer)) != -1)
@@ -324,7 +341,7 @@ public class SiftsClient implements SiftsClientI
     inputStream.close();
     // System.out.println(">>> File downloaded : " + downloadedSiftsFile
     // + " took " + (System.currentTimeMillis() - now) + "ms");
-    return new File(downloadedSiftsFile);
+    return downloadTo;
   }
 
   /**
index fa30021..3ae81d2 100644 (file)
@@ -201,16 +201,16 @@ public class AppletPDBViewer extends EmbmenuFrame
     taylor.setLabel(MessageManager.getString("label.colourScheme_taylor"));
     taylor.addActionListener(this);
     helix.setLabel(MessageManager
-            .getString("label.colourScheme_helix_propensity"));
+            .getString("label.colourScheme_helixpropensity"));
     helix.addActionListener(this);
     strand.setLabel(MessageManager
-            .getString("label.colourScheme_strand_propensity"));
+            .getString("label.colourScheme_strandpropensity"));
     strand.addActionListener(this);
     turn.setLabel(
-            MessageManager.getString("label.colourScheme_turn_propensity"));
+            MessageManager.getString("label.colourScheme_turnpropensity"));
     turn.addActionListener(this);
     buried.setLabel(
-            MessageManager.getString("label.colourScheme_buried_index"));
+            MessageManager.getString("label.colourScheme_buriedindex"));
     buried.addActionListener(this);
     user.setLabel(MessageManager.getString("action.user_defined"));
     user.addActionListener(this);
index 349b5d1..a302d6e 100644 (file)
@@ -25,14 +25,14 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
-import jalview.gui.JvOptionPane;
-
 import java.util.BitSet;
 
 import org.junit.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import jalview.gui.JvOptionPane;
+
 public class SearchResultsTest
 {
 
@@ -303,4 +303,68 @@ public class SearchResultsTest
     sr.addResult(seq1, 3, 6);
     assertEquals(2, sr.getSize());
   }
+
+  /**
+   * Test for method that checks if search results matches a sequence region
+   */
+  @Test(groups = { "Functional" })
+  public void testInvolvesSequence()
+  {
+    SequenceI dataset = new Sequence("genome", "ATGGCCCTTTAAGCAACATTT");
+    // first 'exon':
+    SequenceI cds1 = new Sequence("cds1/1-12", "ATGGCCCTTTAA");
+    cds1.setDatasetSequence(dataset);
+    // overlapping second 'exon':
+    SequenceI cds2 = new Sequence("cds2/7-18", "CTTTAAGCAACA");
+    cds2.setDatasetSequence(dataset);
+    // unrelated sequence
+    SequenceI cds3 = new Sequence("cds3", "ATGGCCCTTTAAGCAACA");
+
+    SearchResults sr = new SearchResults();
+    assertFalse(sr.involvesSequence(cds1));
+
+    /*
+     * cds1 and cds2 share the same dataset sequence, but
+     * only cds1 overlaps match 4:6 (fixes bug JAL-3613)
+     */
+    sr.addResult(dataset, 4, 6);
+    assertTrue(sr.involvesSequence(cds1));
+    assertFalse(sr.involvesSequence(cds2));
+    assertFalse(sr.involvesSequence(cds3));
+
+    /*
+     * search results overlap cds2 only
+     */
+    sr = new SearchResults();
+    sr.addResult(dataset, 18, 18);
+    assertFalse(sr.involvesSequence(cds1));
+    assertTrue(sr.involvesSequence(cds2));
+
+    /*
+     * add a search result overlapping cds1
+     */
+    sr.addResult(dataset, 1, 1);
+    assertTrue(sr.involvesSequence(cds1));
+    assertTrue(sr.involvesSequence(cds2));
+
+    /*
+     * single search result overlapping both
+     */
+    sr = new SearchResults();
+    sr.addResult(dataset, 10, 12);
+    assertTrue(sr.involvesSequence(cds1));
+    assertTrue(sr.involvesSequence(cds2));
+
+    /*
+     * search results matching aligned sequence
+     */
+    sr = new SearchResults();
+    sr.addResult(cds1, 10, 12);
+    assertTrue(sr.involvesSequence(cds1));
+    assertFalse(sr.involvesSequence(cds2));
+    sr.addResult(cds2, 1, 3); // no start-end overlap
+    assertFalse(sr.involvesSequence(cds2));
+    sr.addResult(cds2, 7, 9); // start-end overlap
+    assertTrue(sr.involvesSequence(cds2));
+  }
 }
index cd8f9eb..f8479a3 100644 (file)
@@ -26,11 +26,15 @@ import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
-import jalview.gui.JvOptionPane;
+import java.util.ArrayList;
+import java.util.List;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import jalview.gui.JvOptionPane;
+import jalview.util.MapList;
+
 public class SequenceFeatureTest
 {
 
@@ -285,7 +289,7 @@ public class SequenceFeatureTest
     String expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>22</td></tr>"
             + "<tr><td>Type</td><td>variant</td><td></td></tr>"
             + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport(seqName));
+    assertEquals(expected, sf.getDetailsReport(seqName, null));
 
     // contact feature
     sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
@@ -293,7 +297,7 @@ public class SequenceFeatureTest
     expected = "<br><table><tr><td>Location</td><td>TestSeq</td><td>28:31</td></tr>"
             + "<tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
             + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport(seqName));
+    assertEquals(expected, sf.getDetailsReport(seqName, null));
 
     sf = new SequenceFeature("variant", "G,C", 22, 33,
             12.5f, "group");
@@ -306,7 +310,7 @@ public class SequenceFeatureTest
             + "<tr><td>Group</td><td>group</td><td></td></tr>"
             + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
             + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport(seqName));
+    assertEquals(expected, sf.getDetailsReport(seqName, null));
 
     /*
      * feature with embedded html link in description
@@ -317,6 +321,39 @@ public class SequenceFeatureTest
             + "<tr><td>Type</td><td>Pfam</td><td></td></tr>"
             + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
             + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
-    assertEquals(expected, sf.getDetailsReport(seqName));
+    assertEquals(expected, sf.getDetailsReport(seqName, null));
+  }
+
+  /**
+   * Feature details report for a virtual feature should include original and
+   * mapped locations, and also derived peptide consequence if it can be
+   * determined
+   */
+  @Test(groups = { "Functional" })
+  public void testGetDetailsReport_virtualFeature()
+  {
+    SequenceI cds = new Sequence("Cds/101-121", "CCTttgAGAtttCAAatgGAT");
+    SequenceI seq = new Sequence("TestSeq/8-14", "PLRFQMD");
+    MapList map = new MapList(new int[] { 101, 118 }, new int[] { 8, 13 },
+            3, 1);
+    Mapping mapping = new Mapping(seq, map);
+    List<SequenceFeature> features = new ArrayList<>();
+    // vary ttg (Leu) to ttc (Phe)
+    SequenceFeature sf = new SequenceFeature("variant", "G,C", 106, 106,
+            null);
+    sf.setValue("alleles", "G,C"); // needed to compute peptide consequence!
+    features.add(sf);
+
+    MappedFeatures mf = new MappedFeatures(mapping, cds, 9, 'L', features);
+
+    String expected = "<br><table><tr><td>Location</td><td>Cds</td><td>106</td></tr>"
+            + "<tr><td>Peptide Location</td><td>TestSeq</td><td>9</td></tr>"
+            + "<tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
+            + "<tr><td>Consequence</td><td><i>Translated by Jalview</i></td><td>p.Leu9Phe</td></tr>"
+            + "<tr><td>alleles</td><td></td><td>G,C</td></tr>"
+            + "</table>";
+
+    assertEquals(expected, sf.getDetailsReport(seq.getName(), mf));
   }
 }
index 4bd34cb..f403a57 100644 (file)
@@ -6,14 +6,17 @@ import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
-import jalview.datamodel.SequenceFeature;
-import jalview.util.MessageManager;
-import jalview.util.matcher.Condition;
-
 import java.util.Locale;
 
 import org.testng.annotations.Test;
 
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+import junit.extensions.PA;
+
 public class FeatureMatcherTest
 {
   @Test(groups = "Functional")
@@ -222,7 +225,7 @@ public class FeatureMatcherTest
     FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f",
             "AF");
     assertEquals(fm.getMatcher().getCondition(), Condition.GE);
-    assertEquals(fm.getMatcher().getFloatValue(), -2F);
+    assertEquals(PA.getValue(fm.getMatcher(), "floatValue"), -2F);
     assertEquals(fm.getMatcher().getPattern(), "-2.0");
   }
 
@@ -233,71 +236,88 @@ public class FeatureMatcherTest
     assertFalse(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertEquals(fm.getAttribute(), new String[] { "AF" });
-    assertSame(Condition.LT, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
-    assertEquals(fm.getMatcher().getPattern(), "1.2");
+    MatcherI matcher = fm.getMatcher();
+    assertSame(Condition.LT, matcher.getCondition());
+    assertEquals(PA.getValue(matcher, "floatValue"), 1.2f);
+    assertSame(PA.getValue(matcher, "patternType"),
+            Matcher.PatternType.Float);
+    assertEquals(matcher.getPattern(), "1.2");
 
     // quotes are optional, condition is not case sensitive
     fm = FeatureMatcher.fromString("AF lt '1.2'");
+    matcher = fm.getMatcher();
     assertFalse(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertEquals(fm.getAttribute(), new String[] { "AF" });
-    assertSame(Condition.LT, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
-    assertEquals(fm.getMatcher().getPattern(), "1.2");
+    assertSame(Condition.LT, matcher.getCondition());
+    assertEquals(PA.getValue(matcher, "floatValue"), 1.2F);
+    assertEquals(matcher.getPattern(), "1.2");
 
     fm = FeatureMatcher.fromString("'AF' Present");
+    matcher = fm.getMatcher();
     assertFalse(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertEquals(fm.getAttribute(), new String[] { "AF" });
-    assertSame(Condition.Present, fm.getMatcher().getCondition());
+    assertSame(Condition.Present, matcher.getCondition());
+    assertSame(PA.getValue(matcher, "patternType"),
+            Matcher.PatternType.String);
 
     fm = FeatureMatcher.fromString("CSQ:Consequence contains damaging");
+    matcher = fm.getMatcher();
     assertFalse(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
-    assertSame(Condition.Contains, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "damaging");
+    assertSame(Condition.Contains, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "damaging");
 
     // keyword Label is not case sensitive
     fm = FeatureMatcher.fromString("LABEL Matches 'foobar'");
+    matcher = fm.getMatcher();
     assertTrue(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertNull(fm.getAttribute());
-    assertSame(Condition.Matches, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "foobar");
+    assertSame(Condition.Matches, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "foobar");
 
     fm = FeatureMatcher.fromString("'Label' matches 'foo bar'");
+    matcher = fm.getMatcher();
     assertTrue(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertNull(fm.getAttribute());
-    assertSame(Condition.Matches, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+    assertSame(Condition.Matches, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "foo bar");
 
     // quotes optional on pattern
     fm = FeatureMatcher.fromString("'Label' matches foo bar");
+    matcher = fm.getMatcher();
     assertTrue(fm.isByLabel());
     assertFalse(fm.isByScore());
     assertNull(fm.getAttribute());
-    assertSame(Condition.Matches, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+    assertSame(Condition.Matches, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "foo bar");
 
-    fm = FeatureMatcher.fromString("Score GE 12.2");
+    // integer condition
+    fm = FeatureMatcher.fromString("Score GE 12");
+    matcher = fm.getMatcher();
     assertFalse(fm.isByLabel());
     assertTrue(fm.isByScore());
     assertNull(fm.getAttribute());
-    assertSame(Condition.GE, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "12.2");
-    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+    assertSame(Condition.GE, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "12");
+    assertEquals(PA.getValue(matcher, "floatValue"), 0f);
+    assertEquals(PA.getValue(matcher, "longValue"), 12L);
+    assertSame(PA.getValue(matcher, "patternType"),
+            Matcher.PatternType.Integer);
 
     // keyword Score is not case sensitive
     fm = FeatureMatcher.fromString("'SCORE' ge '12.2'");
+    matcher = fm.getMatcher();
     assertFalse(fm.isByLabel());
     assertTrue(fm.isByScore());
     assertNull(fm.getAttribute());
-    assertSame(Condition.GE, fm.getMatcher().getCondition());
-    assertEquals(fm.getMatcher().getPattern(), "12.2");
-    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+    assertSame(Condition.GE, matcher.getCondition());
+    assertEquals(matcher.getPattern(), "12.2");
+    assertEquals(PA.getValue(matcher, "floatValue"), 12.2F);
 
     // invalid numeric pattern
     assertNull(FeatureMatcher.fromString("Score eq twelve"));
index 4198a37..56512cd 100644 (file)
@@ -5,8 +5,6 @@ import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
-import jalview.datamodel.SequenceFeature;
-
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -15,6 +13,7 @@ import java.util.Set;
 
 import org.testng.annotations.Test;
 
+import jalview.datamodel.SequenceFeature;
 import junit.extensions.PA;
 
 public class SequenceFeaturesTest
@@ -1052,7 +1051,8 @@ public class SequenceFeaturesTest
   public void testSortFeatures()
   {
     List<SequenceFeature> sfs = new ArrayList<>();
-    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30, 80,
+    SequenceFeature sf1 = new SequenceFeature("Pfam", "desc", 30,
+            60,
             Float.NaN, null);
     sfs.add(sf1);
     SequenceFeature sf2 = new SequenceFeature("Rfam", "desc", 40, 50,
@@ -1061,18 +1061,34 @@ public class SequenceFeaturesTest
     SequenceFeature sf3 = new SequenceFeature("Rfam", "desc", 50, 60,
             Float.NaN, null);
     sfs.add(sf3);
+    SequenceFeature sf4 = new SequenceFeature("Xfam", "desc", 30,
+            80,
+            Float.NaN, null);
+    sfs.add(sf4);
+    SequenceFeature sf5 = new SequenceFeature("Xfam", "desc", 30,
+            90,
+            Float.NaN, null);
+    sfs.add(sf5);
 
-    // sort by end position descending
+    /*
+     * sort by end position descending, order unchanged if matched
+     */
     SequenceFeatures.sortFeatures(sfs, false);
-    assertSame(sfs.get(0), sf1);
-    assertSame(sfs.get(1), sf3);
-    assertSame(sfs.get(2), sf2);
+    assertSame(sfs.get(0), sf5); // end 90
+    assertSame(sfs.get(1), sf4); // end 80
+    assertSame(sfs.get(2), sf1); // end 60, start 50
+    assertSame(sfs.get(3), sf3); // end 60, start 30
+    assertSame(sfs.get(4), sf2); // end 50
 
-    // sort by start position ascending
+    /*
+     * resort {5, 4, 1, 3, 2} by start position ascending, end descending
+     */
     SequenceFeatures.sortFeatures(sfs, true);
-    assertSame(sfs.get(0), sf1);
-    assertSame(sfs.get(1), sf2);
-    assertSame(sfs.get(2), sf3);
+    assertSame(sfs.get(0), sf5); // start 30, end 90
+    assertSame(sfs.get(1), sf4); // start 30, end 80
+    assertSame(sfs.get(2), sf1); // start 30, end 60
+    assertSame(sfs.get(3), sf2); // start 40
+    assertSame(sfs.get(4), sf3); // start 50
   }
 
   @Test(groups = "Functional")
index e42b54f..21f7e19 100644 (file)
@@ -23,50 +23,36 @@ package jalview.ext.jmol;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
-import jalview.gui.JvOptionPane;
 import jalview.gui.SequenceRenderer;
 import jalview.schemes.JalviewColourScheme;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
 import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
 import jalview.structure.StructureSelectionManager;
 
-import java.util.HashMap;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
 public class JmolCommandsTest
 {
+  private JmolCommands testee;
 
   @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
-  @Test(groups = { "Functional" })
-  public void testGetColourBySequenceCommand_noFeatures()
+  public void setUp()
   {
-    SequenceI seq1 = new Sequence("seq1", "MHRSQTRALK");
-    SequenceI seq2 = new Sequence("seq2", "MRLEITQSGD");
-    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame af = new AlignFrame(al, 800, 500);
-    SequenceRenderer sr = new SequenceRenderer(af.getViewport());
-    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
-    String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
-    StructureSelectionManager ssm = new StructureSelectionManager();
-
-    // need some mappings!
-
-    StructureMappingcommandSet[] commands = JmolCommands
-            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+    testee = new JmolCommands();
   }
 
   @Test(groups = { "Functional" })
@@ -91,11 +77,11 @@ public class JmolCommandsTest
     SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
     String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
     StructureSelectionManager ssm = new StructureSelectionManager();
-  
+
     /*
      * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
      */
-    HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
+    HashMap<Integer, int[]> map = new HashMap<>();
     for (int pos = 1; pos <= seq1.getLength(); pos++)
     {
       map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
@@ -106,37 +92,195 @@ public class JmolCommandsTest
     StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
             "B", map, null);
     ssm.addStructureMapping(sm2);
-  
-    StructureMappingcommandSet[] commands = JmolCommands
-            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
+
+    String[] commands = testee.colourBySequence(ssm,
+            files,
+            seqs, sr, af.alignPanel);
     assertEquals(commands.length, 2);
-    assertEquals(commands[0].commands.length, 1);
 
-    String chainACommand = commands[0].commands[0];
+    String chainACommand = commands[0];
     // M colour is #82827d == (130, 130, 125) (see strand.html help page)
-    assertTrue(chainACommand
-            .contains("select 21:A/1.1;color[130,130,125]")); // first one
+    assertTrue(
+            chainACommand.contains("select 21:A/1.1;color[130,130,125]")); // first
+                                                                           // one
     // H colour is #60609f == (96, 96, 159)
     assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]"));
     // hidden columns are Gray (128, 128, 128)
     assertTrue(chainACommand
             .contains(";select 23-25:A/1.1;color[128,128,128]"));
     // S and G are both coloured #4949b6 == (73, 73, 182)
-    assertTrue(chainACommand
-            .contains(";select 26-30:A/1.1;color[73,73,182]"));
+    assertTrue(
+            chainACommand.contains(";select 26-30:A/1.1;color[73,73,182]"));
 
-    String chainBCommand = commands[1].commands[0];
+    String chainBCommand = commands[1];
     // M colour is #82827d == (130, 130, 125)
-    assertTrue(chainBCommand
-            .contains("select 21:B/2.1;color[130,130,125]"));
+    assertTrue(
+            chainBCommand.contains("select 21:B/2.1;color[130,130,125]"));
     // V colour is #ffff00 == (255, 255, 0)
-    assertTrue(chainBCommand
-.contains(";select 22:B/2.1;color[255,255,0]"));
+    assertTrue(chainBCommand.contains(";select 22:B/2.1;color[255,255,0]"));
     // hidden columns are Gray (128, 128, 128)
     assertTrue(chainBCommand
             .contains(";select 23-25:B/2.1;color[128,128,128]"));
     // S and G are both coloured #4949b6 == (73, 73, 182)
-    assertTrue(chainBCommand
-            .contains(";select 26-30:B/2.1;color[73,73,182]"));
+    assertTrue(
+            chainBCommand.contains(";select 26-30:B/2.1;color[73,73,182]"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "2-4:A/1.1|8:A/1.1");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-4:A/1.1|8:A/1.1|5-7:B/1.1");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1");
+    model.addRange("2", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1");
+    model.addRange("2", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-7:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|5-9:C/2.1");
+    model.addRange("2", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "2-5:A/1.1|8:A/1.1|5-10:B/1.1|1-4:B/2.1|3-10:C/2.1|25-35:/5.1");
+
+  }
+
+  @Test(groups = { "Functional" })
+  public void testColourBySequence()
+  {
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A");
+    JmolCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B");
+    JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A");
+    JmolCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A");
+    JmolCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A");
+
+    // Colours should appear in the Jmol command in the order in which
+    // they were added; within colour, by model, by chain, ranges in start order
+    List<StructureCommandI> commands = testee.colourBySequence(map);
+    assertEquals(commands.size(), 1);
+    String expected1 = "select 2-5:A/1.1|9-23:A/1.1|7:B/1.1|1:A/2.1|4-7:B/2.1;color[0,0,255]";
+    String expected2 = "select 3-5:A/2.1|8:A/2.1;color[255,255,0]";
+    String expected3 = "select 3-9:A/1.1;color[255,0,0]";
+    assertEquals(commands.get(0).getCommand(),
+            expected1 + ";" + expected2 + ";" + expected3);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange("1", 12, 14, "A");
+    ref.addRange("1", 18, 18, "B");
+    ref.addRange("1", 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> command = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(command.size(), 1);
+    String refSpec = "12-14:A/1.1|18:B/1.1|22-23:B/1.1";
+    String toAlignSpec = "15-17:B/2.1|20-21:B/2.1|22:C/2.1";
+    String expected = String.format(
+            "compare {2.1} {1.1} SUBSET {(*.CA | *.P) and conformation=1} ATOMS {%s}{%s} ROTATE TRANSLATE ;select %s|%s;cartoons",
+            toAlignSpec, refSpec, toAlignSpec, refSpec);
+    assertEquals(command.get(0).getCommand(), expected);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModelStartNo()
+  {
+    assertEquals(testee.getModelStartNo(), 1);
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByChain()
+  {
+    StructureCommandI cmd = testee.colourByChain();
+    assertEquals(cmd.getCommand(), "select *;color chain");
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByCharge()
+  {
+    List<StructureCommandI> cmds = testee.colourByCharge();
+    assertEquals(cmds.size(), 1);
+    assertEquals(cmds.get(0).getCommand(),
+            "select *;color white;select ASP,GLU;color red;"
+                    + "select LYS,ARG;color blue;select CYS;color yellow");
+  }
+
+  @Test(groups = "Functional")
+  public void testSetBackgroundColour()
+  {
+    StructureCommandI cmd = testee.setBackgroundColour(Color.PINK);
+    assertEquals(cmd.getCommand(), "background [255,175,175]");
+  }
+
+  @Test(groups = "Functional")
+  public void testFocusView()
+  {
+    StructureCommandI cmd = testee.focusView();
+    assertEquals(cmd.getCommand(), "zoom 0");
+  }
+
+  @Test(groups = "Functional")
+  public void testSaveSession()
+  {
+    StructureCommandI cmd = testee.saveSession("/some/filepath");
+    assertEquals(cmd.getCommand(), "write STATE \"/some/filepath\"");
+  }
+
+  @Test(groups = "Functional")
+  public void testShowBackbone()
+  {
+    List<StructureCommandI> cmds = testee.showBackbone();
+    assertEquals(cmds.size(), 1);
+    assertEquals(cmds.get(0).getCommand(),
+            "select *; cartoons off; backbone");
+  }
+
+  @Test(groups = "Functional")
+  public void testLoadFile()
+  {
+    StructureCommandI cmd = testee.loadFile("/some/filepath");
+    assertEquals(cmd.getCommand(), "load FILES \"/some/filepath\"");
+
+    // single backslash gets escaped to double
+    cmd = testee.loadFile("\\some\\filepath");
+    assertEquals(cmd.getCommand(), "load FILES \"\\\\some\\\\filepath\"");
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenSession()
+  {
+    StructureCommandI cmd = testee.openSession("/some/filepath");
+    assertEquals(cmd.getCommand(), "load FILES \"/some/filepath\"");
+
+    // single backslash gets escaped to double
+    cmd = testee.openSession("\\some\\filepath");
+    assertEquals(cmd.getCommand(), "load FILES \"\\\\some\\\\filepath\"");
   }
 }
diff --git a/test/jalview/ext/pymol/PymolCommandsTest.java b/test/jalview/ext/pymol/PymolCommandsTest.java
new file mode 100644 (file)
index 0000000..f6bad92
--- /dev/null
@@ -0,0 +1,350 @@
+/*
+ * 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.ext.pymol;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import jalview.ext.rbvi.chimera.ChimeraCommands;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+
+public class PymolCommandsTest
+{
+  private PymolCommands testee;
+
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    testee = new PymolCommands();
+  }
+
+  @Test(groups = { "Functional" })
+  public void testColourBySequence()
+  {
+
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A");
+    PymolCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B");
+    PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A");
+    PymolCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A");
+    PymolCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A");
+
+    // Colours should appear in the Pymol command in the order in which
+    // they were added; within colour, by model, by chain, ranges in start order
+    List<StructureCommandI> commands = testee.colourBySequence(map);
+    assertEquals(commands.size(), 3);
+    assertEquals(commands.get(0), new StructureCommand("color", "0x0000ff",
+            "0//A/2-5+9-23/ 0//B/7/ 1//A/1/ 1//B/4-7/"));
+    assertEquals(commands.get(
+            1),
+            new StructureCommand("color", "0xffff00", "1//A/3-5+8/"));
+    assertEquals(commands.get(
+            2),
+            new StructureCommand("color", "0xff0000", "0//A/3-9/"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4/");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-4+8/ 1//B/5-7/");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false), "1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-7/");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/5-9/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "0//B/1-4/ 0//C/3-10/ 1//A/2-5+8/ 1//B/5-10/ 5///25-35/");
+
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange("1", 12, 14, "A");
+    ref.addRange("1", 18, 18, "B");
+    ref.addRange("1", 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> commands = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(commands.size(), 2);
+    String refSpecCA = "1//A/12-14/CA 1//B/18+22-23/CA";
+    String toAlignSpecCA = "2//B/15-17+20-21/CA 2//C/22/CA";
+    String refSpec = "1//A/12-14/ 1//B/18+22-23/";
+    String toAlignSpec = "2//B/15-17+20-21/ 2//C/22/";
+
+    // super command: separate arguments for regions to align
+    assertEquals(commands.get(0),
+            new StructureCommand("super", refSpecCA, toAlignSpecCA));
+    // show aligned regions: one argument for combined atom specs
+    assertEquals(commands.get(1), new StructureCommand("show", "cartoon",
+            refSpec + " " + toAlignSpec));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec_alphaOnly()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, true), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, true), "1//A/2-4/CA");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, true), "1//A/2-4+8/CA");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "1//A/2-4+8/CA 1//B/5-7/CA");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-7/CA");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/5-9/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, true),
+            "0//B/1-4/CA 0//C/3-10/CA 1//A/2-5+8/CA 1//B/5-10/CA 5///25-35/CA");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModelStartNo()
+  {
+    assertEquals(testee.getModelStartNo(), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResidueSpec()
+  {
+    assertEquals(testee.getResidueSpec("ALA"), "resn ALA");
+  }
+
+  @Test(groups = "Functional")
+  public void testShowBackbone()
+  {
+    List<StructureCommandI> cmds = testee.showBackbone();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("hide", "everything"));
+    assertEquals(cmds.get(1), new StructureCommand("show", "ribbon"));
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByCharge()
+  {
+    List<StructureCommandI> cmds = testee.colourByCharge();
+    assertEquals(cmds.size(), 4);
+    assertEquals(cmds.get(0), new StructureCommand("color", "white", "*"));
+    assertEquals(cmds.get(1),
+            new StructureCommand("color", "red", "resn ASP resn GLU"));
+    assertEquals(cmds.get(2),
+            new StructureCommand("color", "blue", "resn LYS resn ARG"));
+    assertEquals(cmds.get(3),
+            new StructureCommand("color", "yellow", "resn CYS"));
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenCommandFile()
+  {
+    assertEquals(testee.openCommandFile("commands.pml"),
+            new StructureCommand("run", "commands.pml"));
+  }
+
+  @Test(groups = "Functional")
+  public void testSaveSession()
+  {
+    assertEquals(testee.saveSession("somewhere.pse"),
+            new StructureCommand("save", "somewhere.pse"));
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenSession()
+  {
+    assertEquals(testee.openSession("/some/path"),
+            new StructureCommand("load", "/some/path", "", "0", "pse"));
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByChain()
+  {
+    assertEquals(testee.colourByChain(),
+            new StructureCommand("spectrum", "chain"));
+  }
+
+  @Test(groups = "Functional")
+  public void testColourResidues()
+  {
+    assertEquals(testee.colourResidues("something",
+            Color.MAGENTA),
+            new StructureCommand("color", "0xff00ff", "something"));
+  }
+
+  @Test(groups = "Functional")
+  public void testLoadFile()
+  {
+    assertEquals(testee.loadFile("/some/path"),
+            new StructureCommand("load", "/some/path"));
+  }
+
+  @Test(groups = "Functional")
+  public void testSetBackgroundColour()
+  {
+    assertEquals(testee.setBackgroundColour(
+            Color.PINK),
+            new StructureCommand("bg_color", "0xffafaf"));
+  }
+
+  @Test(groups = "Functional")
+  public void testSetAttribute()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    model.addRange("1", 89, 92, "A");
+    model.addRange("2", 12, 20, "B");
+    model.addRange("2", 8, 9, "B");
+    assertEquals(testee.setAttribute("jv_kd", "27.3", model),
+            new StructureCommand("iterate", "1//A/89-92/ 2//B/8-9+12-20/",
+                    "p.jv_kd='27.3'"));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetAttributes()
+  {
+    /*
+     * make a map of { featureType, {featureValue, {residue range specification } } }
+     */
+    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+    Map<Object, AtomSpecModel> featureValues = new HashMap<>();
+
+    /*
+     * start with just one feature/value...
+     */
+    featuresMap.put("chain", featureValues);
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A");
+
+    List<StructureCommandI> commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+
+    /*
+     * feature name gets a jv_ namespace prefix
+     */
+    assertEquals(commands.get(0), new StructureCommand("iterate",
+            "0//A/8-20/", "p.jv_chain='X'"));
+
+    // add same feature value, overlapping range
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A");
+    // same feature value, contiguous range
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    assertEquals(commands.get(0), new StructureCommand("iterate",
+            "0//A/3-25/", "p.jv_chain='X'"));
+
+    // same feature value and model, different chain
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B");
+    // same feature value and chain, different model
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    StructureCommand expected1 = new StructureCommand("iterate",
+            "0//A/3-25/ 0//B/21-25/ 1//A/26-30/", "p.jv_chain='X'");
+    assertEquals(commands.get(0), expected1);
+
+    // same feature, different value
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(2, commands.size());
+    // commands are ordered by feature type but not by value
+    // so test for the expected command in either order
+    StructureCommandI cmd1 = commands.get(0);
+    StructureCommandI cmd2 = commands.get(1);
+    StructureCommand expected2 = new StructureCommand("iterate",
+            "0//A/40-50/", "p.jv_chain='Y'");
+    assertTrue(cmd1.equals(expected1) || cmd2.equals(expected1));
+    // String expected2 = "setattr #0/A:40-50 res jv_chain 'Y' create true";
+    assertTrue(cmd1.equals(expected2) || cmd2.equals(expected2));
+
+    featuresMap.clear();
+    featureValues.clear();
+    featuresMap.put("side-chain binding!", featureValues);
+    ChimeraCommands.addAtomSpecRange(featureValues,
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", "0", 7, 15, "A");
+    // feature names are sanitised to change non-alphanumeric to underscore
+    // feature values are sanitised to encode single quote characters
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    StructureCommandI expected3 = new StructureCommand("iterate",
+            "0//A/7-15/",
+            "p.jv_side_chain_binding_='<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!'");
+    assertEquals(commands.get(0), expected3);
+  }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("quit"));
+  }
+}
diff --git a/test/jalview/ext/pymol/PymolManagerTest.java b/test/jalview/ext/pymol/PymolManagerTest.java
new file mode 100644 (file)
index 0000000..c415ace
--- /dev/null
@@ -0,0 +1,64 @@
+package jalview.ext.pymol;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+import jalview.structure.StructureCommand;
+
+public class PymolManagerTest
+{
+  @Test(groups = "Functional")
+  public void testGetPostRequest()
+  {
+    String req = PymolManager
+            .getPostRequest(new StructureCommand("foobar"));
+    assertEquals(req,
+            "<methodCall><methodName>foobar</methodName><params></params></methodCall>");
+
+    req = PymolManager
+            .getPostRequest(new StructureCommand("foobar", "blue", "all"));
+    assertEquals(req, "<methodCall><methodName>foobar</methodName><params>"
+            + "<parameter><value>blue</value></parameter>"
+            + "<parameter><value>all</value></parameter>"
+            + "</params></methodCall>");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetPymolPaths()
+  {
+    /*
+     * OSX
+     */
+    List<String> paths = PymolManager.getPymolPaths("Mac OS X");
+    assertEquals(paths.size(), 1);
+    assertTrue(
+            paths.contains("/Applications/PyMOL.app/Contents/MacOS/PyMOL"));
+
+    /*
+     * Linux
+     */
+    paths = PymolManager.getPymolPaths("Linux i386 1.5.0");
+    assertTrue(paths.contains("/usr/local/pymol/bin/PyMOL"));
+    assertTrue(paths.contains("/usr/local/bin/PyMOL"));
+    assertTrue(paths.contains("/usr/bin/PyMOL"));
+    assertTrue(paths.contains("/usr/local/pymol/bin/PyMOL"));
+    assertTrue(paths
+            .contains(System.getProperty("user.home") + "/opt/bin/PyMOL"));
+
+    /*
+     * Windows
+     */
+    paths = PymolManager.getPymolPaths("Windows 10");
+    assertTrue(paths.isEmpty()); // TODO - Windows paths
+
+    /*
+     * Other
+     */
+    paths = PymolManager.getPymolPaths("VAX/VMS");
+    assertTrue(paths.isEmpty());
+  }
+}
diff --git a/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java b/test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java
deleted file mode 100644 (file)
index 63d5e4e..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-package jalview.ext.rbvi.chimera;
-
-import static org.testng.Assert.assertEquals;
-
-import org.testng.annotations.Test;
-
-public class AtomSpecModelTest
-{
-  @Test(groups = "Functional")
-  public void testGetAtomSpec()
-  {
-    AtomSpecModel model = new AtomSpecModel();
-    assertEquals(model.getAtomSpec(), "");
-    model.addRange(1, 2, 4, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A");
-    model.addRange(1, 8, 8, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A");
-    model.addRange(1, 5, 7, "B");
-    assertEquals(model.getAtomSpec(), "#1:2-4.A,8.A,5-7.B");
-    model.addRange(1, 3, 5, "A");
-    assertEquals(model.getAtomSpec(), "#1:2-5.A,8.A,5-7.B");
-    model.addRange(0, 1, 4, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B|#1:2-5.A,8.A,5-7.B");
-    model.addRange(0, 5, 9, "C");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B");
-    model.addRange(1, 8, 10, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(1, 8, 9, "B");
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(0, 3, 10, "C"); // subsumes 5-9
-    assertEquals(model.getAtomSpec(), "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
-    model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology
-                                    // modelling
-    assertEquals(model.getAtomSpec(),
-            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
-
-  }
-
-}
index 06a09df..6880985 100644 (file)
@@ -23,19 +23,6 @@ package jalview.ext.rbvi.chimera;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
-import jalview.datamodel.Alignment;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.ColumnSelection;
-import jalview.datamodel.Sequence;
-import jalview.datamodel.SequenceI;
-import jalview.gui.AlignFrame;
-import jalview.gui.JvOptionPane;
-import jalview.gui.SequenceRenderer;
-import jalview.schemes.JalviewColourScheme;
-import jalview.structure.StructureMapping;
-import jalview.structure.StructureMappingcommandSet;
-import jalview.structure.StructureSelectionManager;
-
 import java.awt.Color;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -45,176 +32,365 @@ import java.util.Map;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+
 public class ChimeraCommandsTest
 {
+  private ChimeraCommands testee;
 
   @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+  public void setUp()
   {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+    testee = new ChimeraCommands();
   }
 
   @Test(groups = { "Functional" })
-  public void testBuildColourCommands()
+  public void testColourBySequence()
   {
 
-    Map<Object, AtomSpecModel> map = new LinkedHashMap<Object, AtomSpecModel>();
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 2, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 7, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 0, 9, 23, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 1, 1, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.blue, 1, 4, 7, "B");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 8, 8, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.yellow, 1, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 3, 5, "A");
-    ChimeraCommands.addAtomSpecRange(map, Color.red, 0, 6, 9, "A");
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "0", 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "1", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "0", 6, 9, "A");
 
     // Colours should appear in the Chimera command in the order in which
     // they were added; within colour, by model, by chain, ranges in start order
-    String command = ChimeraCommands.buildColourCommands(map).get(0);
-    assertEquals(
-            command,
-            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B; color #ffff00 #1:3-5.A,8.A; color #ff0000 #0:3-9.A");
+    List<StructureCommandI> commands = testee.colourBySequence(map);
+    assertEquals(commands.size(), 1);
+    assertEquals(commands.get(0).getCommand(),
+            "color #0000ff #0:2-5.A,9-23.A,7.B|#1:1.A,4-7.B;color #ffff00 #1:3-5.A,8.A;color #ff0000 #0:3-9.A");
   }
 
   @Test(groups = { "Functional" })
-  public void testBuildSetAttributeCommands()
+  public void testSetAttributes()
   {
     /*
      * make a map of { featureType, {featureValue, {residue range specification } } }
      */
-    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<String, Map<Object, AtomSpecModel>>();
-    Map<Object, AtomSpecModel> featureValues = new HashMap<Object, AtomSpecModel>();
-    
+    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+    Map<Object, AtomSpecModel> featureValues = new HashMap<>();
+
     /*
      * start with just one feature/value...
      */
     featuresMap.put("chain", featureValues);
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 8, 20, "A");
-  
-    List<String> commands = ChimeraCommands
-            .buildSetAttributeCommands(featuresMap);
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A");
+
+    List<StructureCommandI> commands = testee.setAttributes(featuresMap);
     assertEquals(1, commands.size());
 
     /*
      * feature name gets a jv_ namespace prefix
      * feature value is quoted in case it contains spaces
      */
-    assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:8-20.A");
+    assertEquals(commands.get(0).getCommand(),
+            "setattr res jv_chain 'X' #0:8-20.A");
 
     // add same feature value, overlapping range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 3, 9, "A");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A");
     // same feature value, contiguous range
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "A");
-    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A");
+    commands = testee.setAttributes(featuresMap);
     assertEquals(1, commands.size());
-    assertEquals(commands.get(0), "setattr r jv_chain 'X' #0:3-25.A");
+    assertEquals(commands.get(0).getCommand(),
+            "setattr res jv_chain 'X' #0:3-25.A");
 
     // same feature value and model, different chain
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 0, 21, 25, "B");
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B");
     // same feature value and chain, different model
-    ChimeraCommands.addAtomSpecRange(featureValues, "X", 1, 26, 30, "A");
-    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A");
+    commands = testee.setAttributes(featuresMap);
     assertEquals(1, commands.size());
-    assertEquals(commands.get(0),
-            "setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A");
+    String expected1 = "setattr res jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A";
+    assertEquals(commands.get(0).getCommand(), expected1);
 
     // same feature, different value
-    ChimeraCommands.addAtomSpecRange(featureValues, "Y", 0, 40, 50, "A");
-    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A");
+    commands = testee.setAttributes(featuresMap);
     assertEquals(2, commands.size());
     // commands are ordered by feature type but not by value
-    // so use contains to test for the expected command:
-    assertTrue(commands
-            .contains("setattr r jv_chain 'X' #0:3-25.A,21-25.B|#1:26-30.A"));
-    assertTrue(commands.contains("setattr r jv_chain 'Y' #0:40-50.A"));
+    // so test for the expected command in either order
+    String cmd1 = commands.get(0).getCommand();
+    String cmd2 = commands.get(1).getCommand();
+    assertTrue(cmd1.equals(expected1) || cmd2.equals(expected1));
+    String expected2 = "setattr res jv_chain 'Y' #0:40-50.A";
+    assertTrue(cmd1.equals(expected2) || cmd2.equals(expected2));
 
     featuresMap.clear();
     featureValues.clear();
     featuresMap.put("side-chain binding!", featureValues);
     ChimeraCommands.addAtomSpecRange(featureValues,
-            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", 0, 7, 15,
-            "A");
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", "0", 7, 15, "A");
     // feature names are sanitised to change non-alphanumeric to underscore
     // feature values are sanitised to encode single quote characters
-    commands = ChimeraCommands.buildSetAttributeCommands(featuresMap);
-    assertTrue(commands
-            .contains("setattr r jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' #0:7-15.A"));
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    String expected3 = "setattr res jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' #0:7-15.A";
+    assertTrue(commands.get(0).getCommand().equals(expected3));
   }
 
   /**
    * Tests for the method that prefixes and sanitises a feature name so it can
-   * be used as a valid, namespaced attribute name in Chimera
+   * be used as a valid, namespaced attribute name in Chimera or PyMol
    */
   @Test(groups = { "Functional" })
   public void testMakeAttributeName()
   {
-    assertEquals(ChimeraCommands.makeAttributeName(null), "jv_");
-    assertEquals(ChimeraCommands.makeAttributeName(""), "jv_");
-    assertEquals(ChimeraCommands.makeAttributeName("helix"), "jv_helix");
-    assertEquals(ChimeraCommands.makeAttributeName("Hello World 24"),
+    assertEquals(testee.makeAttributeName(null), "jv_");
+    assertEquals(testee.makeAttributeName(""), "jv_");
+    assertEquals(testee.makeAttributeName("helix"), "jv_helix");
+    assertEquals(testee.makeAttributeName(
+            "Hello World 24"),
             "jv_Hello_World_24");
-    assertEquals(
-            ChimeraCommands.makeAttributeName("!this is-a_very*{odd(name"),
+    assertEquals(testee.makeAttributeName(
+            "!this is-a_very*{odd(name"),
             "jv__this_is_a_very__odd_name");
     // name ending in color gets underscore appended
-    assertEquals(ChimeraCommands.makeAttributeName("helixColor"),
-            "jv_helixColor_");
+    assertEquals(testee.makeAttributeName("helixColor"), "jv_helixColor_");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-4.A,8.A,5-7.B");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1:2-5.A,8.A,5-7.B");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B|#1:2-5.A,8.A,5-7.B");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-7.B");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
+
   }
 
   @Test(groups = { "Functional" })
-  public void testGetColourBySequenceCommands_hiddenColumns()
+  public void testSuperposeStructures()
   {
-    /*
-     * load these sequences, coloured by Strand propensity,
-     * with columns 2-4 hidden
-     */
-    SequenceI seq1 = new Sequence("seq1", "MHRSQSSSGG");
-    SequenceI seq2 = new Sequence("seq2", "MVRSNGGSSS");
-    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame af = new AlignFrame(al, 800, 500);
-    af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString());
-    ColumnSelection cs = new ColumnSelection();
-    cs.addElement(2);
-    cs.addElement(3);
-    cs.addElement(4);
-    af.getViewport().setColumnSelection(cs);
-    af.hideSelColumns_actionPerformed(null);
-    SequenceRenderer sr = new SequenceRenderer(af.getViewport());
-    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
-    String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
-    StructureSelectionManager ssm = new StructureSelectionManager();
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange("1", 12, 14, "A");
+    ref.addRange("1", 18, 18, "B");
+    ref.addRange("1", 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> command = testee.superposeStructures(ref,
+            toAlign);
+    // qualifier to restrict match to CA and no altlocs
+    String carbonAlphas = "@CA&~@.B-Z&~@.2-9";
+    String refSpec = "#1:12-14.A,18.B,22-23.B";
+    String toAlignSpec = "#2:15-17.B,20-21.B,22.C";
+    String expected = String.format("match %s%s %s%s; ribbon %s|%s; focus",
+            toAlignSpec, carbonAlphas, refSpec, carbonAlphas, toAlignSpec,
+            refSpec);
+    assertEquals(command.get(0).getCommand(), expected);
+  }
 
-    /*
-     * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
-     */
-    HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
-    for (int pos = 1; pos <= seq1.getLength(); pos++)
-    {
-      map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
-    }
-    StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1",
-            "A", map, null);
-    ssm.addStructureMapping(sm1);
-    StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
-            "B", map, null);
-    ssm.addStructureMapping(sm2);
-
-    StructureMappingcommandSet[] commands = ChimeraCommands
-            .getColourBySequenceCommand(ssm, files, seqs, sr, af.alignPanel);
-    assertEquals(1, commands.length);
-    assertEquals(1, commands[0].commands.length);
-    String theCommand = commands[0].commands[0];
-    // M colour is #82827d (see strand.html help page)
-    assertTrue(theCommand.contains("color #82827d #0:21.A|#1:21.B"));
-    // H colour is #60609f
-    assertTrue(theCommand.contains("color #60609f #0:22.A"));
-    // V colour is #ffff00
-    assertTrue(theCommand.contains("color #ffff00 #1:22.B"));
-    // hidden columns are Gray (128, 128, 128)
-    assertTrue(theCommand.contains("color #808080 #0:23-25.A|#1:23-25.B"));
-    // S and G are both coloured #4949b6
-    assertTrue(theCommand.contains("color #4949b6 #0:26-30.A|#1:26-30.B"));
+  @Test(groups = "Functional")
+  public void testGetAtomSpec_alphaOnly()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, true), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-4.A@CA&~@.B-Z&~@.2-9");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-4.A,8.A@CA&~@.B-Z&~@.2-9");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-4.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-7.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,5-9.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,3-10.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9");
+    model.addRange("5", 25, 35, " "); // empty chain code
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0:1-4.B,3-10.C@CA&~@.B-Z&~@.2-9|#1:2-5.A,8.A,5-10.B@CA&~@.B-Z&~@.2-9|#5:25-35.@CA&~@.B-Z&~@.2-9");
+
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModelStartNo()
+  {
+    assertEquals(testee.getModelStartNo(), 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResidueSpec()
+  {
+    assertEquals(testee.getResidueSpec("ALA"), "::ALA");
+  }
+
+  @Test(groups = "Functional")
+  public void testShowBackbone()
+  {
+    List<StructureCommandI> cmds = testee.showBackbone();
+    assertEquals(cmds.size(), 1);
+    assertEquals(cmds.get(0).getCommand(),
+            "~display all;~ribbon;chain @CA|P");
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenCommandFile()
+  {
+    assertEquals(testee.openCommandFile("nowhere").getCommand(),
+            "open cmd:nowhere");
+  }
+
+  @Test(groups = "Functional")
+  public void testSaveSession()
+  {
+    assertEquals(testee.saveSession("somewhere").getCommand(),
+            "save somewhere");
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByChain()
+  {
+    assertEquals(testee.colourByChain().getCommand(), "rainbow chain");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetBackgroundColour()
+  {
+    StructureCommandI cmd = testee.setBackgroundColour(Color.PINK);
+    assertEquals(cmd.getCommand(), "set bgColor #ffafaf");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testLoadFile()
+  {
+    StructureCommandI cmd = testee.loadFile("/some/filepath");
+    assertEquals(cmd.getCommand(), "open /some/filepath");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testOpenSession()
+  {
+    StructureCommandI cmd = testee.openSession("/some/filepath");
+    assertEquals(cmd.getCommand(), "open chimera:/some/filepath");
+  }
+
+  @Test(groups = "Functional")
+  public void testColourByCharge()
+  {
+    List<StructureCommandI> cmds = testee.colourByCharge();
+    assertEquals(cmds.size(), 1);
+    assertEquals(cmds.get(0)
+            .getCommand(),
+            "color white;color red ::ASP,GLU;color blue ::LYS,ARG;color yellow ::CYS");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourCommand()
+  {
+    assertEquals(testee.colourResidues("something", Color.MAGENTA)
+            .getCommand(),
+            "color #ff00ff something");
+  }
+
+  @Test(groups = "Functional")
+  public void testFocusView()
+  {
+    assertEquals(testee.focusView().getCommand(), "focus");
+  }
+
+  @Test(groups = "Functional")
+  public void testSetAttribute()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    model.addRange("1", 89, 92, "A");
+    model.addRange("2", 12, 20, "B");
+    model.addRange("2", 8, 9, "B");
+    assertEquals(testee.setAttribute("jv_kd", "27.3", model).getCommand(),
+            "setattr res jv_kd '27.3' #1:89-92.A|#2:8-9.B,12-20.B");
+  }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("stop really"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSelectedResidues()
+  {
+    assertEquals(testee.getSelectedResidues(),
+            new StructureCommand("list selection level residue"));
+  }
+
+  @Test(groups = "Functional")
+  public void testListResidueAttributes()
+  {
+    assertEquals(testee.listResidueAttributes(),
+            new StructureCommand("list resattr"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResidueAttributes()
+  {
+    assertEquals(testee.getResidueAttributes("binding site"),
+            new StructureCommand("list residues attr 'binding site'"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStartNotifications()
+  {
+    List<StructureCommandI> cmds = testee.startNotifications("to here");
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("listen start models url to here"));
+    assertEquals(cmds.get(1), new StructureCommand("listen start select prefix SelectionChanged url to here"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStopNotifications()
+  {
+    List<StructureCommandI> cmds = testee.stopNotifications();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("listen stop models"));
+    assertEquals(cmds.get(1), new StructureCommand("listen stop selection"));
   }
 }
index 99394dc..b07a622 100644 (file)
@@ -47,7 +47,7 @@ public class ChimeraConnect
     final StructureManager structureManager = new StructureManager(true);
     ChimeraManager cm = new ChimeraManager(structureManager);
     assertTrue("Couldn't launch chimera",
-            cm.launchChimera(StructureManager.getChimeraPaths()));
+            cm.launchChimera(StructureManager.getChimeraPaths(false)));
     assertTrue(cm.isChimeraLaunched()); // Chimera process is alive
     // int n=0;
     // not sure of the point of this is unless the tester is loading models
diff --git a/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java b/test/jalview/ext/rbvi/chimera/ChimeraXCommandsTest.java
new file mode 100644 (file)
index 0000000..f677cab
--- /dev/null
@@ -0,0 +1,364 @@
+/*
+ * 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.ext.rbvi.chimera;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.awt.Color;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommand;
+import jalview.structure.StructureCommandI;
+
+public class ChimeraXCommandsTest
+{
+  private ChimeraXCommands testee;
+
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    testee = new ChimeraXCommands();
+  }
+
+  @Test(groups = { "Functional" })
+  public void testColourByCharge()
+  {
+    List<StructureCommandI> cmd = testee.colourByCharge();
+    assertEquals(cmd.size(), 1);
+    assertEquals(cmd.get(0).getCommand(),
+            "color white;color :ASP,GLU red;color :LYS,ARG blue;color :CYS yellow");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testColourByChain()
+  {
+    StructureCommandI cmd = testee.colourByChain();
+    assertEquals(cmd.getCommand(), "rainbow chain");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testFocusView()
+  {
+    StructureCommandI cmd = testee.focusView();
+    assertEquals(cmd.getCommand(), "view");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetBackgroundColour()
+  {
+    StructureCommandI cmd = testee.setBackgroundColour(Color.PINK);
+    assertEquals(cmd.getCommand(), "set bgColor #ffafaf");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testOpenSession()
+  {
+    StructureCommandI cmd = testee.openSession("/some/filepath");
+    assertEquals(cmd.getCommand(), "open /some/filepath format session");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testColourBySequence()
+  {
+    Map<Object, AtomSpecModel> map = new LinkedHashMap<>();
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 2, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 7, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "1", 9, 23, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 1, 1, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.blue, "2", 4, 7, "B");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 8, 8, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.yellow, "2", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 3, 5, "A");
+    ChimeraCommands.addAtomSpecRange(map, Color.red, "1", 6, 9, "A");
+
+    /*
+     * Colours should appear in the Chimera command in the order in which
+     * they were added; within colour, by model, by chain, ranges in start order
+     */
+    List<StructureCommandI> commands = testee.colourBySequence(map);
+    assertEquals(commands.size(), 1);
+    assertEquals(commands.get(0).getCommand(),
+            "color #1/A:2-5,9-23/B:7|#2/A:1/B:4-7 #0000ff;color #2/A:3-5,8 #ffff00;color #1/A:3-9 #ff0000");
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetAttributes()
+  {
+    /*
+     * make a map of { featureType, {featureValue, {residue range specification } } }
+     */
+    Map<String, Map<Object, AtomSpecModel>> featuresMap = new LinkedHashMap<>();
+    Map<Object, AtomSpecModel> featureValues = new HashMap<>();
+
+    /*
+     * start with just one feature/value...
+     */
+    featuresMap.put("chain", featureValues);
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 8, 20, "A");
+
+    List<StructureCommandI> commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+
+    /*
+     * feature name gets a jv_ namespace prefix
+     * feature value is quoted in case it contains spaces
+     */
+    assertEquals(commands.get(0).getCommand(),
+            "setattr #0/A:8-20 res jv_chain 'X' create true");
+
+    // add same feature value, overlapping range
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 3, 9, "A");
+    // same feature value, contiguous range
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    assertEquals(commands.get(0).getCommand(),
+            "setattr #0/A:3-25 res jv_chain 'X' create true");
+
+    // same feature value and model, different chain
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "0", 21, 25, "B");
+    // same feature value and chain, different model
+    ChimeraCommands.addAtomSpecRange(featureValues, "X", "1", 26, 30, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    String expected1 = "setattr #0/A:3-25/B:21-25|#1/A:26-30 res jv_chain 'X' create true";
+    assertEquals(commands.get(0).getCommand(), expected1);
+
+    // same feature, different value
+    ChimeraCommands.addAtomSpecRange(featureValues, "Y", "0", 40, 50, "A");
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(2, commands.size());
+    // commands are ordered by feature type but not by value
+    // so test for the expected command in either order
+    String cmd1 = commands.get(0).getCommand();
+    String cmd2 = commands.get(1).getCommand();
+    assertTrue(cmd1.equals(expected1) || cmd2.equals(expected1));
+    String expected2 = "setattr #0/A:40-50 res jv_chain 'Y' create true";
+    assertTrue(cmd1.equals(expected2) || cmd2.equals(expected2));
+
+    featuresMap.clear();
+    featureValues.clear();
+    featuresMap.put("side-chain binding!", featureValues);
+    ChimeraCommands.addAtomSpecRange(featureValues,
+            "<html>metal <a href=\"http:a.b.c/x\"> 'ion!", "0", 7, 15, "A");
+    // feature names are sanitised to change non-alphanumeric to underscore
+    // feature values are sanitised to encode single quote characters
+    commands = testee.setAttributes(featuresMap);
+    assertEquals(commands.size(), 1);
+    String expected3 = "setattr #0/A:7-15 res jv_side_chain_binding_ '<html>metal <a href=\"http:a.b.c/x\"> &#39;ion!' create true";
+    assertTrue(commands.get(0).getCommand().equals(expected3));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSuperposeStructures()
+  {
+    AtomSpecModel ref = new AtomSpecModel();
+    ref.addRange("1", 12, 14, "A");
+    ref.addRange("1", 18, 18, "B");
+    ref.addRange("1", 22, 23, "B");
+    AtomSpecModel toAlign = new AtomSpecModel();
+    toAlign.addRange("2", 15, 17, "B");
+    toAlign.addRange("2", 20, 21, "B");
+    toAlign.addRange("2", 22, 22, "C");
+    List<StructureCommandI> command = testee.superposeStructures(ref,
+            toAlign);
+    assertEquals(command.size(), 1);
+    String cmd = command.get(0).getCommand();
+    String refSpec = "#1/A:12-14/B:18,22-23";
+    String toAlignSpec = "#2/B:15-17,20-21/C:22";
+
+    /*
+     * superposition arguments include AlphaCarbon restriction,
+     * ribbon command does not
+     */
+    String expected = String.format(
+            "align %s@CA toAtoms %s@CA; ribbon %s|%s; view", toAlignSpec,
+            refSpec, toAlignSpec, refSpec);
+    assertEquals(cmd, expected);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, false), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, false), "#1/A:2-4,8/B:5-7");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, false), "#1/A:2-5,8/B:5-7");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4|#1/A:2-5,8/B:5-7");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-7");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4/C:5-9|#1/A:2-5,8/B:5-10");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10");
+    model.addRange("5", 25, 35, " ");
+    assertEquals(testee.getAtomSpec(model, false),
+            "#0/B:1-4/C:3-10|#1/A:2-5,8/B:5-10|#5/:25-35");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAtomSpec_alphaOnly()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertEquals(testee.getAtomSpec(model, true), "");
+    model.addRange("1", 2, 4, "A");
+    assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4@CA");
+    model.addRange("1", 8, 8, "A");
+    assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8@CA");
+    model.addRange("1", 5, 7, "B");
+    assertEquals(testee.getAtomSpec(model, true), "#1/A:2-4,8/B:5-7@CA");
+    model.addRange("1", 3, 5, "A");
+    assertEquals(testee.getAtomSpec(model, true), "#1/A:2-5,8/B:5-7@CA");
+    model.addRange("0", 1, 4, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4@CA|#1/A:2-5,8/B:5-7@CA");
+    model.addRange("0", 5, 9, "C");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-7@CA");
+    model.addRange("1", 8, 10, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-10@CA");
+    model.addRange("1", 8, 9, "B");
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4/C:5-9@CA|#1/A:2-5,8/B:5-10@CA");
+    model.addRange("0", 3, 10, "C"); // subsumes 5-9
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4/C:3-10@CA|#1/A:2-5,8/B:5-10@CA");
+    model.addRange("5", 25, 35, " "); // empty chain code
+    assertEquals(testee.getAtomSpec(model, true),
+            "#0/B:1-4/C:3-10@CA|#1/A:2-5,8/B:5-10@CA|#5/:25-35@CA");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetModelStartNo()
+  {
+    assertEquals(testee.getModelStartNo(), 1);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetResidueSpec()
+  {
+    assertEquals(testee.getResidueSpec("ALA"), ":ALA");
+  }
+
+  @Test(groups = "Functional")
+  public void testShowBackbone()
+  {
+    List<StructureCommandI> showBackbone = testee.showBackbone();
+    assertEquals(showBackbone.size(), 1);
+    assertEquals(showBackbone.get(0).getCommand(),
+            "~display all;~ribbon;show @CA|P atoms");
+  }
+
+  @Test(groups = "Functional")
+  public void testOpenCommandFile()
+  {
+    assertEquals(testee.openCommandFile("nowhere").getCommand(),
+            "open nowhere");
+  }
+
+  @Test(groups = "Functional")
+  public void testSaveSession()
+  {
+    assertEquals(testee.saveSession("somewhere").getCommand(),
+            "save somewhere format session");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColourCommand()
+  {
+    assertEquals(testee.colourResidues("something", Color.MAGENTA)
+            .getCommand(),
+            "color something #ff00ff");
+  }
+
+  @Test(groups = "Functional")
+  public void testSetAttribute()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    model.addRange("1", 89, 92, "A");
+    model.addRange("2", 12, 20, "B");
+    model.addRange("2", 8, 9, "B");
+    assertEquals(testee.setAttribute("jv_kd", "27.3", model)
+            .getCommand(),
+            "setattr #1/A:89-92|#2/B:8-9,12-20 res jv_kd '27.3' create true");
+  }
+
+  @Test(groups = "Functional")
+  public void testCloseViewer()
+  {
+    assertEquals(testee.closeViewer(), new StructureCommand("exit"));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetSelectedResidues()
+  {
+    assertEquals(testee.getSelectedResidues(),
+            new StructureCommand("info selection level residue"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStartNotifications()
+  {
+    List<StructureCommandI> cmds = testee.startNotifications("to here");
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("info notify start models prefix ModelChanged jalview url to here"));
+    assertEquals(cmds.get(1), new StructureCommand("info notify start selection jalview prefix SelectionChanged url to here"));
+  }
+
+  @Test(groups = "Functional")
+  public void testStopNotifications()
+  {
+    List<StructureCommandI> cmds = testee.stopNotifications();
+    assertEquals(cmds.size(), 2);
+    assertEquals(cmds.get(0), new StructureCommand("info notify stop models jalview"));
+    assertEquals(cmds.get(1), new StructureCommand("info notify stop selection jalview"));
+  }
+
+  @Test(groups = "Functional")
+  public void testListResidueAttributes()
+  {
+    assertEquals(testee.listResidueAttributes(),
+            new StructureCommand("info resattr"));
+  }
+}
index 734f7eb..d397a6b 100644 (file)
@@ -25,6 +25,16 @@ import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Vector;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
 import jalview.api.FeatureRenderer;
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
@@ -41,22 +51,13 @@ import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+import jalview.structure.StructureCommand;
 import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
 import jalview.ws.sifts.SiftsClient;
 import jalview.ws.sifts.SiftsException;
 import jalview.ws.sifts.SiftsSettings;
 
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-import java.util.Vector;
-
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
 @Test(singleThreaded = true)
 public class JalviewChimeraView
 {
@@ -148,7 +149,7 @@ public class JalviewChimeraView
       }
     }
 
-    assertTrue(binding.isChimeraRunning(), "Failed to start Chimera");
+    assertTrue(binding.isViewerRunning(), "Failed to start Chimera");
 
     assertEquals(chimeraViewer.getBinding().getPdbCount(), 1);
     chimeraViewer.closeViewer(true);
@@ -208,7 +209,7 @@ public class JalviewChimeraView
       }
     } while (!binding.isFinishedInit());
 
-    assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera");
+    assertTrue(binding.isViewerRunning(), "Failed to launch Chimera");
 
     assertEquals(binding.getPdbCount(), 1);
 
@@ -290,7 +291,8 @@ public class JalviewChimeraView
     /*
      * ask Chimera for its residue attribute names
      */
-    List<String> reply = binding.sendChimeraCommand("list resattr", true);
+    List<String> reply = binding
+            .executeCommand(new StructureCommand("list resattr"), true);
     // prefixed and sanitised attribute names for Jalview features:
     assertTrue(reply.contains("resattr jv_domain"));
     assertTrue(reply.contains("resattr jv_metal_ion_binding_site"));
@@ -306,8 +308,9 @@ public class JalviewChimeraView
      * ask Chimera for residues with an attribute
      * 91 and 96 on sequence --> residues 40 and 45 on chains A and B
      */
-    reply = binding.sendChimeraCommand(
-            "list resi att jv_metal_ion_binding_site", true);
+    reply = binding.executeCommand(
+            new StructureCommand("list resi att jv_metal_ion_binding_site"),
+            true);
     assertEquals(reply.size(), 4);
     assertTrue(reply
             .contains("residue id #0:40.A jv_metal_ion_binding_site \"Iron-Sulfur (2Fe-2S)\" index 40"));
@@ -322,7 +325,8 @@ public class JalviewChimeraView
      * check attributes with score values
      * sequence positions 62 and 65 --> residues 11 and 14 on chains A and B
      */
-    reply = binding.sendChimeraCommand("list resi att jv_kd", true);
+    reply = binding.executeCommand(
+            new StructureCommand("list resi att jv_kd"), true);
     assertEquals(reply.size(), 4);
     assertTrue(reply.contains("residue id #0:11.A jv_kd -2.1 index 11"));
     assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14"));
@@ -332,8 +336,9 @@ public class JalviewChimeraView
     /*
      * list residues with positive kd score 
      */
-    reply = binding.sendChimeraCommand(
-            "list resi spec :*/jv_kd>0 attr jv_kd", true);
+    reply = binding.executeCommand(
+            new StructureCommand("list resi spec :*/jv_kd>0 attr jv_kd"),
+            true);
     assertEquals(reply.size(), 2);
     assertTrue(reply.contains("residue id #0:14.A jv_kd 3.6 index 14"));
     assertTrue(reply.contains("residue id #0:14.B jv_kd 3.6 index 14"));
@@ -397,13 +402,13 @@ public class JalviewChimeraView
       }
     } while (!binding.isFinishedInit());
   
-    assertTrue(binding.isChimeraRunning(), "Failed to launch Chimera");
+    assertTrue(binding.isViewerRunning(), "Failed to launch Chimera");
   
     assertEquals(binding.getPdbCount(), 1);
   
     /*
-     * 'perform' menu action to copy visible features to
-     * attributes in Chimera
+     * 'perform' menu action to copy Chimera attributes
+     * to features in Jalview
      */
     // TODO rename and pull up method to binding interface
     // once functionality is added for Jmol as well
@@ -440,14 +445,9 @@ public class JalviewChimeraView
     binding.copyStructureAttributesToFeatures("phi", af.getViewport()
             .getAlignPanel());
     fr.setVisible("phi");
-    List<SequenceFeature> fs = fer2Arath.getFeatures().findFeatures(54, 54);
-    assertEquals(fs.size(), 3);
-    /*
-     * order of returned features is not guaranteed
-     */
-    assertTrue("RESNUM".equals(fs.get(0).getType())
-            || "RESNUM".equals(fs.get(1).getType())
-            || "RESNUM".equals(fs.get(2).getType()));
+    List<SequenceFeature> fs = fer2Arath.getFeatures().findFeatures(54, 54,
+            "phi");
+    assertEquals(fs.size(), 2);
     assertTrue(fs.contains(new SequenceFeature("phi", "A", 54, 54,
             -131.0713f, "Chimera")));
     assertTrue(fs.contains(new SequenceFeature("phi", "B", 54, 54,
@@ -473,11 +473,11 @@ public class JalviewChimeraView
           int res, String featureType)
   {
     String where = "at position " + res;
-    List<SequenceFeature> fs = seq.getFeatures().findFeatures(res, res);
+    List<SequenceFeature> fs = seq.getFeatures().findFeatures(res, res,
+            featureType);
 
-    assertEquals(fs.size(), 2, where);
-    assertEquals(fs.get(0).getType(), "RESNUM", where);
-    SequenceFeature sf = fs.get(1);
+    assertEquals(fs.size(), 1, where);
+    SequenceFeature sf = fs.get(0);
     assertEquals(sf.getType(), featureType, where);
     assertEquals(sf.getFeatureGroup(), "Chimera", where);
     assertEquals(sf.getDescription(), "True", where);
index fd4bd4b..f09c7c7 100644 (file)
@@ -4,6 +4,13 @@ import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.testng.annotations.Test;
+
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -17,13 +24,6 @@ import jalview.schemes.FeatureColourTest;
 import jalview.util.matcher.Condition;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
-import java.awt.Color;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-
-import org.testng.annotations.Test;
-
 public class FeatureSettingsTest
 {
   /**
@@ -160,7 +160,7 @@ public class FeatureSettingsTest
     assertEquals(fr.getFeatureFilter("type2").toStableString(),
             "(Score LE 2.4) AND (Score GT 1.1)");
     assertEquals(fr.getFeatureFilter("type3").toStableString(),
-            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0)");
   }
 
   /**
index 9b21274..24697c0 100644 (file)
@@ -68,13 +68,27 @@ public class FreeUpMemoryTest
     File f = generateAlignment();
     f.deleteOnExit();
 
+    long expectedMin = 35L;
+    long usedMemoryAtStart=getUsedMemory();
+    if (usedMemoryAtStart>expectedMin)
+    {
+      System.err.println("used memory before test is "+usedMemoryAtStart+" > "+expectedMin+"MB .. adjusting minimum.");
+      expectedMin = usedMemoryAtStart;
+    }
     doStuffInJalview(f);
 
     Desktop.instance.closeAll_actionPerformed(null);
 
-    checkUsedMemory(35L);
+    checkUsedMemory(expectedMin);
   }
 
+  private static long getUsedMemory()
+  {
+    long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
+    long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
+    long usedMemory = availableMemory - freeMemory;
+    return usedMemory;
+  }
   /**
    * Requests garbage collection and then checks whether remaining memory in use
    * is less than the expected value (in Megabytes)
@@ -101,10 +115,7 @@ public class FreeUpMemoryTest
     /*
      * check used memory is 'reasonably low'
      */
-    long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
-    long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
-    long usedMemory = availableMemory - freeMemory;
-
+    long usedMemory = getUsedMemory();
     /*
      * sanity check - fails if any frame was added after
      * closeAll_actionPerformed
index 22035f1..84d2b79 100644 (file)
  */
 package jalview.gui;
 
-import static org.testng.Assert.assertEquals;
-
 import java.awt.Font;
 import java.awt.FontMetrics;
 
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
 
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SearchResults;
+import jalview.datamodel.SearchResultsI;
+import jalview.io.DataSourceType;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+
 import junit.extensions.PA;
 
 public class SeqCanvasTest
@@ -306,7 +314,7 @@ public class SeqCanvasTest
    * endSeq should be unchanged, but the vertical repeat height should include
    * all sequences.
    */
-  @Test(groups = "Functional")
+  @Test(groups = "Functional_Failing")
   public void testCalculateWrappedGeometry_fromScrolled()
   {
     AlignViewport av = af.getViewport();
@@ -358,4 +366,24 @@ public class SeqCanvasTest
       }
     } while (af.getViewport().getCalcManager().isWorking());
   }
+  @Test(groups = "Functional")
+  public void testClear_HighlightAndSelection()
+  {
+    AlignViewport av = af.getViewport();
+    SearchResultsI highlight = new SearchResults();
+    highlight.addResult(
+            av.getAlignment().getSequenceAt(1).getDatasetSequence(), 50,
+            80);
+    af.alignPanel.highlightSearchResults(highlight);
+    af.avc.markHighlightedColumns(false, false, false);
+    assertNotNull(av.getSearchResults(),
+            "No highlight was created on alignment");
+    assertFalse(av.getColumnSelection().isEmpty(),
+            "No selection was created from highlight");
+    af.deselectAllSequenceMenuItem_actionPerformed(null);
+    assertTrue(av.getColumnSelection().isEmpty(),
+            "No Selection should be present after deselecting all.");
+    assertNull(av.getSearchResults(),
+            "No higlighted search results should be present after deselecting all.");
+  }
 }
index 298ae6b..b753e94 100644 (file)
@@ -27,6 +27,18 @@ import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureRenderer;
 import jalview.datamodel.Alignment;
@@ -47,18 +59,7 @@ import jalview.structure.StructureSelectionManager;
 import jalview.util.matcher.Condition;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
-
-import java.awt.Color;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-
-import org.testng.annotations.AfterClass;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
+import junit.extensions.PA;
 
 public class FeaturesFileTest
 {
@@ -725,7 +726,7 @@ public class FeaturesFileTest
     assertTrue(matcher.isByScore());
     assertSame(matcher.getMatcher().getCondition(), Condition.LT);
     assertEquals(matcher.getMatcher().getPattern(), "1.3");
-    assertEquals(matcher.getMatcher().getFloatValue(), 1.3f);
+    assertEquals(PA.getValue(matcher.getMatcher(), "floatValue"), 1.3f);
 
     assertFalse(matchers.hasNext());
   }
index 42183ca..772ed2b 100644 (file)
@@ -23,8 +23,18 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
 import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.MappedFeatures;
+import jalview.datamodel.Mapping;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -32,16 +42,8 @@ import jalview.gui.JvOptionPane;
 import jalview.io.gff.GffConstants;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
+import jalview.util.MapList;
 import jalview.viewmodel.seqfeatures.FeatureRendererModel;
-
-import java.awt.Color;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
 import junit.extensions.PA;
 
 public class SequenceAnnotationReportTest
@@ -57,24 +59,24 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeature_disulfideBond()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     sb.append("123456");
     SequenceFeature sf = new SequenceFeature("disulfide bond", "desc", 1,
             3, 1.2f, "group");
 
     // residuePos == 2 does not match start or end of feature, nothing done:
-    sar.appendFeature(sb, 2, null, sf, null);
+    sar.appendFeature(sb, 2, null, sf, null, 0);
     assertEquals("123456", sb.toString());
 
     // residuePos == 1 matches start of feature, text appended (but no <br/>)
     // feature score is not included
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertEquals("123456disulfide bond 1:3", sb.toString());
 
     // residuePos == 3 matches end of feature, text appended
     // <br/> is prefixed once sb.length() > 6
-    sar.appendFeature(sb, 3, null, sf, null);
+    sar.appendFeature(sb, 3, null, sf, null, 0);
     assertEquals("123456disulfide bond 1:3<br/>disulfide bond 1:3",
             sb.toString());
   }
@@ -82,13 +84,13 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeatures_longText()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     String longString = "Abcd".repeat(50);
     SequenceFeature sf = new SequenceFeature("sequence", longString, 1, 3,
             "group");
 
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertTrue(sb.length() < 100);
 
     List<SequenceFeature> sfl = new ArrayList<>();
@@ -103,7 +105,7 @@ public class SequenceAnnotationReportTest
     sfl.add(sf);
     sfl.add(sf);
     sfl.add(sf);
-    int n = sar.appendFeaturesLengthLimit(sb, 1, sfl,
+    int n = sar.appendFeatures(sb, 1, sfl,
             new FeatureRenderer(null), 200); // text should terminate before 200 characters
     String s = sb.toString();
     assertTrue(s.length() < 200);
@@ -114,27 +116,27 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeature_status()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3,
             Float.NaN, "group");
     sf.setStatus("Confirmed");
 
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S; (Confirmed)", sb.toString());
   }
 
   @Test(groups = "Functional")
   public void testAppendFeature_withScore()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
             "group");
 
     FeatureRendererModel fr = new FeatureRenderer(null);
     Map<String, float[][]> minmax = fr.getMinMax();
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -144,7 +146,7 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     // <br/> is appended to a buffer > 6 in length
     assertEquals("METAL 1 3; Fe2-S<br/>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
@@ -154,19 +156,19 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
   @Test(groups = "Functional")
   public void testAppendFeature_noScore()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -176,7 +178,7 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeature_colouredByAttribute()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3,
             Float.NaN, "group");
@@ -186,7 +188,7 @@ public class SequenceAnnotationReportTest
      * first with no colour by attribute
      */
     FeatureRendererModel fr = new FeatureRenderer(null);
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
 
     /*
@@ -197,7 +199,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("Pfam");
     fr.setColour("METAL", fc);
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
 
     /*
@@ -205,7 +207,7 @@ public class SequenceAnnotationReportTest
      */
     fc.setAttributeName("clinical_significance");
     sb.setLength(0);
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
     assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
             sb.toString());
   }
@@ -213,7 +215,7 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeature_withScoreStatusAttribute()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
             "group");
@@ -227,7 +229,7 @@ public class SequenceAnnotationReportTest
     fc.setAttributeName("clinical_significance");
     fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, fr, sf, null);
+    sar.appendFeature(sb, 1, fr, sf, null, 0);
 
     assertEquals(
             "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
@@ -237,38 +239,38 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testAppendFeature_DescEqualsType()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL", "METAL", 1, 3,
             Float.NaN, "group");
 
     // description is not included if it duplicates type:
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertEquals("METAL 1 3", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("Metal");
     // test is case-sensitive:
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     assertEquals("METAL 1 3; Metal", sb.toString());
   }
 
   @Test(groups = "Functional")
   public void testAppendFeature_stripHtml()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
     SequenceFeature sf = new SequenceFeature("METAL",
             "<html><body>hello<em>world</em></body></html>", 1, 3,
             Float.NaN, "group");
 
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     // !! strips off </body> but not <body> ??
     assertEquals("METAL 1 3; <body>hello<em>world</em>", sb.toString());
 
     sb.setLength(0);
     sf.setDescription("<br>&kHD>6");
-    sar.appendFeature(sb, 1, null, sf, null);
+    sar.appendFeature(sb, 1, null, sf, null, 0);
     // if no <html> tag, html-encodes > and < (only):
     assertEquals("METAL 1 3; &lt;br&gt;&kHD&gt;6", sb.toString());
   }
@@ -276,7 +278,7 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testCreateSequenceAnnotationReport()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
 
     SequenceI seq = new Sequence("s1", "MAKLKRFQSSTLL");
@@ -398,7 +400,7 @@ public class SequenceAnnotationReportTest
   @Test(groups = "Functional")
   public void testCreateSequenceAnnotationReport_withEllipsis()
   {
-    SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
     StringBuilder sb = new StringBuilder();
   
     SequenceI seq = new Sequence("s1", "ABC");
@@ -424,4 +426,63 @@ public class SequenceAnnotationReportTest
             .endsWith(
                     "<br/>PDB7 3iu1<br/>PDB8,...<br/>(Output Sequence Details to list all database references)</i>"));
   }
+
+  /**
+   * Test adding a linked feature to the tooltip
+   */
+  @Test(groups = "Functional")
+  public void testAppendFeature_virtualFeature()
+  {
+    /*
+     * map CDS to peptide sequence
+     */
+    SequenceI cds = new Sequence("Cds/101-121", "CCTttgAGAtttCAAatgGAT");
+    SequenceI peptide = new Sequence("Peptide/8-14", "PLRFQMD");
+    MapList map = new MapList(new int[] { 101, 118 }, new int[] { 8, 13 },
+            3, 1);
+    Mapping mapping = new Mapping(peptide, map);
+
+    /*
+     * assume variant feature found at CDS position 106 G>C
+     */
+    List<SequenceFeature> features = new ArrayList<>();
+    // vary ttg (Leu) to ttc (Phe)
+    SequenceFeature sf = new SequenceFeature("variant", "G,C", 106, 106,
+            Float.NaN, null);
+    features.add(sf);
+    MappedFeatures mf = new MappedFeatures(mapping, cds, 9, 'L', features);
+
+    StringBuilder sb = new StringBuilder();
+    SequenceAnnotationReport sar = new SequenceAnnotationReport(false);
+    sar.appendFeature(sb, 1, null, sf, mf, 0);
+
+    /*
+     * linked feature shown in tooltip in protein coordinates
+     */
+    assertEquals("variant 9; G,C", sb.toString());
+
+    /*
+     * adding "alleles" attribute to variant allows peptide consequence
+     * to be calculated and added to the tooltip
+     */
+    sf.setValue("alleles", "G,C");
+    sb = new StringBuilder();
+    sar.appendFeature(sb, 1, null, sf, mf, 0);
+    assertEquals("variant 9; G,C p.Leu9Phe", sb.toString());
+
+    /*
+     * now a virtual peptide feature on CDS
+     * feature at 11-12 on peptide maps to 110-115 on CDS
+     * here we test for tooltip at 113 (t)
+     */
+    SequenceFeature sf2 = new SequenceFeature("metal", "Fe", 11, 12,
+            2.3f, "Uniprot");
+    features.clear();
+    features.add(sf2);
+    mapping = new Mapping(peptide, map);
+    mf = new MappedFeatures(mapping, peptide, 113, 't', features);
+    sb = new StringBuilder();
+    sar.appendFeature(sb, 1, null, sf2, mf, 0);
+    assertEquals("metal 110 115; Fe Score=2.3", sb.toString());
+  }
 }
index ba4312a..5b74306 100644 (file)
  */
 package jalview.io;
 
+import static org.testng.Assert.assertTrue;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
 
-import jalview.datamodel.AlignmentAnnotation;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.Annotation;
-import jalview.datamodel.SequenceFeature;
-import jalview.datamodel.SequenceI;
-import jalview.gui.JvOptionPane;
-
 import java.io.File;
 import java.util.Arrays;
 import java.util.BitSet;
@@ -45,6 +39,17 @@ import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.JvOptionPane;
+import jalview.util.DBRefUtils;
+
 public class StockholmFileTest
 {
 
@@ -93,14 +98,57 @@ public class StockholmFileTest
   }
 
   /**
+   * JAL-3529 - verify uniprot refs for sequences are output for sequences
+   * retrieved via Pfam
+   */
+  @Test(groups = { "Functional" })
+  public void dbrefOutput() throws Exception
+  {
+    // sequences retrieved in a Pfam domain alignment also have a PFAM database
+    // reference
+    SequenceI sq = new Sequence("FER2_SPIOL", "AASSDDDFFF");
+    sq.addDBRef(new DBRefEntry("UNIPROT", "1", "P00224"));
+    sq.addDBRef(new DBRefEntry("PFAM", "1", "P00224.1"));
+    sq.addDBRef(new DBRefEntry("PFAM", "1", "PF00111"));
+    AppletFormatAdapter af = new AppletFormatAdapter();
+    String toStockholm = af.formatSequences(FileFormat.Stockholm,
+            new Alignment(new SequenceI[]
+            { sq }), false);
+    System.out.println(toStockholm);
+    // bleh - java.util.Regex sucks
+    assertTrue(
+            Pattern.compile(
+                    "^#=GS\\s+FER2_SPIOL(/\\d+-\\d+)?\\s+AC\\s+P00224$",
+                    Pattern.MULTILINE).matcher(toStockholm)
+                    .find(),
+            "Couldn't locate UNIPROT Accession in generated Stockholm file.");
+    AlignmentI fromStockholm = af.readFile(toStockholm,
+            DataSourceType.PASTE, FileFormat.Stockholm);
+    SequenceI importedSeq = fromStockholm.getSequenceAt(0);
+    assertTrue(importedSeq.getDBRefs()
+            .size() == 1,
+            "Expected just one database reference to be added to sequence.");
+    assertTrue(
+            importedSeq.getDBRefs().get(0).getAccessionId().indexOf(
+                    " ") == -1,
+            "Spaces were found in accession ID.");
+    List<DBRefEntry> dbrefs = DBRefUtils.searchRefs(importedSeq.getDBRefs(),
+            "P00224");
+    assertTrue(dbrefs.size() == 1,
+            "Couldn't find Uniprot DBRef on re-imported sequence.");
+
+  }
+
+  /**
    * test alignment data in given file can be imported, exported and reimported
    * with no dataloss
    * 
    * @param f
-   *          - source datafile (IdentifyFile.identify() should work with it)
+   *                               - source datafile (IdentifyFile.identify()
+   *                               should work with it)
    * @param ioformat
-   *          - label for IO class used to write and read back in the data from
-   *          f
+   *                               - label for IO class used to write and read
+   *                               back in the data from f
    * @param ignoreFeatures
    * @param ignoreRowVisibility
    * @param allowNullAnnotations
index 80ed4c1..eb66416 100644 (file)
@@ -1035,7 +1035,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     assertEquals(fr.getFeatureFilter("type2").toStableString(),
             "(Score LE 2.4) AND (Score GT 1.1)");
     assertEquals(fr.getFeatureFilter("type3").toStableString(),
-            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0)");
   }
 
   private void addFeature(SequenceI seq, String featureType, int score)
diff --git a/test/jalview/structure/AtomSpecModelTest.java b/test/jalview/structure/AtomSpecModelTest.java
new file mode 100644 (file)
index 0000000..394679f
--- /dev/null
@@ -0,0 +1,51 @@
+package jalview.structure;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class AtomSpecModelTest
+{
+  @Test(groups="Functional")
+  public void testGetRanges()
+  {
+    AtomSpecModel model = new AtomSpecModel();
+    assertFalse(model.getModels().iterator().hasNext());
+    List<int[]> ranges = model.getRanges("1", "A");
+    assertTrue(ranges.isEmpty());
+
+    model.addRange("1", 12, 14, "A");
+    assertTrue(model.getRanges("1", "B").isEmpty());
+    assertTrue(model.getRanges("2", "A").isEmpty());
+    ranges = model.getRanges("1", "A");
+    assertEquals(ranges.size(), 1);
+    int[] range = ranges.get(0);
+    assertEquals(range[0], 12);
+    assertEquals(range[1], 14);
+
+    /*
+     * add some ranges; they should be coalesced and
+     * ordered when retrieved
+     */
+    model.addRange("1", 25, 25, "A");
+    model.addRange("1", 20, 24, "A");
+    model.addRange("1", 6, 8, "A");
+    model.addRange("1", 13, 18, "A");
+    model.addRange("1", 5, 6, "A");
+    ranges = model.getRanges("1", "A");
+    assertEquals(ranges.size(), 3);
+    range = ranges.get(0);
+    assertEquals(range[0], 5);
+    assertEquals(range[1], 8);
+    range = ranges.get(1);
+    assertEquals(range[0], 12);
+    assertEquals(range[1], 18);
+    range = ranges.get(2);
+    assertEquals(range[0], 20);
+    assertEquals(range[1], 25);
+  }
+}
index ea53131..9881baf 100644 (file)
@@ -71,4 +71,68 @@ public class AtomSpecTest
       // ok
     }
   }
+
+  @Test
+  public void testFromChimeraXAtomSpec()
+  {
+    AtomSpec as = AtomSpec.fromChimeraXAtomspec("#1/B:12");
+    assertEquals(as.getModelNumber(), 1);
+    assertEquals(as.getPdbResNum(), 12);
+    assertEquals(as.getChain(), "B");
+    assertNull(as.getPdbFile());
+  
+    // no model - default to zero
+    as = AtomSpec.fromChimeraXAtomspec("/C:13");
+    assertEquals(as.getModelNumber(), 0);
+    assertEquals(as.getPdbResNum(), 13);
+    assertEquals(as.getChain(), "C");
+    assertNull(as.getPdbFile());
+  
+    // model.submodel
+    as = AtomSpec.fromChimeraXAtomspec("#3.2/:15");
+    assertEquals(as.getModelNumber(), 3);
+    assertEquals(as.getPdbResNum(), 15);
+    assertEquals(as.getChain(), "");
+    assertNull(as.getPdbFile());
+  
+    String spec = "3:12.B";
+    try
+    {
+      as = AtomSpec.fromChimeraXAtomspec(spec);
+      fail("Expected exception for " + spec);
+    } catch (IllegalArgumentException e)
+    {
+      // ok
+    }
+  
+    spec = "#3:12-14.B";
+    try
+    {
+      as = AtomSpec.fromChimeraXAtomspec(spec);
+      fail("Expected exception for " + spec);
+    } catch (IllegalArgumentException e)
+    {
+      // ok
+    }
+  
+    spec = "";
+    try
+    {
+      as = AtomSpec.fromChimeraXAtomspec(spec);
+      fail("Expected exception for " + spec);
+    } catch (IllegalArgumentException e)
+    {
+      // ok
+    }
+  
+    spec = null;
+    try
+    {
+      as = AtomSpec.fromChimeraXAtomspec(spec);
+      fail("Expected exception for " + spec);
+    } catch (NullPointerException e)
+    {
+      // ok
+    }
+  }
 }
diff --git a/test/jalview/structure/StructureCommandTest.java b/test/jalview/structure/StructureCommandTest.java
new file mode 100644 (file)
index 0000000..fb4b4c2
--- /dev/null
@@ -0,0 +1,31 @@
+package jalview.structure;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class StructureCommandTest
+{
+  @Test(groups = "Functional")
+  public void testEquals()
+  {
+    StructureCommand sc1 = new StructureCommand("open");
+    assertTrue(sc1.equals(sc1));
+    assertTrue(sc1.equals(new StructureCommand("open")));
+    assertFalse(sc1.equals(null));
+    assertFalse(sc1.equals(new StructureCommand("Open")));
+    assertFalse(sc1.equals("Open"));
+
+    StructureCommand sc3 = new StructureCommand("Open", "file",
+            "/some/path");
+    StructureCommand sc2 = new StructureCommand("Open", "file",
+            "/some/path");
+    assertFalse(sc1.equals(sc2));
+    assertTrue(sc3.equals(sc2));
+    assertEquals(sc2.hashCode(), sc3.hashCode());
+    assertFalse(
+            new StructureCommand("Open file", "/some/path").equals(sc2));
+  }
+}
index e59648f..9b240d3 100644 (file)
@@ -124,10 +124,10 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase
     acf3.addMap(new Sequence("s3", "ttt"), new Sequence("p3", "p"),
             new MapList(new int[] { 1, 3 }, new int[] { 1, 1 }, 1, 1));
 
-    List<AlignedCodonFrame> set1 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> set1 = new ArrayList<>();
     set1.add(acf1);
     set1.add(acf2);
-    List<AlignedCodonFrame> set2 = new ArrayList<AlignedCodonFrame>();
+    List<AlignedCodonFrame> set2 = new ArrayList<>();
     set2.add(acf2);
     set2.add(acf3);
 
@@ -218,7 +218,7 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase
     assertEquals(1, pmap.getSeqs().size());
     assertEquals("4IM2|A", pmap.getSeqs().get(0).getName());
 
-    List<int[]> structuremap1 = new ArrayList(
+    List<int[]> structuremap1 = new ArrayList<>(
             sm.getMapping(P4IM2_MISSING)[0]
                     .getPDBResNumRanges(seq.getStart(), seq.getEnd()));
 
@@ -313,8 +313,7 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase
     // positional mapping to atoms for color by structure is still wrong, even
     // though panel looks correct.
 
-    StructureMappingcommandSet smcr[] = JmolCommands
-            .getColourBySequenceCommand(apssm,
+    String[] smcr = new JmolCommands().colourBySequence(apssm,
             new String[]
             { pdbe.getFile() },
             new SequenceI[][]
@@ -322,12 +321,10 @@ public class StructureSelectionManagerTest extends Jalview2xmlBase
                     new SequenceRenderer(alf.alignPanel.getAlignViewport()),
                     alf.alignPanel);
     // Expected - all residues are white
-    for (StructureMappingcommandSet smm : smcr)
+    for (String c : smcr)
     {
-      for (String c : smm.commands)
-      {
-        System.out.println(c);
-      }
+      assertTrue(c.contains("color[255,255,255]"));
+      System.out.println(c);
     }
   }
 
index af02d5e..c1ad03a 100644 (file)
  */
 package jalview.structures.models;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
-import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
 
 import jalview.api.AlignmentViewPanel;
-import jalview.api.FeatureRenderer;
 import jalview.api.SequenceRenderer;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
-import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.ext.rbvi.chimera.ChimeraCommands;
+import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
+import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormats;
-import jalview.schemes.ColourSchemeI;
+import jalview.io.FileLoader;
+import jalview.schemes.JalviewColourScheme;
 import jalview.structure.AtomSpec;
-import jalview.structure.StructureMappingcommandSet;
+import jalview.structure.AtomSpecModel;
+import jalview.structure.StructureCommandI;
+import jalview.structure.StructureMapping;
 import jalview.structure.StructureSelectionManager;
-import jalview.structures.models.AAStructureBindingModel.SuperposeData;
-
-import java.awt.Color;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.List;
-
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
+import junit.extensions.PA;
 
 /**
  * Unit tests for non-abstract methods of abstract base class
@@ -139,110 +147,55 @@ public class AAStructureBindingModelTest
       @Override
       public void updateColours(Object source)
       {
-        // TODO Auto-generated method stub
-        
       }
       
       @Override
       public void releaseReferences(Object svl)
       {
-        // TODO Auto-generated method stub
-        
       }
       
       @Override
       public String[] getStructureFiles()
       {
-        // TODO Auto-generated method stub
-        return null;
-      }
-      
-      @Override
-      public String superposeStructures(AlignmentI[] alignments,
-              int[] structureIndices, HiddenColumns[] hiddenCols)
-      {
-        // TODO Auto-generated method stub
         return null;
       }
       
       @Override
-      public void setJalviewColourScheme(ColourSchemeI cs)
-      {
-        // TODO Auto-generated method stub
-        
-      }
-      
-      @Override
-      public void setBackgroundColour(Color col)
-      {
-        // TODO Auto-generated method stub
-        
-      }
-      
-      @Override
       public void highlightAtoms(List<AtomSpec> atoms)
       {
-        // TODO Auto-generated method stub
-        
       }
       
       @Override
       public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
       {
-        // TODO Auto-generated method stub
         return null;
       }
-      
+
       @Override
-      public FeatureRenderer getFeatureRenderer(AlignmentViewPanel alignment)
+      protected List<String> executeCommand(StructureCommandI command,
+              boolean getReply)
       {
-        // TODO Auto-generated method stub
         return null;
       }
-      
+
       @Override
-      protected StructureMappingcommandSet[] getColourBySequenceCommands(
-              String[] files, SequenceRenderer sr, AlignmentViewPanel avp)
+      protected String getModelIdForFile(String chainId)
       {
-        // TODO Auto-generated method stub
-        return null;
+        return "";
       }
-      
+
       @Override
-      public List<String> getChainNames()
+      protected ViewerType getViewerType()
       {
-        // TODO Auto-generated method stub
         return null;
       }
-      
-      @Override
-      protected void colourBySequence(
-              StructureMappingcommandSet[] colourBySequenceCommands)
-      {
-        // TODO Auto-generated method stub
-        
-      }
-      
-      @Override
-      public void colourByCharge()
-      {
-        // TODO Auto-generated method stub
-        
-      }
-      
-      @Override
-      public void colourByChain()
-      {
-        // TODO Auto-generated method stub
-        
-      }
     };
     String[][] chains = binder.getChains();
     assertFalse(chains == null || chains[0] == null,
             "No chains discovered by binding");
-    assertEquals(2, chains[0].length);
-    assertEquals("A", chains[0][0]);
-    assertEquals("B", chains[0][1]);
+    assertEquals(chains[0].length, 2);
+    assertEquals(chains[0][0], "A");
+    assertEquals(chains[0][1], "B");
   }
   AAStructureBindingModel testee;
 
@@ -281,12 +234,33 @@ public class AAStructureBindingModelTest
     ssm.setMapping(new SequenceI[] { seq3 }, null, PDB_3,
             DataSourceType.PASTE, null);
 
-    testee = new AAStructureBindingModel(ssm, pdbFiles, seqs, null)
+    testee = newBindingModel(pdbFiles, seqs, ssm, null);
+  }
+
+  /**
+   * A helper method to construct the test target object
+   * 
+   * @param pdbFiles
+   * @param seqs
+   * @param ssm
+   * @param alignPanel 
+   */
+  protected AAStructureBindingModel newBindingModel(PDBEntry[] pdbFiles,
+          SequenceI[][] seqs,
+          StructureSelectionManager ssm, AlignmentViewPanel avp)
+  {
+    AAStructureBindingModel model = new AAStructureBindingModel(ssm,
+            pdbFiles, seqs, null)
     {
       @Override
       public String[] getStructureFiles()
       {
-        return new String[] { "INLINE1YCS", "INLINE3A6S", "INLINE1OOT" };
+        String[] files = new String[getPdbCount()];
+        for (int i = 0; i < this.getPdbCount(); i++)
+        {
+          files[i] = getPdbEntry(i).getFile();
+        }
+        return files;
       }
 
       @Override
@@ -305,65 +279,46 @@ public class AAStructureBindingModelTest
       }
 
       @Override
-      public List<String> getChainNames()
-      {
-        return null;
-      }
-
-      @Override
-      public void setJalviewColourScheme(ColourSchemeI cs)
-      {
-      }
-
-      @Override
-      public String superposeStructures(AlignmentI[] als, int[] alm,
-              HiddenColumns[] alc)
-      {
-        return null;
-      }
-
-      @Override
-      public void setBackgroundColour(Color col)
-      {
-      }
-
-      @Override
-      protected StructureMappingcommandSet[] getColourBySequenceCommands(
-              String[] files, SequenceRenderer sr, AlignmentViewPanel avp)
-      {
-        return null;
-      }
-
-      @Override
       public SequenceRenderer getSequenceRenderer(
-              AlignmentViewPanel alignment)
+              AlignmentViewPanel avp)
       {
-        return null;
+        return avp == null ? null
+                : new jalview.gui.SequenceRenderer(
+                        avp.getAlignViewport());
       }
 
       @Override
-      protected void colourBySequence(
-              StructureMappingcommandSet[] colourBySequenceCommands)
-      {
-      }
-
-      @Override
-      public void colourByChain()
+      protected List<String> executeCommand(StructureCommandI command,
+              boolean getReply)
       {
+        return null;
       }
 
+      /*
+       * for this test, let structure model ids be 0, 1, ...
+       * corresponding to first, second etc pdbfile
+       */
       @Override
-      public void colourByCharge()
+      protected String getModelIdForFile(String pdbfile)
       {
+        for (int i = 0; i < this.getPdbCount(); i++)
+        {
+          if (pdbfile.equals(this.getPdbEntry(i).getFile()))
+          {
+            return String.valueOf(i);
+          }
+        }
+        return "";
       }
 
       @Override
-      public FeatureRenderer getFeatureRenderer(
-              AlignmentViewPanel alignment)
+      protected ViewerType getViewerType()
       {
         return null;
       }
     };
+    PA.setValue(model, "commandGenerator", new ChimeraCommands());
+    return model;
   }
 
   /**
@@ -376,10 +331,10 @@ public class AAStructureBindingModelTest
     /*
      * create a data bean to hold data per structure file
      */
-    SuperposeData[] structs = new SuperposeData[testee.getStructureFiles().length];
+    AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[testee.getStructureFiles().length];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = testee.new SuperposeData(al.getWidth());
+      structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0");
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for
@@ -394,7 +349,7 @@ public class AAStructureBindingModelTest
     int refStructure = testee
             .findSuperposableResidues(al, matched, structs);
 
-    assertEquals(0, refStructure);
+    assertEquals(refStructure, 0);
 
     /*
      * only ungapped, structure-mapped columns are superposable
@@ -406,27 +361,28 @@ public class AAStructureBindingModelTest
     assertTrue(matched.get(4));
     assertTrue(matched.get(5)); // gap in second sequence
 
-    assertEquals("1YCS", structs[0].pdbId);
-    assertEquals("3A6S", structs[1].pdbId);
-    assertEquals("1OOT", structs[2].pdbId);
-    assertEquals("A", structs[0].chain); // ? struct has chains A _and_ B
-    assertEquals("B", structs[1].chain);
-    assertEquals("A", structs[2].chain);
+    assertEquals(structs[0].pdbId, "1YCS");
+    assertEquals(structs[1].pdbId, "3A6S");
+    assertEquals(structs[2].pdbId, "1OOT");
+    assertEquals(structs[0].chain, "A"); // ? struct has chains A _and_ B
+    assertEquals(structs[1].chain, "B");
+    assertEquals(structs[2].chain, "A");
     // the 0's for unsuperposable positions propagate down the columns:
-    assertEquals("[0, 97, 98, 99, 100, 102]",
-            Arrays.toString(structs[0].pdbResNo));
-    assertEquals("[0, 2, 0, 3, 4, 5]", Arrays.toString(structs[1].pdbResNo));
-    assertEquals("[0, 8, 0, 0, 10, 12]",
-            Arrays.toString(structs[2].pdbResNo));
+    assertEquals(Arrays.toString(structs[0].pdbResNo),
+            "[0, 97, 98, 99, 100, 102]");
+    assertEquals(Arrays.toString(structs[1].pdbResNo),
+            "[0, 2, 0, 3, 4, 5]");
+    assertEquals(Arrays.toString(structs[2].pdbResNo),
+            "[0, 8, 0, 0, 10, 12]");
   }
 
   @Test(groups = { "Functional" })
   public void testFindSuperposableResidues_hiddenColumn()
   {
-    SuperposeData[] structs = new SuperposeData[al.getHeight()];
+    AAStructureBindingModel.SuperposeData[] structs = new AAStructureBindingModel.SuperposeData[al.getHeight()];
     for (int i = 0; i < structs.length; i++)
     {
-      structs[i] = testee.new SuperposeData(al.getWidth());
+      structs[i] = new AAStructureBindingModel.SuperposeData(al.getWidth(), "0");
     }
     /*
      * initialise BitSet of 'superposable columns' to true (would be false for
@@ -444,7 +400,7 @@ public class AAStructureBindingModelTest
     int refStructure = testee
             .findSuperposableResidues(al, matched, structs);
 
-    assertEquals(0, refStructure);
+    assertEquals(refStructure, 0);
 
     // only ungapped, structure-mapped columns are not superposable
     assertFalse(matched.get(0));
@@ -454,4 +410,100 @@ public class AAStructureBindingModelTest
     assertFalse(matched.get(4)); // superposable, but hidden, column
     assertTrue(matched.get(5));
   }
-}
+
+  @Test(groups = { "Functional" })
+  public void testBuildColoursMap()
+  {
+    /*
+     * load these sequences, coloured by Strand propensity,
+     * with columns 2-4 hidden
+     */
+    String fasta = ">seq1\nMHRSQSSSGG\n>seq2\nMVRSNGGSSS";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(fasta,
+            DataSourceType.PASTE);
+    AlignmentI al = af.getViewport().getAlignment();
+    af.changeColour_actionPerformed(JalviewColourScheme.Strand.toString());
+    ColumnSelection cs = new ColumnSelection();
+    cs.addElement(2);
+    cs.addElement(3);
+    cs.addElement(4);
+    af.getViewport().setColumnSelection(cs);
+    af.hideSelColumns_actionPerformed(null);
+    SequenceI seq1 = al.getSequenceAt(0);
+    SequenceI seq2 = al.getSequenceAt(1);
+    SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
+    PDBEntry[] pdbFiles = new PDBEntry[2];
+    pdbFiles[0] = new PDBEntry("PDB1", "A", Type.PDB, "seq1.pdb");
+    pdbFiles[1] = new PDBEntry("PDB2", "B", Type.PDB, "seq2.pdb");
+    StructureSelectionManager ssm = new StructureSelectionManager();
+  
+    /*
+     * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
+     */
+    HashMap<Integer, int[]> map = new HashMap<>();
+    for (int pos = 1; pos <= seq1.getLength(); pos++)
+    {
+      map.put(pos, new int[] { 20 + pos, 5 * (20 + pos) });
+    }
+    StructureMapping sm1 = new StructureMapping(seq1, "seq1.pdb", "pdb1",
+            "A", map, null);
+    ssm.addStructureMapping(sm1);
+    StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
+            "B", map, null);
+    ssm.addStructureMapping(sm2);
+
+    AAStructureBindingModel binding = newBindingModel(pdbFiles, seqs, ssm,
+            af.alignPanel);
+
+    /*
+     * method under test builds a map of structures residues by colour
+     * verify the map holds what it should
+     */
+    Map<Object, AtomSpecModel> colours = binding.buildColoursMap(ssm, seqs,
+            af.alignPanel);
+    ChimeraCommands helper = new ChimeraCommands();
+    
+    /*
+     * M colour is #82827d (see strand.html help page)
+     * sequence residue 1 mapped to structure residue 21
+     */
+    Color mColor = new Color(0x82827d);
+    AtomSpecModel atomSpec = colours.get(mColor);
+    assertNotNull(atomSpec);
+    assertEquals(helper.getAtomSpec(atomSpec, false), "#0:21.A|#1:21.B");
+
+    /*
+     * H colour is #60609f, seq1.2 mapped to structure 0 residue 22
+     */
+    Color hColor = new Color(0x60609f);
+    atomSpec = colours.get(hColor);
+    assertNotNull(atomSpec);
+    assertEquals(helper.getAtomSpec(atomSpec, false), "#0:22.A");
+
+    /*
+     * V colour is #ffff00, seq2.2 mapped to structure 1 residue 22
+     */
+    Color vColor = new Color(0xffff00);
+    atomSpec = colours.get(vColor);
+    assertNotNull(atomSpec);
+    assertEquals(helper.getAtomSpec(atomSpec, false), "#1:22.B");
+
+    /*
+     * hidden columns are Gray (128, 128, 128)
+     * sequence positions 3-5 mapped to structure residues 23-25
+     */
+    Color gray = new Color(128, 128, 128);
+    atomSpec = colours.get(gray);
+    assertNotNull(atomSpec);
+    assertEquals(helper.getAtomSpec(atomSpec, false), "#0:23-25.A|#1:23-25.B");
+
+    /*
+     * S and G are both coloured #4949b6, structure residues 26-30
+     */
+    Color sgColour = new Color(0x4949b6);
+    atomSpec = colours.get(sgColour);
+    assertNotNull(atomSpec);
+    assertEquals(helper.getAtomSpec(atomSpec, false),
+            "#0:26-30.A|#1:26-30.B");
+  }
+}
\ No newline at end of file
index d3e6221..f92e454 100644 (file)
@@ -136,6 +136,11 @@ public class DBRefUtilsTest
     assertEquals("1.2", ref.getVersion());
     assertEquals("a7890", ref.getAccessionId());
     assertTrue(seq.getAllPDBEntries().isEmpty());
+    SequenceI seq2 = new Sequence("Seq2", "ABCD");
+    // Check that whitespace doesn't confuse parseToDbRef
+    DBRefEntry ref2 = DBRefUtils.parseToDbRef(seq2, "EMBL", "1.2",
+            " a7890");
+    assertEquals(ref, ref2);
   }
 
   /**
index a47fb60..c964195 100644 (file)
@@ -3,6 +3,7 @@ package jalview.util.matcher;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 import static org.testng.Assert.fail;
 
@@ -10,6 +11,7 @@ import java.util.Locale;
 
 import org.testng.annotations.Test;
 
+import jalview.util.matcher.Matcher.PatternType;
 import junit.extensions.PA;
 
 public class MatcherTest
@@ -21,22 +23,44 @@ public class MatcherTest
     assertEquals(m.getCondition(), Condition.Contains);
     assertEquals(m.getPattern(), "foo");
     assertEquals(PA.getValue(m, "uppercasePattern"), "FOO");
-    assertEquals(m.getFloatValue(), 0f);
+    assertEquals(PA.getValue(m, "floatValue"), 0f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.String);
 
     m = new Matcher(Condition.GT, -2.1f);
     assertEquals(m.getCondition(), Condition.GT);
     assertEquals(m.getPattern(), "-2.1");
-    assertEquals(m.getFloatValue(), -2.1f);
+    assertEquals(PA.getValue(m, "floatValue"), -2.1f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.Float);
 
     m = new Matcher(Condition.NotContains, "-1.2f");
     assertEquals(m.getCondition(), Condition.NotContains);
     assertEquals(m.getPattern(), "-1.2f");
-    assertEquals(m.getFloatValue(), 0f);
+    assertEquals(PA.getValue(m, "floatValue"), 0f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.String);
 
     m = new Matcher(Condition.GE, "-1.2f");
     assertEquals(m.getCondition(), Condition.GE);
     assertEquals(m.getPattern(), "-1.2");
-    assertEquals(m.getFloatValue(), -1.2f);
+    assertEquals(PA.getValue(m, "floatValue"), -1.2f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.Float);
+
+    m = new Matcher(Condition.GE, "113890813");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "113890813");
+    assertEquals(PA.getValue(m, "floatValue"), 0f);
+    assertEquals(PA.getValue(m, "longValue"), 113890813L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.Integer);
+
+    m = new Matcher(Condition.GE, "-987f");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "-987.0");
+    assertEquals(PA.getValue(m, "floatValue"), -987f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.Float);
 
     try
     {
@@ -53,7 +77,25 @@ public class MatcherTest
       fail("Expected exception");
     } catch (NumberFormatException e)
     {
-      // expected
+      // expected - see Long.valueOf()
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123_456");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected - see Long.valueOf()
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123456L");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected - see Long.valueOf()
     }
   }
 
@@ -82,7 +124,7 @@ public class MatcherTest
     /*
      * >= test
      */
-    m = new Matcher(Condition.GE, 2f);
+    m = new Matcher(Condition.GE, "2f");
     assertTrue(m.matches("2"));
     assertTrue(m.matches("2.1"));
     assertFalse(m.matches("1.9"));
@@ -98,7 +140,7 @@ public class MatcherTest
     /*
      * <= test
      */
-    m = new Matcher(Condition.LE, 2f);
+    m = new Matcher(Condition.LE, "2.0f");
     assertTrue(m.matches("2"));
     assertFalse(m.matches("2.1"));
     assertTrue(m.matches("1.9"));
@@ -112,17 +154,25 @@ public class MatcherTest
     assertTrue(m.matches("1.9"));
   }
 
+  /**
+   * Verifies that all numeric match conditions fail when applied to non-numeric
+   * or null values
+   */
   @Test(groups = "Functional")
-  public void testMatches_floatNullOrInvalid()
+  public void testNumericMatch_nullOrInvalidValue()
   {
     for (Condition cond : Condition.values())
     {
       if (cond.isNumeric())
       {
-        MatcherI m = new Matcher(cond, 2f);
-        assertFalse(m.matches(null));
-        assertFalse(m.matches(""));
-        assertFalse(m.matches("two"));
+        MatcherI m1 = new Matcher(cond, 2.1f);
+        MatcherI m2 = new Matcher(cond, 2345L);
+        assertFalse(m1.matches(null));
+        assertFalse(m1.matches(""));
+        assertFalse(m1.matches("two"));
+        assertFalse(m2.matches(null));
+        assertFalse(m2.matches(""));
+        assertFalse(m2.matches("two"));
       }
     }
   }
@@ -166,7 +216,7 @@ public class MatcherTest
      */
     m = new Matcher(Condition.NotMatches, "benign");
     assertFalse(m.matches("benign"));
-    assertFalse(m.matches(" Benign ")); // trim before testing
+    assertFalse(m.matches(" Benign ")); // trimmed before testing
     assertTrue(m.matches("MOSTLY BENIGN"));
     assertTrue(m.matches("pathogenic"));
     assertTrue(m.matches(null));
@@ -188,11 +238,18 @@ public class MatcherTest
     assertTrue(m.matches(null));
 
     /*
-     * a float with a string match condition will be treated as string
+     * a number with a string match condition will be treated as string
+     * (these cases shouldn't arise as the match() method is coded)
      */
     Matcher m1 = new Matcher(Condition.Contains, "32");
-    assertFalse(m1.matches(-203f));
-    assertTrue(m1.matches(-4321.0f));
+    assertFalse(m1.matchesFloat("-203f", 0f));
+    assertTrue(m1.matchesFloat("-4321.0f", 0f));
+    assertFalse(m1.matchesFloat("-203", 0f));
+    assertTrue(m1.matchesFloat("-4321", 0f));
+    assertFalse(m1.matchesLong("-203"));
+    assertTrue(m1.matchesLong("-4321"));
+    assertFalse(m1.matchesLong("-203f"));
+    assertTrue(m1.matchesLong("-4321.0f"));
   }
 
   /**
@@ -202,9 +259,15 @@ public class MatcherTest
   public void testMatches_floatWithStringCondition()
   {
     MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+    assertEquals(m.getPattern(), "1.2E-6");
+    assertEquals(PA.getValue(m, "uppercasePattern"), "1.2E-6");
+    assertEquals(PA.getValue(m, "floatValue"), 0f);
+    assertEquals(PA.getValue(m, "longValue"), 0L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.String);
     assertTrue(m.matches("1.2e-6"));
 
     m = new Matcher(Condition.Contains, 0.0000001f);
+    assertEquals(m.getPattern(), "1.0E-7");
     assertTrue(m.matches("1.0e-7"));
     assertTrue(m.matches("1.0E-7"));
     assertFalse(m.matches("0.0000001f"));
@@ -218,6 +281,9 @@ public class MatcherTest
     MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
     assertEquals(m.toString(), "< 1.2E-6");
 
+    m = new Matcher(Condition.GE, "20200413");
+    assertEquals(m.toString(), ">= 20200413");
+
     m = new Matcher(Condition.NotMatches, "ABC");
     assertEquals(m.toString(), "Does not match 'ABC'");
 
@@ -242,7 +308,7 @@ public class MatcherTest
     assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
 
     /*
-     * numeric conditions
+     * numeric conditions - float values
      */
     m = new Matcher(Condition.LT, -1f);
     assertFalse(m.equals(null));
@@ -256,6 +322,19 @@ public class MatcherTest
     assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
     assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
     assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+
+    /*
+     * numeric conditions - integer values
+     */
+    m = new Matcher(Condition.LT, -123456);
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.LT, -123456)));
+    assertFalse(m.equals(new Matcher(Condition.LT, +123456)));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-123456")));
+    assertFalse(m.equals(new Matcher(Condition.LT, -123456f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, "-123456f")));
   }
 
   @Test(groups = "Functional")
@@ -270,4 +349,76 @@ public class MatcherTest
     assertNotEquals(m1.hashCode(), m4.hashCode());
     assertNotEquals(m3.hashCode(), m4.hashCode());
   }
+
+  /**
+   * Tests for integer comparison conditions
+   */
+  @Test(groups = "Functional")
+  public void testMatches_long()
+  {
+    /*
+     * EQUALS test
+     */
+    MatcherI m = new Matcher(Condition.EQ, 2);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("+2"));
+    assertFalse(m.matches("3"));
+    // a float value may be passed to an integer matcher
+    assertTrue(m.matches("2.0"));
+    assertTrue(m.matches("2.000000f"));
+    assertFalse(m.matches("2.01"));
+  
+    /*
+     * NOT EQUALS test
+     */
+    m = new Matcher(Condition.NE, 123);
+    assertFalse(m.matches("123"));
+    assertFalse(m.matches("123.0"));
+    assertTrue(m.matches("-123"));
+  
+    /*
+     * >= test
+     */
+    m = new Matcher(Condition.GE, "113890813");
+    assertTrue(m.matches("113890813"));
+    assertTrue(m.matches("113890814"));
+    assertFalse(m.matches("-113890813"));
+  
+    /*
+     * > test
+     */
+    m = new Matcher(Condition.GT, 113890813);
+    assertFalse(m.matches("113890813"));
+    assertTrue(m.matches("113890814"));
+  
+    /*
+     * <= test
+     */
+    m = new Matcher(Condition.LE, "113890813");
+    assertTrue(m.matches("113890813"));
+    assertFalse(m.matches("113890814"));
+    assertTrue(m.matches("113890812"));
+  
+    /*
+     * < test
+     */
+    m = new Matcher(Condition.LT, 113890813);
+    assertFalse(m.matches("113890813"));
+    assertFalse(m.matches("113890814"));
+    assertTrue(m.matches("113890812"));
+  }
+
+  /**
+   * Tests comparing a float value with an integer condition
+   */
+  @Test(groups = "Functional")
+  public void testMatches_floatValueIntegerCondition()
+  {
+    MatcherI m = new Matcher(Condition.GT, 1234);
+    assertEquals(PA.getValue(m, "longValue"), 1234L);
+    assertSame(PA.getValue(m, "patternType"), PatternType.Integer);
+    assertTrue(m.matches("1235"));
+    assertTrue(m.matches("9867.345"));
+    assertTrue(m.matches("9867.345f"));
+  }
 }
index f5cc640..23cceb2 100644 (file)
@@ -38,7 +38,7 @@ public class PfamFullTest
   @Test(groups = "Functional")
   public void testGetURL()
   {
-    String path = "pfam.xfam.org/family/ABC/alignment/full";
+    String path = "pfam.xfam.org/family/ABC/alignment/full/gzipped";
 
     // with default value for domain
     String url = new PfamFull().getURL(" abc ");
index 355ef0c..451810b 100644 (file)
@@ -38,7 +38,7 @@ public class PfamSeedTest
   @Test(groups = "Functional")
   public void testGetURL()
   {
-    String path = "pfam.xfam.org/family/ABC/alignment/seed";
+    String path = "pfam.xfam.org/family/ABC/alignment/seed/gzipped";
 
     // with default value for domain
     String url = new PfamSeed().getURL(" abc ");
index 2d1497f..87b963f 100644 (file)
@@ -38,7 +38,7 @@ public class RfamFullTest
   @Test(groups = "Functional")
   public void testGetURL()
   {
-    String path = "rfam.xfam.org/family/ABC/alignment/full";
+    String path = "rfam.xfam.org/family/ABC/alignment/full?gzip=1&download=1";
 
     // with default value for domain
     String url = new RfamFull().getURL(" abc ");
index 745ba2e..1165d1f 100644 (file)
@@ -38,7 +38,7 @@ public class RfamSeedTest
   @Test(groups = "Functional")
   public void testGetURL()
   {
-    String path = "rfam.xfam.org/family/ABC/alignment/stockholm";
+    String path = "rfam.xfam.org/family/ABC/alignment/stockholm?gzip=1&download=1";
 
     // with default value for domain
     String url = new RfamSeed().getURL(" abc ");
index c870f6d..b1927e1 100644 (file)
@@ -1,3 +1,4 @@
+
 /*
  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
  * Copyright (C) $$Year-Rel$$ The Jalview Authors
@@ -23,6 +24,7 @@ import java.io.File;
 import java.io.FileReader;
 import java.io.IOException;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeSet;
@@ -127,8 +129,9 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
    */
   private void doMain(String srcPath) throws IOException
   {
-    System.out.println("Scanning " + srcPath
-            + " for calls to MessageManager\n");
+    System.out.println(
+            "Scanning " + srcPath + " for calls to MessageManager\n");
+
     sourcePath = srcPath;
     loadMessages();
     File dir = new File(srcPath);
@@ -157,18 +160,19 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
    */
   private void reportResults()
   {
-    System.out.println("\nScanned " + javaCount + " source files");
     System.out.println(
-            "Messages.properties has " + messages.size()
-            + " keys");
+            "\nMessages.properties has " + messages.size() + " keys");
+    System.out.println("Scanned " + javaCount + " source files\n");
+
     if (!invalidKeys.isEmpty())
     {
       System.out.println("Found " + invalidKeys.size()
-              + " possibly invalid parameter call"
+              + " possibly invalid unmatched key(s) in source code"
               + (invalidKeys.size() > 1 ? "s" : ""));
     }
 
-    System.out.println("Keys not found, assumed constructed dynamically:");
+    System.out.println(
+            "Keys not found in source code, assumed constructed dynamically:");
     int dynamicCount = 0;
     for (String key : messageKeys)
     {
@@ -182,7 +186,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
     if (dynamicCount < messageKeys.size())
     {
       System.out.println((messageKeys.size() - dynamicCount)
-              + " keys not found, possibly unused");
+              + " key(s) not found, possibly unused, or used indirectly (check code manually!)");
       for (String key : messageKeys)
       {
         if (!isDynamic(key))
@@ -192,7 +196,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
       }
     }
     System.out
-            .println("(Run i18nAnt.xml to compare other message bundles)");
+            .println("\nRun i18nAnt.xml to compare other message bundles");
   }
 
   /**
@@ -284,11 +288,11 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
    * @param lineNo
    * @param line
    */
-  private void inspectSourceLines(String path, int lineNo, String line)
+  private void inspectSourceLines(String path, int lineNo,
+          final String line)
   {
-    String lineNos = String
-            .format("%d-%d", lineNo, lineNo + bufferSize
-            - 1);
+    String lineNos = String.format("%d-%d", lineNo,
+            lineNo + bufferSize - 1);
     for (String method : METHODS)
     {
       int pos = line.indexOf(method);
@@ -328,10 +332,14 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 
       if (METHOD3 == method)
       {
-        System.out.println(String.format("Dynamic key at %s line %s %s",
-                path.substring(sourcePath.length()), lineNos, line));
         String key = messageKey.substring(1, messageKey.length() - 1);
-        dynamicKeys.add(key);
+        if (!dynamicKeys.contains(key))
+        {
+//          System.out.println(String.format(
+//                  "Dynamic key \"" + key + "\" at %s line %s %s",
+//                  path.substring(sourcePath.length()), lineNos, line));
+          dynamicKeys.add(key);
+        }
         continue;
       }
 
@@ -344,8 +352,8 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 
       if (!(STRING_PATTERN.matcher(messageKey).matches()))
       {
-        System.out.println(String.format("Dynamic key at %s line %s %s",
-                path.substring(sourcePath.length()), lineNos, line));
+//        System.out.println(String.format("Dynamic key at %s line %s %s",
+//                path.substring(sourcePath.length()), lineNos, line));
         continue;
       }
 
@@ -356,16 +364,33 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 
       if (!this.messages.containsKey(messageKey))
       {
-        System.out.println(String.format(
-                "Unmatched key '%s' at line %s of %s", messageKey, lineNos,
-                path.substring(sourcePath.length())));
         if (!invalidKeys.contains(messageKey))
         {
+          // report each key the first time found only
+          System.out.println(String.format(
+                  "Unmatched key '%s' at line %s of %s", messageKey,
+                  lineNos, path.substring(sourcePath.length())));
           invalidKeys.add(messageKey);
         }
       }
       messageKeys.remove(messageKey);
     }
+
+    /*
+     * and a brute force scan for _any_ as yet unseen message key, to catch the
+     * cases where it is in a variable or a condition
+     */
+    if (line.contains("\""))
+    {
+      Iterator<String> it = messageKeys.iterator();
+      while (it.hasNext())
+      {
+        if (line.contains(it.next()))
+        {
+          it.remove(); // remove as 'seen'
+        }
+      }
+    }
   }
 
   /**
@@ -399,8 +424,8 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
      */
     int commaPos = key.indexOf(",");
     int bracePos = key.indexOf(")");
-    int endPos = commaPos == -1 ? bracePos : (bracePos == -1 ? commaPos
-            : Math.min(commaPos, bracePos));
+    int endPos = commaPos == -1 ? bracePos
+            : (bracePos == -1 ? commaPos : Math.min(commaPos, bracePos));
     if (endPos == -1 && key.length() > 1 && key.endsWith("\""))
     {
       endPos = key.length();
@@ -417,8 +442,8 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
   void loadMessages() throws IOException
   {
     messages = new Properties();
-    FileReader reader = new FileReader(new File(sourcePath,
-            "../resources/lang/Messages.properties"));
+    FileReader reader = new FileReader(
+            new File(sourcePath, "../resources/lang/Messages.properties"));
     messages.load(reader);
     reader.close();
 
diff --git a/utils/clover/lib/clover-ant-4.4.1.jar b/utils/clover/lib/clover-ant-4.4.1.jar
new file mode 100644 (file)
index 0000000..4f92395
Binary files /dev/null and b/utils/clover/lib/clover-ant-4.4.1.jar differ
diff --git a/utils/cmd-nox.sh b/utils/cmd-nox.sh
new file mode 100755 (executable)
index 0000000..bd615bd
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+CMD=$(basename $0)
+CMD=${CMD%-nox.sh}
+
+echo "Running '$CMD' headlessly"
+
+xvfb-run -s "-screen 0 1280x800x16" -e /dev/stdout -a $CMD ${@}
similarity index 100%
rename from doc/github.css
rename to utils/doc/github.css
index 93bda1c..7dd41d6 100644 (file)
@@ -1,2 +1,69 @@
 eclipse.preferences.version=1
 cleanup.remove_redundant_type_arguments=false
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=true
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_missing_override_annotations_interface_methods=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_functional_interfaces=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=true
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.insert_inferred_type_arguments=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=false
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.number_suffix=false
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=true
+sp_cleanup.push_down_negation=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_redundant_modifiers=false
+sp_cleanup.remove_redundant_semicolons=false
+sp_cleanup.remove_redundant_type_arguments=false
+sp_cleanup.remove_trailing_whitespaces=false
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_array_creation=false
+sp_cleanup.remove_unnecessary_casts=true
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.simplify_lambda_expression_and_method_ref=false
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_anonymous_class_creation=false
+sp_cleanup.use_autoboxing=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_directly_map_method=false
+sp_cleanup.use_lambda=true
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+sp_cleanup.use_unboxing=false
diff --git a/utils/gradle-nox.sh b/utils/gradle-nox.sh
new file mode 120000 (symlink)
index 0000000..4abb9a2
--- /dev/null
@@ -0,0 +1 @@
+cmd-nox.sh
\ No newline at end of file
index eb585d9..a02cd6d 100644 (file)
@@ -684,7 +684,7 @@ return console.askYesNo(message, true);
               </action>
               <action id="1525" beanClass="com.install4j.runtime.beans.actions.files.DeleteFileAction" actionElevationType="elevated" rollbackBarrierExitCode="0">
                 <serializedBean>
-                  <property name="files" type="array" class="java.io.File" length="32">
+                  <property name="files" type="array" class="java.io.File" length="36">
                     <element index="0">
                       <object class="java.io.File">
                         <string>jre</string>
@@ -702,117 +702,117 @@ return console.askYesNo(message, true);
                     </element>
                     <element index="3">
                       <object class="java.io.File">
-                        <string>${compiler:GETDOWN_DIST_DIR}</string>
+                        <string>getdown-launcher.jar</string>
                       </object>
                     </element>
                     <element index="4">
                       <object class="java.io.File">
-                        <string>${compiler:GETDOWN_ALT_DIR}</string>
+                        <string>getdown-launcher-old.jar</string>
                       </object>
                     </element>
                     <element index="5">
                       <object class="java.io.File">
-                        <string>${compiler:GETDOWN_RESOURCE_DIR}</string>
+                        <string>getdown-launcher-new.jar</string>
                       </object>
                     </element>
                     <element index="6">
                       <object class="java.io.File">
-                        <string>getdown-launcher.jar</string>
+                        <string>gettingdown.lock</string>
                       </object>
                     </element>
                     <element index="7">
                       <object class="java.io.File">
-                        <string>getdown-launcher-old.jar</string>
+                        <string>jre.zip</string>
                       </object>
                     </element>
                     <element index="8">
                       <object class="java.io.File">
-                        <string>getdown-launcher-new.jar</string>
+                        <string>digest.txt</string>
                       </object>
                     </element>
                     <element index="9">
                       <object class="java.io.File">
-                        <string>*.jarv</string>
+                        <string>digest2.txt</string>
                       </object>
                     </element>
                     <element index="10">
                       <object class="java.io.File">
-                        <string>gettingdown.lock</string>
+                        <string>getdown-launcher.jarv</string>
                       </object>
                     </element>
                     <element index="11">
                       <object class="java.io.File">
-                        <string>*.log</string>
+                        <string>getdown-launcher-new.jarv</string>
                       </object>
                     </element>
                     <element index="12">
                       <object class="java.io.File">
-                        <string>*.txt</string>
+                        <string>launcher.log</string>
                       </object>
                     </element>
                     <element index="13">
                       <object class="java.io.File">
-                        <string>*_new</string>
+                        <string>proxy.txt</string>
                       </object>
                     </element>
                     <element index="14">
                       <object class="java.io.File">
-                        <string>digest.txt</string>
+                        <string>build_properties</string>
                       </object>
                     </element>
                     <element index="15">
                       <object class="java.io.File">
-                        <string>digest2.txt</string>
+                        <string>channel_launch*.jvl</string>
                       </object>
                     </element>
                     <element index="16">
                       <object class="java.io.File">
-                        <string>getdown-launcher.jarv</string>
+                        <string>jalview*.jvl</string>
                       </object>
                     </element>
                     <element index="17">
                       <object class="java.io.File">
-                        <string>getdown-launcher-new.jarv</string>
+                        <string>*.jarv</string>
                       </object>
                     </element>
                     <element index="18">
                       <object class="java.io.File">
-                        <string>channel_launch*.jvl</string>
+                        <string>*.log</string>
                       </object>
                     </element>
                     <element index="19">
                       <object class="java.io.File">
-                        <string>launcher.log</string>
+                        <string>*.txt</string>
                       </object>
                     </element>
                     <element index="20">
                       <object class="java.io.File">
-                        <string>proxy.txt</string>
+                        <string>*_new</string>
                       </object>
                     </element>
                     <element index="21">
                       <object class="java.io.File">
-                        <string>META-INF</string>
+                        <string>hs_err_*.*</string>
                       </object>
                     </element>
                     <element index="22">
                       <object class="java.io.File">
-                        <string>install/getdown-launcher.jar</string>
+                        <string>${compiler:GETDOWN_DIST_DIR}</string>
                       </object>
                     </element>
                     <element index="23">
                       <object class="java.io.File">
-                        <string>install/getdown.txt</string>
+                        <string>${compiler:GETDOWN_ALT_DIR}</string>
                       </object>
                     </element>
                     <element index="24">
                       <object class="java.io.File">
-                        <string>install/build_properties</string>
+                        <string>${compiler:GETDOWN_RESOURCE_DIR}</string>
                       </object>
                     </element>
                     <element index="25">
                       <object class="java.io.File">
-                        <string>build_properties</string>
+                        <string>META-INF</string>
                       </object>
                     </element>
                     <element index="26">
@@ -822,27 +822,47 @@ return console.askYesNo(message, true);
                     </element>
                     <element index="27">
                       <object class="java.io.File">
-                        <string>dist</string>
+                        <string>resource</string>
                       </object>
                     </element>
                     <element index="28">
                       <object class="java.io.File">
-                        <string>release</string>
+                        <string>dist</string>
                       </object>
                     </element>
                     <element index="29">
                       <object class="java.io.File">
-                        <string>alt</string>
+                        <string>release</string>
                       </object>
                     </element>
                     <element index="30">
                       <object class="java.io.File">
-                        <string>resource</string>
+                        <string>alt</string>
                       </object>
                     </element>
                     <element index="31">
                       <object class="java.io.File">
-                        <string>hs_err_*.*</string>
+                        <string>dev</string>
+                      </object>
+                    </element>
+                    <element index="32">
+                      <object class="java.io.File">
+                        <string>build</string>
+                      </object>
+                    </element>
+                    <element index="33">
+                      <object class="java.io.File">
+                        <string>alt_*</string>
+                      </object>
+                    </element>
+                    <element index="34">
+                      <object class="java.io.File">
+                        <string>dev_*</string>
+                      </object>
+                    </element>
+                    <element index="35">
+                      <object class="java.io.File">
+                        <string>build_*</string>
                       </object>
                     </element>
                   </property>