Merge branch 'develop' into update_212_Dec_merge_with_21125_chamges
authorJim Procter <j.procter@dundee.ac.uk>
Wed, 14 Dec 2022 16:01:09 +0000 (16:01 +0000)
committerJim Procter <j.procter@dundee.ac.uk>
Wed, 14 Dec 2022 16:01:09 +0000 (16:01 +0000)
 Conflicts:
THIRDPARTYLIBS
build.gradle
gradle.properties
help/help/html/features/preferences.html
help/help/html/releases.html
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/datamodel/SequenceI.java
src/jalview/fts/service/uniprot/UniProtFTSRestClient.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureEditor.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OptsAndParamsPage.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/UserQuestionnaireCheck.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/WsJobParameters.java
src/jalview/gui/WsParamSetManager.java
src/jalview/io/BackupFiles.java
src/jalview/io/ModellerDescription.java
src/jalview/io/NewickFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/javascript/log4j/Logger.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GCutAndPasteHtmlTransfer.java
src/jalview/jbgui/GCutAndPasteTransfer.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/GPreferences.java
src/jalview/project/Jalview2XML.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/urls/IdentifiersUrlProvider.java
src/jalview/util/HttpUtils.java
src/jalview/util/MappingUtils.java
src/jalview/util/MessageManager.java
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/ws/AWSThread.java
src/jalview/ws/dbsources/EmblXmlSource.java
src/jalview/ws/gui/MsaWSThread.java
src/jalview/ws/jws1/Discoverer.java
src/jalview/ws/jws1/JPredClient.java
src/jalview/ws/jws1/JPredThread.java
src/jalview/ws/jws2/AADisorderClient.java
src/jalview/ws/jws2/AbstractJabaCalcWorker.java
src/jalview/ws/jws2/Jws2Client.java
src/jalview/ws/jws2/Jws2Discoverer.java
src/jalview/ws/jws2/MsaWSClient.java
src/jalview/ws/rest/HttpResultSet.java
src/jalview/ws/rest/RestClient.java
src/jalview/ws/rest/RestJobThread.java
src/jalview/ws/utils/UrlDownloadClient.java
src/jalview/xml/binding/jalview/DoubleVector.java
src/jalview/xml/binding/jalview/JalviewModel.java
test/jalview/bin/CommandLineOperations.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/rbvi/chimera/ChimeraCommandsTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/AlignmentPanelTest.java
test/jalview/gui/AnnotationChooserTest.java
test/jalview/gui/AnnotationRowFilterTest.java
test/jalview/gui/JvSwingUtilsTest.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/gui/SequenceRendererTest.java
test/jalview/gui/StructureChooserTest.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/renderer/OverviewResColourFinderTest.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
test/jalview/schemes/ClustalxColourSchemeTest.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/util/ColorUtilsTest.java
test/jalview/workers/AlignCalcManagerTest.java
test/jalview/ws/gui/Jws2ParamView.java
test/jalview/ws/sifts/SiftsClientTest.java

248 files changed:
1  2 
THIRDPARTYLIBS
build.gradle
gradle.properties
help/help/help.jhm
help/help/helpTOC.xml
help/help/html/features/preferences.html
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/AAFrequency.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/CrossRef.java
src/jalview/analysis/Dna.java
src/jalview/analysis/Finder.java
src/jalview/analysis/SeqsetUtils.java
src/jalview/api/AlignViewportI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/AlignmentPanel.java
src/jalview/appletgui/AnnotationPanel.java
src/jalview/appletgui/OverviewPanel.java
src/jalview/appletgui/SeqPanel.java
src/jalview/appletgui/TreeCanvas.java
src/jalview/appletgui/js/MouseOverListener.java
src/jalview/appletgui/js/MouseOverStructureListener.java
src/jalview/bin/ApplicationSingletonProvider.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewJS2.java
src/jalview/bin/JalviewLite.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/PDBEntry.java
src/jalview/datamodel/ResidueCount.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceGroup.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/ext/ensembl/EnsemblCds.java
src/jalview/ext/ensembl/EnsemblFeatures.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/ensembl/EnsemblSequenceFetcher.java
src/jalview/ext/ensembl/EnsemblXref.java
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/fts/service/uniprot/UniProtFTSRestClient.java
src/jalview/gui/AlignExportOptions.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationColourChooser.java
src/jalview/gui/AnnotationExporter.java
src/jalview/gui/AnnotationLabels.java
src/jalview/gui/AnnotationPanel.java
src/jalview/gui/AnnotationRowFilter.java
src/jalview/gui/AppJmol.java
src/jalview/gui/AppJmolBinding.java
src/jalview/gui/CalculationChooser.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/ColourMenuHelper.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/CutAndPasteTransfer.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureEditor.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/Finder.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OOMWarning.java
src/jalview/gui/OptsAndParamsPage.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PairwiseAlignPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/PromptUserConfig.java
src/jalview/gui/PymolViewer.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/SliderPanel.java
src/jalview/gui/SlivkaPreferences.java
src/jalview/gui/SplashScreen.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/TreeCanvas.java
src/jalview/gui/TreePanel.java
src/jalview/gui/UserDefinedColours.java
src/jalview/gui/UserQuestionnaireCheck.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/WebserviceInfo.java
src/jalview/gui/WsJobParameters.java
src/jalview/gui/WsParamSetManager.java
src/jalview/hmmer/HMMBuild.java
src/jalview/hmmer/HMMSearch.java
src/jalview/hmmer/HmmerCommand.java
src/jalview/hmmer/JackHMMER.java
src/jalview/io/AlignFile.java
src/jalview/io/AnnotationFile.java
src/jalview/io/AppletFormatAdapter.java
src/jalview/io/BackupFiles.java
src/jalview/io/BioJsHTMLOutput.java
src/jalview/io/FileFormat.java
src/jalview/io/FileFormats.java
src/jalview/io/FileLoader.java
src/jalview/io/IdentifyFile.java
src/jalview/io/ModellerDescription.java
src/jalview/io/NewickFile.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/StockholmFile.java
src/jalview/io/VamsasAppDatastore.java
src/jalview/io/gff/InterProScanHelper.java
src/jalview/io/vamsas/Sequencemapping.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GCutAndPasteTransfer.java
src/jalview/jbgui/GDesktop.java
src/jalview/jbgui/GPreferences.java
src/jalview/project/Jalview2XML.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/schemes/AnnotationColourGradient.java
src/jalview/schemes/ColourSchemes.java
src/jalview/schemes/JalviewColourScheme.java
src/jalview/schemes/ResidueProperties.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/urls/IdentifiersUrlProvider.java
src/jalview/util/BrowserLauncher.java
src/jalview/util/ColorUtils.java
src/jalview/util/Comparison.java
src/jalview/util/DBRefUtils.java
src/jalview/util/HttpUtils.java
src/jalview/util/MapList.java
src/jalview/util/MappingUtils.java
src/jalview/util/MessageManager.java
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/OverviewDimensionsShowHidden.java
src/jalview/viewmodel/ViewportRanges.java
src/jalview/workers/AlignCalcManager2.java
src/jalview/ws/AWSThread.java
src/jalview/ws/SequenceFetcher.java
src/jalview/ws/api/ServiceWithParameters.java
src/jalview/ws/dbsources/EmblFlatfileSource.java
src/jalview/ws/dbsources/EmblXmlSource.java
src/jalview/ws/dbsources/Pdb.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/gui/MsaWSThread.java
src/jalview/ws/jws1/Discoverer.java
src/jalview/ws/jws1/JPredClient.java
src/jalview/ws/jws1/JPredThread.java
src/jalview/ws/jws1/MsaWSThread.java
src/jalview/ws/jws1/SeqSearchWSClient.java
src/jalview/ws/jws1/SeqSearchWSThread.java
src/jalview/ws/jws2/Jws2Client.java
src/jalview/ws/jws2/Jws2ClientFactory.java
src/jalview/ws/jws2/Jws2Discoverer.java
src/jalview/ws/jws2/MsaWSClient.java
src/jalview/ws/jws2/SeqAnnotationServiceCalcWorker.java
src/jalview/ws/jws2/SequenceAnnotationWSClient.java
src/jalview/ws/jws2/jabaws2/AADisorderClient.java
src/jalview/ws/jws2/jabaws2/JabawsServiceInstance.java
src/jalview/ws/rest/HttpResultSet.java
src/jalview/ws/rest/InputType.java
src/jalview/ws/rest/RestClient.java
src/jalview/ws/rest/RestJobThread.java
src/jalview/ws/sifts/SiftsClient.java
src/jalview/ws/slivkaws/SlivkaAnnotationServiceInstance.java
src/jalview/ws/slivkaws/SlivkaWSDiscoverer.java
src/jalview/ws/utils/UrlDownloadClient.java
src/org/json/JSONObject.java
src/swingjs/api/JSUtilI.java
test/jalview/analysis/AAFrequencyTest.java
test/jalview/analysis/AlignmentGenerator.java
test/jalview/analysis/AlignmentSorterTest.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/CrossRefTest.java
test/jalview/analysis/FinderTest.java
test/jalview/analysis/SeqsetUtilsTest.java
test/jalview/analysis/scoremodels/FeatureDistanceModelTest.java
test/jalview/bin/CommandLineOperations.java
test/jalview/datamodel/AlignmentAnnotationTests.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/ResidueCountTest.java
test/jalview/datamodel/SequenceGroupTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/features/FeatureAttributesTest.java
test/jalview/ext/ensembl/EnsemblCdnaTest.java
test/jalview/ext/ensembl/EnsemblCdsTest.java
test/jalview/ext/ensembl/EnsemblGeneTest.java
test/jalview/ext/ensembl/EnsemblGenomeTest.java
test/jalview/ext/ensembl/EnsemblSeqProxyTest.java
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/jmol/JmolParserTest.java
test/jalview/ext/rbvi/chimera/JalviewChimeraView.java
test/jalview/fts/service/pdb/PDBFTSPanelTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/AlignmentPanelTest.java
test/jalview/gui/AnnotationRowFilterTest.java
test/jalview/gui/CalculationChooserTest.java
test/jalview/gui/FreeUpMemoryTest.java
test/jalview/gui/JvSwingUtilsTest.java
test/jalview/gui/PairwiseAlignmentPanelTest.java
test/jalview/gui/PopupMenuTest.java
test/jalview/gui/SeqCanvasTest.java
test/jalview/gui/SeqPanelTest.java
test/jalview/gui/StructureChooserTest.java
test/jalview/io/AnnotatedPDBFileInputTest.java
test/jalview/io/BackupFilesTest.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/FileFormatsTest.java
test/jalview/io/FileLoaderTest.java
test/jalview/io/FormatAdapterTest.java
test/jalview/io/Jalview2xmlBase.java
test/jalview/io/JalviewExportPropertiesTests.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/StockholmFileTest.java
test/jalview/project/Jalview2xmlTests.java
test/jalview/renderer/OverviewResColourFinderTest.java
test/jalview/renderer/ResidueColourFinderTest.java
test/jalview/renderer/ScaleRendererTest.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java
test/jalview/schemes/ClustalxColourSchemeTest.java
test/jalview/schemes/ColourSchemesTest.java
test/jalview/schemes/PIDColourSchemeTest.java
test/jalview/structure/Mapping.java
test/jalview/structures/models/AAStructureBindingModelTest.java
test/jalview/util/ColorUtilsTest.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java
test/jalview/util/PlatformTest.java
test/jalview/util/StringUtilsTest.java
test/jalview/ws/PDBSequenceFetcherTest.java
test/jalview/ws/dbsources/RemoteFormatTest.java
test/jalview/ws/gui/Jws2ParamView.java
test/jalview/ws/jabaws/DisorderAnnotExportImport.java
test/jalview/ws/jabaws/JalviewJabawsTestUtils.java
test/jalview/ws/jabaws/RNAStructExportImport.java
test/jalview/ws/jws2/ParameterUtilsTest.java
test/jalview/ws/rest/ShmmrRSBSService.java
test/jalview/ws/sifts/SiftsClientTest.java
test/jalview/ws/utils/UrlDownloadClientTest.java
test/mc_view/PDBfileTest.java

diff --cc THIRDPARTYLIBS
@@@ -28,8 -28,8 +28,8 @@@ htsjdk-2.12.0.jar     built from maven mast
  httpclient-4.0.3.jar
  httpcore-4.0.1.jar
  httpmime-4.0.3.jar
 -intervalstore-v1.0.jar
 +intervalstore-v1.1.jar
- jabaws-min-client-2.2.0.jar
+ jabaws-min-client-NO_LOG4J-2.2.1.jar  Apache license - pre-release of JABAWS 2.2.1 client built from https://source.jalview.org/crucible/changelog/jabaws?cs=586260b9f877e0954513fcffb0aa27eaddc5d0ff 
  java-json.jar
  jaxrpc.jar
  jersey-client-1.19.4.jar      CDDL 1.1 + GPL2 w/ CPE - http://glassfish.java.net/public/CDDL+GPL_1_1.html
diff --cc build.gradle
@@@ -383,9 -409,21 +409,20 @@@ ext 
    modules_compileClasspath = fileTree(dir: "${jalviewDir}/${j11modDir}", include: ["*.jar"])
    modules_runtimeClasspath = modules_compileClasspath
    */
-   def details = versionDetails()
-   gitHash = details.gitHash
-   gitBranch = details.branchName
 -
+   gitHash = "SOURCE"
+   gitBranch = "Source"
+   try {
+     apply plugin: "com.palantir.git-version"
+     def details = versionDetails()
+     gitHash = details.gitHash
+     gitBranch = details.branchName
+   } catch(org.gradle.api.internal.plugins.PluginApplicationException e) {
+     println("Not in a git repository. Using git values from RELEASE properties file.")
+     gitHash = releaseProps.getProperty("git.hash")
+     gitBranch = releaseProps.getProperty("git.branch")
+   } catch(java.lang.RuntimeException e1) {
+     throw new GradleException("Error with git-version plugin.  Directory '.git' exists but versionDetails() cannot be found.")
+   }
  
    println("Using a ${CHANNEL} profile.")
  
@@@ -1166,6 -1200,214 +1199,212 @@@ task convertMdFiles 
  }
  
  
+ def hugoTemplateSubstitutions(String input, Map extras=null) {
+   def replacements = [
+     DATE: getDate("yyyy-MM-dd"),
+     CHANNEL: propertiesChannelName,
+     APPLICATION_NAME: applicationName,
+     GIT_HASH: gitHash,
+     GIT_BRANCH: gitBranch,
+     VERSION: JALVIEW_VERSION,
+     JAVA_VERSION: JAVA_VERSION,
+     VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
+     DRAFT: "false",
+     JVL_HEADER: ""
+   ]
+   def output = input
+   if (extras != null) {
+     extras.each{ k, v ->
+       output = output.replaceAll("__${k}__", ((v == null)?"":v))
+     }
+   }
+   replacements.each{ k, v ->
+     output = output.replaceAll("__${k}__", ((v == null)?"":v))
+   }
+   return output
+ }
+ def mdFileComponents(File mdFile, def dateOnly=false) {
+   def map = [:]
+   def content = ""
+   if (mdFile.exists()) {
+     def inFrontMatter = false
+     def firstLine = true
+     mdFile.eachLine { line ->
+       if (line.matches("---")) {
+         def prev = inFrontMatter
+         inFrontMatter = firstLine
+         if (inFrontMatter != prev)
+           return false
+       }
+       if (inFrontMatter) {
+         def m = null
+         if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
+           map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
+         } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
+           map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
+         } else if (m = line =~ /^channel:\s*(\S+)/) {
+           map["channel"] = m[0][1]
+         } else if (m = line =~ /^version:\s*(\S+)/) {
+           map["version"] = m[0][1]
+         } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
+           map[ m[0][1] ] = m[0][2]
+         }
+         if (dateOnly && map["date"] != null) {
+           return false
+         }
+       } else {
+         if (dateOnly)
+           return false
+         content += line+"\n"
+       }
+       firstLine = false
+     }
+   }
+   return dateOnly ? map["date"] : [map, content]
+ }
+ task hugoTemplates {
+   group "website"
+   description "Create partially populated md pages for hugo website build"
+   def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
+   def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
+   def templateFiles = fileTree(dir: hugoTemplatesDir)
+   def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+   def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+   def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
+   def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")
+   doFirst {
+     // specific release template for version archive
+     def changes = ""
+     def whatsnew = null
+     def givenDate = null
+     def givenChannel = null
+     def givenVersion = null
+     if (CHANNEL == "RELEASE") {
+       def (map, content) = mdFileComponents(releaseMdFile)
+       givenDate = map.date
+       givenChannel = map.channel
+       givenVersion = map.version
+       changes = content
+       if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
+         throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
+       }
+       if (whatsnewMdFile.exists())
+         whatsnew = whatsnewMdFile.text
+     }
+     def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
+     def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []
+     def changesHugo = null
+     if (changes != null) {
+       changesHugo = '<div class="release_notes">\n\n'
+       def inSection = false
+       changes.eachLine { line ->
+         def m = null
+         if (m = line =~ /^##([^#].*)$/) {
+           if (inSection) {
+             changesHugo += "</div>\n\n"
+           }
+           def section = m[0][1].trim()
+           section = section.toLowerCase()
+           section = section.replaceAll(/ +/, "_")
+           section = section.replaceAll(/[^a-z0-9_\-]/, "")
+           changesHugo += "<div class=\"${section}\">\n\n"
+           inSection = true
+         } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
+           def comment = m[0][2].trim()
+           if (comment != "") {
+             comment = comment.replaceAll('"', "&quot;")
+             def issuekeys = []
+             comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
+             def newline = m[0][1]
+             if (comment.trim() != "")
+               newline += "{{<comment>}}${comment}{{</comment>}}  "
+             newline += m[0][3].trim()
+             if (issuekeys.size() > 0)
+               newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
+             if (m[0][4] != null)
+               newline += m[0][4]
+             line = newline
+           }
+         }
+         changesHugo += line+"\n"
+       }
+       if (inSection) {
+         changesHugo += "\n</div>\n\n"
+       }
+       changesHugo += '</div>'
+     }
+     templateFiles.each{ templateFile ->
+       def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
+       def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
+       def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )
+       def outPathName = string("${hugoBuildDir}/$newRelPathName")
+       copy {
+         from templateFile
+         rename(templateFile.getName(), newFileName)
+         into outPathName
+       }
+       def newFile = file("${outPathName}/${newFileName}".toString())
+       def content = newFile.text
+       newFile.text = hugoTemplateSubstitutions(content,
+         [
+           WHATSNEW: whatsnew,
+           CHANGES: changesHugo,
+           DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
+           DRAFT: givenDate == null ? "true" : "false",
+           JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
+           JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
+         ]
+       )
+     }
+   }
+   inputs.file(oldJvlFile)
+   inputs.dir(hugoTemplatesDir)
+   inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
+   inputs.property("CHANNEL", { CHANNEL })
+ }
+ def getMdDate(File mdFile) {
+   return mdFileComponents(mdFile, true)
+ }
+ def getMdSections(String content) {
+   def sections = [:]
+   def sectionContent = ""
+   def sectionName = null
+   content.eachLine { line ->
+     def m = null
+     if (m = line =~ /^##([^#].*)$/) {
+       if (sectionName != null) {
+         sections[sectionName] = sectionContent
+         sectionName = null
+         sectionContent = ""
+       }
+       sectionName = m[0][1].trim()
+       sectionName = sectionName.toLowerCase()
+       sectionName = sectionName.replaceAll(/ +/, "_")
+       sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
+     } else if (sectionName != null) {
+       sectionContent += line+"\n"
+     }
+   }
+   if (sectionContent != null) {
+     sections[sectionName] = sectionContent
+   }
+   return sections
+ }
 -
 -
  task copyHelp(type: Copy) {
    def inputDir = helpSourceDir
    def outputDir = "${helpBuildDir}/${help_dir}"
  }
  
  
+ task releasesTemplates {
+   group "help"
+   description "Recreate whatsNew.html and releases.html from markdown files and templates in help"
+   dependsOn copyHelp
+   def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
+   def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
+   def releasesHtmlFile = file("${helpBuildDir}/${help_dir}/${releases_html}")
+   def whatsnewHtmlFile = file("${helpBuildDir}/${help_dir}/${whatsnew_html}")
+   def releasesMdDir = "${jalviewDir}/${releases_dir}"
+   def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"
+   doFirst {
+     def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
+     def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
+     if (CHANNEL == "RELEASE") {
+       if (!releaseMdFile.exists()) {
+         throw new GradleException("File ${releaseMdFile} must be created for RELEASE")
+       }
+       if (!whatsnewMdFile.exists()) {
+         throw new GradleException("File ${whatsnewMdFile} must be created for RELEASE")
+       }
+     }
+     def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
+     def releaseFilesDates = releaseFiles.collectEntries {
+       [(it): getMdDate(it)]
+     }
+     releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }
+     def releasesTemplate = releasesTemplateFile.text
+     def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
+     def versionTemplate = m[0][1]
+     MutableDataSet options = new MutableDataSet()
+     def extensions = new ArrayList<>()
+     options.set(Parser.EXTENSIONS, extensions)
+     options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)
+     Parser parser = Parser.builder(options).build()
+     HtmlRenderer renderer = HtmlRenderer.builder(options).build()
+     def actualVersions = releaseFiles.collect { rf ->
+       def (rfMap, rfContent) = mdFileComponents(rf)
+       return rfMap.version
+     }
+     def versionsHtml = ""
+     def linkedVersions = []
+     releaseFiles.reverse().each { rFile ->
+       def (rMap, rContent) = mdFileComponents(rFile)
+       def versionLink = ""
+       def partialVersion = ""
+       def firstPart = true
+       rMap.version.split("\\.").each { part ->
+         def displayPart = ( firstPart ? "" : "." ) + part
+         partialVersion += displayPart
+         if (
+             linkedVersions.contains(partialVersion)
+             || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
+             ) {
+           versionLink += displayPart
+         } else {
+           versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
+           linkedVersions += partialVersion
+         }
+         firstPart = false
+       }
+       def displayDate = releaseFilesDates[rFile].format("dd/MM/yyyy")
+       def lm = null
+       def rContentProcessed = ""
+       rContent.eachLine { line ->
+         if (lm = line =~ /^(\s*-)(\s*<!--[^>]*?-->)(.*)$/) {
+           line = "${lm[0][1]}${lm[0][3]}${lm[0][2]}"
+       } else if (lm = line =~ /^###([^#]+.*)$/) {
+           line = "_${lm[0][1].trim()}_"
+         }
+         rContentProcessed += line + "\n"
+       }
+       def rContentSections = getMdSections(rContentProcessed)
+       def rVersion = versionTemplate
+       if (rVersion != "") {
+         def rNewFeatures = rContentSections["new_features"]
+         def rIssuesResolved = rContentSections["issues_resolved"]
+         Node newFeaturesNode = parser.parse(rNewFeatures)
+         String newFeaturesHtml = renderer.render(newFeaturesNode)
+         Node issuesResolvedNode = parser.parse(rIssuesResolved)
+         String issuesResolvedHtml = renderer.render(issuesResolvedNode)
+         rVersion = hugoTemplateSubstitutions(rVersion,
+           [
+             VERSION: rMap.version,
+             VERSION_LINK: versionLink,
+             DISPLAY_DATE: displayDate,
+             NEW_FEATURES: newFeaturesHtml,
+             ISSUES_RESOLVED: issuesResolvedHtml
+           ]
+         )
+         versionsHtml += rVersion
+       }
+     }
+     releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
+     releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
+     releasesHtmlFile.text = releasesTemplate
+     if (whatsnewMdFile.exists()) {
+       def wnDisplayDate = releaseFilesDates[releaseMdFile] != null ? releaseFilesDates[releaseMdFile].format("dd MMMM yyyy") : ""
+       def whatsnewMd = hugoTemplateSubstitutions(whatsnewMdFile.text)
+       Node whatsnewNode = parser.parse(whatsnewMd)
+       String whatsnewHtml = renderer.render(whatsnewNode)
+       whatsnewHtml = whatsnewTemplateFile.text.replaceAll("__WHATS_NEW__", whatsnewHtml)
+       whatsnewHtmlFile.text = hugoTemplateSubstitutions(whatsnewHtml,
+         [
+             VERSION: JALVIEW_VERSION,
+           DISPLAY_DATE: wnDisplayDate
+         ]
+       )
+     } else if (gradle.taskGraph.hasTask(":linkCheck")) {
+       whatsnewHtmlFile.text = "Development build " + getDate("yyyy-MM-dd HH:mm:ss")
+     }
+   }
+   inputs.file(releasesTemplateFile)
+   inputs.file(whatsnewTemplateFile)
+   inputs.dir(releasesMdDir)
+   inputs.dir(whatsnewMdDir)
+   outputs.file(releasesHtmlFile)
+   outputs.file(whatsnewHtmlFile)
+ }
 -
 -
  task copyResources(type: Copy) {
    group = "build"
    description = "Copy (and make text substitutions in) the resources dir to the build area"
@@@ -1759,6 -2153,106 +2148,105 @@@ task getdown() 
  }
  
  
+ task getdownArchiveBuild() {
+   group = "distribution"
+   description = "Put files in the archive dir to go on the website"
+   dependsOn getdownWebsite
+   def v = "v${JALVIEW_VERSION_UNDERSCORES}"
+   def vDir = "${getdownArchiveDir}/${v}"
+   getdownFullArchiveDir = "${vDir}/getdown"
+   getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"
+   def vAltDir = "alt_${v}"
+   def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"
+   doFirst {
+     // cleanup old "old" dir
+     delete getdownArchiveDir
+     def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
+     getdownArchiveTxt.getParentFile().mkdirs()
+     def getdownArchiveTextLines = []
+     def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"
+     // the libdir
+     copy {
+       from "${getdownAppBaseDir}/${getdownAppDistDir}"
+       into "${getdownFullArchiveDir}/${vAltDir}"
+     }
+     getdownTextLines.each { line ->
+       line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
+       line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
+       line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
+       line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
+       line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
+       line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
+       // remove the existing resource = resource/ or bin/ lines
+       if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
+         getdownArchiveTextLines += line
+       }
+     }
+     // the resource dir -- add these files as resource lines in getdown.txt
+     copy {
+       from "${archiveImagesDir}"
+       into "${getdownFullArchiveDir}/${getdown_resource_dir}"
+       eachFile { file ->
+         getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
+       }
+     }
+     getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))
+     def vLaunchJvl = file(getdownVersionLaunchJvl)
+     vLaunchJvl.getParentFile().mkdirs()
+     vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
+     def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
+     def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
+     // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
+     //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
+     java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));
+     // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
+     copy {
+       from getdownLauncher
+       from "${getdownAppBaseDir}/${getdownLaunchJvl}"
+       from "${getdownAppBaseDir}/${getdown_launcher_new}"
+       from "${getdownAppBaseDir}/${channel_props}"
+       if (file(getdownLauncher).getName() != getdown_launcher) {
+         rename(file(getdownLauncher).getName(), getdown_launcher)
+       }
+       into getdownFullArchiveDir
+     }
+   }
+ }
+ task getdownArchiveDigest(type: JavaExec) {
+   group = "distribution"
+   description = "Digest the getdown archive folder"
+   dependsOn getdownArchiveBuild
+   doFirst {
+     classpath = files(getdownLauncher)
+     args getdownFullArchiveDir
+   }
+   main = "com.threerings.getdown.tools.Digester"
+   inputs.dir(getdownFullArchiveDir)
+   outputs.file("${getdownFullArchiveDir}/digest2.txt")
+ }
+ task getdownArchive() {
+   group = "distribution"
+   description = "Build the website archive dir with getdown digest"
+   dependsOn getdownArchiveBuild
+   dependsOn getdownArchiveDigest
+ }
 -
  tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
  }
@@@ -1807,13 -2302,15 +2296,14 @@@ task copyInstall4jTemplate 
        }
      }
  
+     // disable install screen for OSX dmg (for 2.11.2.0)
+     install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
+       macosArchive.attributes().remove('executeSetupApp')
+       macosArchive.attributes().remove('setupAppId')
+     }
 -
      // turn off checksum creation for LOCAL channel
      def e = install4jConfigXml.application[0]
-     if (CHANNEL == "LOCAL") {
-       e.'@createChecksums' = "false"
-     } else {
-       e.'@createChecksums' = "true"
-     }
+     e.'@createChecksums' = string(install4jCheckSums)
  
      // put file association actions where placeholder action is
      def install4jFileAssociationsText = install4jFileAssociationsFile.text
@@@ -1980,7 -2486,85 +2479,84 @@@ task installerFiles(type: com.install4j
    outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
  }
  
+ def getDataHash(File myFile) {
+   HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
+   return myFile.exists()
+   ? [
+       "file" : myFile.getName(),
+       "filesize" : myFile.length(),
+       "sha256" : hash.toString()
+     ]
+   : null
+ }
+ def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
+   def hash = [
+     "channel" : getdownChannelName,
+     "date" : getDate("yyyy-MM-dd HH:mm:ss"),
+     "git-commit" : "${gitHash} [${gitBranch}]",
+     "version" : JALVIEW_VERSION
+   ]
+   // install4j installer files
+   if (installersOutputTxt.exists()) {
+     def idHash = [:]
+     installersOutputTxt.readLines().each { def line ->
+       if (line.startsWith("#")) {
+         return;
+       }
+       line.replaceAll("\n","")
+       def vals = line.split("\t")
+       def filename = vals[3]
+       def filesize = file(filename).length()
+       filename = filename.replaceAll(/^.*\//, "")
+       hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
+       idHash."${filename}" = vals[0]
+     }
+     if (install4jCheckSums && installersSha256.exists()) {
+       installersSha256.readLines().each { def line ->
+         if (line.startsWith("#")) {
+           return;
+         }
+         line.replaceAll("\n","")
+         def vals = line.split(/\s+\*?/)
+         def filename = vals[1]
+         def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
+       }
+     }
+   }
+   [
+     "JAR": shadowJar.archiveFile, // executable JAR
+     "JVL": getdownVersionLaunchJvl, // version JVL
+     "SOURCE": sourceDist.archiveFile // source TGZ
+   ].each { key, value ->
+     def file = file(value)
+     if (file.exists()) {
+       def fileHash = getDataHash(file)
+       if (fileHash != null) {
+         hash."${key}" = fileHash;
+       }
+     }
+   }
+   return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
+ }
+ task staticMakeInstallersJsonFile {
+   doFirst {
+     def output = findProperty("i4j_output")
+     def sha256 = findProperty("i4j_sha256")
+     def json = findProperty("i4j_json")
+     if (output == null || sha256 == null || json == null) {
+       throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
+     }
+     writeDataJsonFile(file(output), file(sha256), file(json))
+   }
+ }
+ task installers {
+   dependsOn installerFiles
+ }
  
 -
  spotless {
    java {
      eclipse().configFile(eclipse_codestyle_file)
@@@ -2086,10 -2694,52 +2686,51 @@@ task sourceDist(type: Tar) 
        line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
      })
    }
+   def sourceTarBuildDir = "${buildDir}/sourceTar"
+   from(sourceTarBuildDir) {
+     // this includes the appended RELEASE properties file
+   }
  }
  
+ task dataInstallersJson {
+   group "website"
+   description "Create the installers-VERSION.json data file for installer files created"
+   mustRunAfter installers
+   mustRunAfter shadowJar
+   mustRunAfter sourceDist
+   mustRunAfter getdownArchive
+   def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
+   def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
+   if (installersOutputTxt.exists()) {
+     inputs.file(installersOutputTxt)
+   }
+   if (install4jCheckSums && installersSha256.exists()) {
+     inputs.file(installersSha256)
+   }
+   [
+     shadowJar.archiveFile, // executable JAR
+     getdownVersionLaunchJvl, // version JVL
+     sourceDist.archiveFile // source TGZ
+   ].each { fileName ->
+     if (file(fileName).exists()) {
+       inputs.file(fileName)
+     }
+   }
+   outputs.file(hugoDataJsonFile)
+   doFirst {
+     writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
+   }
+ }
  
  task helppages {
+   group "help"
+   description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."
 -
    dependsOn copyHelp
    dependsOn pubhtmlhelp
    
@@@ -58,10 -58,25 +58,24 @@@ shadow_jar_main_class = jalview.bin.Lau
  
  jalview_name = Jalview
  
+ hugo_old_jvl = utils/hugo/old_jvl.txt
+ hugo_jalviewjs = utils/hugo/jalviewjs.txt
+ hugo_build_dir = build/website/hugo
+ hugo_data_installers_dir = data/installers
+ hugo_version_archive_dir = content/development/archive
+ hugo_templates_dir = utils/hugo/templates
+ releases_template = help/templates/releases.html
+ whatsnew_template = help/templates/whatsNew.html
+ releases_html = html/releases.html
+ whatsnew_html = html/whatsNew.html
+ whatsnew_dir = help/markdown/whatsnew
+ releases_dir = help/markdown/releases
+ # these are going to be used in the future to gather website release files together
+ getdown_website_dir = build/website/docroot/getdown
+ getdown_archive_dir = build/website/docroot/old
+ getdown_files_dir = build/getdown/files
 -
  getdown_local = false
- getdown_website_dir = getdown/website
  getdown_resource_dir = resource
- getdown_files_dir = getdown/files
  getdown_lib_dir = getdown/lib
  getdown_launcher = getdown-launcher.jar
  getdown_launcher_local = getdown-launcher-local.jar
Simple merge
Simple merge
      <li>The <a href="#editing"><strong>&quot;Editing&quot;</strong>
          Preferences</a> tab contains settings affecting behaviour when editing alignments.
      </li>
+     <li>The <a href="#startup"><strong>&quot;Startup&quot;</strong>
+         Preferences</a> tab allows you to adjust how much memory is
+       allocated to Jalview when it is launched.
+     </li>
 +    <li>The <a href="#hmmer"><strong>&quot;HMMER&quot;</strong>
 +        Preferences</a> tab allows you to configure locally installed HMMER tools.
 +    </li>
      <li>The <a href="../webServices/webServicesPrefs.html"><strong>&quot;Web
            Service&quot;</strong> Preferences</a> tab allows you to configure the <a
        href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS</a>
      <em>Sort with New Tree</em> - When selected, any trees calculated or
      loaded onto the alignment will automatically sort the alignment.
    </p>
-     <p>
+   <p>&nbsp;</p>
+   <p>
+     <a name="startup"><strong>Startup</strong></a>
+   </p>
+   <p>
+     When Jalview is launched it by default examines the available memory
+     and requests up to 90% to be allocated to the application, or 32G,
+     which ever is smaller. The <em>Startup</em> tab allows you to adjust
+     the maximum percentage and hard limits for Jalview memory allocation
+     stored in your .jalview_properties file.
+   </p>
+   <p>&nbsp;</p>
++  <p>
 +    <a name="hmmer"><strong>&quot;HMMER&quot; Preferences tab</strong></a>
 +  </p>
 +  <p>If you have installed HMMER tools (available from <a href="http://hmmerorg">hmmer.org</a>),
 +  then you should specify on this screen the location of the installation (the path to the folder 
 +  containing binary executable programs). Double-click in the input field to open a file browser.</p>
 +  <p>When this path is configured, the <a href="../menus/alwhmmer.html">HMMER menu</a> will be
 +  enabled in the Alignment window.</p>
 +  <p>&nbsp;</p>
    <em>Web Services Preferences</em> - documentation for this tab is
    given in the
    <a href="../webServices/webServicesPrefs.html">Web Services
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
   */
  package jalview.analysis;
  
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.HiddenMarkovModel;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.Sequence;
  import jalview.datamodel.SequenceFeature;
@@@ -105,38 -89,53 +106,38 @@@ public class SeqsetUtil
      {
        return false;
      }
 -    String oldname = (String) sqinfo.get("Name");
 -    Integer start = (Integer) sqinfo.get("Start");
 -    Integer end = (Integer) sqinfo.get("End");
 -    Vector<SequenceFeature> sfeatures = (Vector<SequenceFeature>) sqinfo
 -            .get("SeqFeatures");
 -    Vector<PDBEntry> pdbid = (Vector<PDBEntry>) sqinfo.get("PdbId");
 -    String description = (String) sqinfo.get("Description");
 -    Sequence seqds = (Sequence) sqinfo.get("datasetSequence");
 -    if (oldname == null)
 -    {
 -      namePresent = false;
 -    }
 -    else
 +    if (sqinfo.name != null)
      {
 -      sq.setName(oldname);
 +      sq.setName(sqinfo.name);
      }
 -    if (pdbid != null && pdbid.size() > 0)
 -    {
 -      sq.setPDBId(pdbid);
 -    }
 -
 -    if ((start != null) && (end != null))
 +    sq.setStart(sqinfo.start);
 +    sq.setEnd(sqinfo.end);
 +    if (sqinfo.pdbId.isPresent() && !sqinfo.pdbId.get().isEmpty())
 +      sq.setPDBId(new Vector<>(sqinfo.pdbId.get()));
 +    if (sqinfo.features.isPresent() && !sqinfo.features.get().isEmpty())
 +      sq.setSequenceFeatures(sqinfo.features.get());
 +    if (sqinfo.description.isPresent())
 +      sq.setDescription(sqinfo.description.get());
 +    if (sqinfo.dataset.isPresent())
      {
 -      sq.setStart(start.intValue());
 -      sq.setEnd(end.intValue());
 -    }
 -
 -    if (sfeatures != null && !sfeatures.isEmpty())
 -    {
 -      sq.setSequenceFeatures(sfeatures);
 -    }
 -    if (description != null)
 -    {
 -      sq.setDescription(description);
 +      if (sqinfo.features.isPresent())
 +      {
-         Cache.log.warn("Setting dataset sequence for a sequence which has " +
++        Console.warn("Setting dataset sequence for a sequence which has " +
 +            "sequence features. Dataset sequence features will not be visible.");
 +        assert false;
 +      }
 +      sq.setDatasetSequence(sqinfo.dataset.get());
      }
 -    if ((seqds != null) && !(seqds.getName().equals("THISISAPLACEHOLDER")
 -            && seqds.getLength() == 0))
 +    if (sqinfo.hmm.isPresent())
 +      sq.setHMM(new HiddenMarkovModel(sqinfo.hmm.get(), sq));
 +    if (sqinfo.searchScores.isPresent())
      {
 -      if (sfeatures != null)
 +      for (AlignmentAnnotation score : sqinfo.searchScores.get())
        {
 -        System.err.println(
 -                "Implementation error: setting dataset sequence for a sequence which has sequence features.\n\tDataset sequence features will not be visible.");
 +        sq.addAlignmentAnnotation(score);
        }
 -      sq.setDatasetSequence(seqds);
      }
 -
 -    return namePresent;
 +    return sqinfo.name != null;
    }
  
    /**
          {
            if (!quiet)
            {
-             Cache.log.warn(format("Can't find '%s' in uniquified alignment",
 -            System.err.println("Can't find '" + ((String) key)
 -                    + "' in uniquified alignment");
++            Console.warn(format("Can't find '%s' in uniquified alignment",
 +                key));
            }
          }
 +      } catch (ClassCastException ccastex) {
 +        if (!quiet)
 +        {
-           Cache.log.error("Unexpected object in SeqSet map : "+ key.getClass());
++          Console.error("Unexpected object in SeqSet map : "+ key.getClass());
 +        }
        }
      }
      if (unmatched.size() > 0 && !quiet)
      {
 -      System.err.println("Did not find matches for :");
 -      for (Enumeration i = unmatched.elements(); i
 -              .hasMoreElements(); System.out
 -                      .println(((SequenceI) i.nextElement()).getName()))
 +      StringBuilder sb = new StringBuilder("Did not find match for sequences: ");
 +      Enumeration<SequenceI> i = unmatched.elements();
 +      sb.append(i.nextElement().getName());
 +      for (; i.hasMoreElements();)
        {
 -        ;
 +        sb.append(", " + i.nextElement().getName());
        }
-       Cache.log.warn(sb.toString());
++      Console.warn(sb.toString());
        return false;
      }
  
Simple merge
Simple merge
Simple merge
@@@ -198,12 -192,11 +198,11 @@@ public class AlignViewport extends Alig
  
        if (colour != null)
        {
-         residueShading = new ResidueShader(
-                 ColourSchemeProperty.getColourScheme(this, alignment,
-                         colour));
+         residueShading = new ResidueShader(ColourSchemeProperty
+                 .getColourScheme(this, alignment, colour));
          if (residueShading != null)
          {
 -          residueShading.setConsensus(hconsensus);
 +          residueShading.setConsensus(consensusProfiles);
          }
        }
  
Simple merge
Simple merge
index b64f40c,0000000..450809a
mode 100644,000000..100644
--- /dev/null
@@@ -1,163 -1,0 +1,163 @@@
 +/*
 + * 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 jalview.util.Platform;
 +
 +import java.lang.reflect.Constructor;
 +import java.lang.reflect.InvocationTargetException;
 +import java.util.HashMap;
 +import java.util.Map;
 +
 +/**
 + * A class to hold singleton objects, whose scope (context) is
 + * <ul>
 + * <li>the Java runtime (JVM) when running as Java</li>
 + * <li>one 'applet', when running as JalviewJS</li>
 + * </ul>
 + * This allows separation of multiple JS applets running on the same browser
 + * page, each with their own 'singleton' instances.
 + * <p>
 + * Instance objects are held in a separate Map (keyed by Class) for each
 + * context. For Java, this is just a single static Map. For SwingJS, the map is
 + * stored as a field {@code _swingjsSingletons} of
 + * {@code Thread.currentThread.getThreadGroup()}, as a proxy for the applet.
 + * <p>
 + * Note that when an applet is stopped, its ThreadGroup is removed, allowing any
 + * singleton references to be garbage collected.
 + * 
 + * @author hansonr
 + */
 +public class ApplicationSingletonProvider
 +{
 +  /**
 +   * A tagging interface to mark classes whose singleton instances may be served
 +   * by {@code ApplicationSingletonProvider}, giving a distinct instance per JS
 +   * 'applet'.
 +   * <p>
 +   * A class whose singleton should have global scope (be shared across all
 +   * applets on a page) should <em>not</em> use this mechanism, but just provide
 +   * a single instance (class static member) in the normal way.
 +   */
 +  public interface ApplicationSingletonI
 +  {
 +  }
 +  
 +  /*
 +   * Map used to hold singletons in JVM context
 +   */
 +  private static Map<Class<? extends ApplicationSingletonI>, ApplicationSingletonI> singletons = new HashMap<>();
 +
 +  /**
 +   * private constructor for non-instantiable class
 +   */
 +  private ApplicationSingletonProvider()
 +  {
 +  }
 +
 +  /**
 +   * Returns the singletons map for the current context (JVM for Java,
 +   * ThreadGroup for JS), creating the map on the first request for each JS
 +   * ThreadGroup
 +   * 
 +   * @return
 +   */
 +  private static Map<Class<? extends ApplicationSingletonI>, ApplicationSingletonI> getContextMap()
 +  {
 +    @SuppressWarnings("unused")
 +    ThreadGroup g = (Platform.isJS()
 +            ? Thread.currentThread().getThreadGroup()
 +            : null);
 +    Map<Class<? extends ApplicationSingletonI>, ApplicationSingletonI> map = singletons;
 +    /** @j2sNative map = g._swingjsSingletons; */
 +    if (map == null)
 +    {
 +      map = new HashMap<>();
 +      /** @j2sNative g._swingjsSingletons = map; */
 +    }
 +
 +    return map;
 +  }
 +
 +  /**
 +   * Answers the singleton instance of the given class for the current context
 +   * (JVM or SwingJS 'applet'). If no instance yet exists, one is created, by
 +   * calling the class's no-argument constructor. Answers null if any error
 +   * occurs (or occurred previously for the same class).
 +   * 
 +   * @param c
 +   * @return
 +   */
 +  public static ApplicationSingletonI getInstance(Class<? extends ApplicationSingletonI> c)
 +  {
 +    Map<Class<? extends ApplicationSingletonI>, ApplicationSingletonI> map = getContextMap();
 +    if (map.containsKey(c))
 +    {
 +      /*
 +       * singleton already created _or_ creation failed (null value stored)
 +       */
 +      return map.get(c);
 +    }
 +
 +    /*
 +     * create and save the singleton
 +     */
 +    ApplicationSingletonI o = map.get(c);
 +    try
 +    {
 +      Constructor<? extends ApplicationSingletonI> con = c
 +              .getDeclaredConstructor();
 +      con.setAccessible(true);
 +      o = con.newInstance();
 +    } catch (IllegalAccessException | InstantiationException
 +            | IllegalArgumentException | InvocationTargetException
 +            | NoSuchMethodException | SecurityException e)
 +    {
-       Cache.log.error("Failed to create singleton for " + c.toString()
++      Console.error("Failed to create singleton for " + c.toString()
 +              + ", error was: " + e.toString());
 +      e.printStackTrace();
 +    }
 +
 +    /*
 +     * store the new singleton; note that a
 +     * null value is saved if construction failed
 +     */
 +    getContextMap().put(c, o);
 +    return o;
 +  }
 +
 +  /**
 +   * Removes the current singleton instance of the given class from the current
 +   * application context. This has the effect of ensuring that a new instance is
 +   * created the next time one is requested.
 +   * 
 +   * @param c
 +   */
 +  public static void removeInstance(
 +          Class<? extends ApplicationSingletonI> c)
 +  {
 +    Map<Class<? extends ApplicationSingletonI>, ApplicationSingletonI> map = getContextMap();
 +    if (map != null)
 +    {
 +      map.remove(c);
 +    }
 +  }
 +}
@@@ -47,12 -46,6 +47,7 @@@ import java.util.regex.Pattern
  import javax.swing.LookAndFeel;
  import javax.swing.UIManager;
  
- import org.apache.log4j.ConsoleAppender;
- import org.apache.log4j.Level;
- import org.apache.log4j.Logger;
- import org.apache.log4j.SimpleLayout;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.datamodel.PDBEntry;
  import jalview.gui.Preferences;
  import jalview.gui.UserDefinedColours;
@@@ -221,26 -223,8 +225,24 @@@ import jalview.ws.sifts.SiftsSettings
   * @author $author$
   * @version $Revision$
   */
 -public class Cache
 +public class Cache implements ApplicationSingletonI
  {
 +  private Cache()
 +  {
 +    // private singleton
 +  }
 +
 +  /**
 +   * In Java, this will be a static field instance, which will be
 +   * application-specific; in JavaScript it will be an applet-specific instance
 +   * tied to the applet's ThreadGroup.
 +   * 
 +   * @return
 +   */
 +  public static Cache getInstance()
 +  {
 +    return (Cache) ApplicationSingletonProvider.getInstance(Cache.class);
 +  }
    /**
     * property giving log4j level for CASTOR loggers
     */
    /**
     * Sifts settings
     */
 -  public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = System
 -          .getProperty("user.home") + File.separatorChar
 -          + ".sifts_downloads" + File.separatorChar;
 +  public static final String DEFAULT_SIFTS_DOWNLOAD_DIR = Platform.getUserPath(".sifts_downloads/");
 +  
    private final static String DEFAULT_CACHE_THRESHOLD_IN_DAYS = "2";
  
    private final static String DEFAULT_FAIL_SAFE_PID_THRESHOLD = "30";
    /**
     * Initialises the Jalview Application Log
     */
-   public static Logger log;
 -
+   public final static String JALVIEW_LOGGER_NAME = "JalviewLogger";
  
    // save the proxy properties set at startup
    public final static String[] startupProxyProperties = {
  
    // in-memory only storage of proxy password, safer to use char array
    public static char[] proxyAuthPassword = null;
    /** Jalview Properties */
 -  public static Properties applicationProperties = new Properties()
 +  private Properties applicationProperties = new Properties()
    {
      // override results in properties output in alphabetical order
      @Override
  
    private final static String JS_PROPERTY_PREFIX = "jalview_";
  
-   public static void initLogger()
-   {
-     if (log != null)
-     {
-       return;
-     }
-     try
-     {
-       // TODO: redirect stdout and stderr here in order to grab the output of
-       // the log
-       ConsoleAppender ap = new ConsoleAppender(new SimpleLayout(),
-               "System.err");
-       ap.setName("JalviewLogger");
-       org.apache.log4j.Logger.getRootLogger().addAppender(ap); // catch all for
-       // log output
-       Logger laxis = Logger.getLogger("org.apache.axis");
-       Logger lcastor = Logger.getLogger("org.exolab.castor");
-       jalview.bin.Cache.log = Logger.getLogger("jalview.bin.Jalview");
-       laxis.setLevel(Level.toLevel(
-               Cache.getDefault("logs.Axis.Level", Level.INFO.toString())));
-       lcastor.setLevel(Level.toLevel(Cache.getDefault("logs.Castor.Level",
-               Level.INFO.toString())));
-       lcastor = Logger.getLogger("org.exolab.castor.xml");
-       lcastor.setLevel(Level.toLevel(Cache.getDefault("logs.Castor.Level",
-               Level.INFO.toString())));
-       // 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);
-       // lcastor.addAppender(ap);
-       // jalview.bin.Cache.log.addAppender(ap);
-       // Tell the user that debug is enabled
-       jalview.bin.Cache.log.debug(ChannelProperties.getProperty("app_name")
-               + " Debugging Output Follows.");
-     } catch (Exception ex)
-     {
-       System.err.println("Problems initializing the log4j system\n");
-       ex.printStackTrace(System.err);
-     }
-   }
 +
    /**
 -   * Loads properties from the given properties file. Any existing properties
 -   * are first cleared.
 +   * Loads properties from the given properties file. Any existing properties are
 +   * first cleared.
     */
    public static void loadProperties(String propsFile)
    {
 +    getInstance().loadPropertiesImpl(propsFile);
 +
 +  }
 +
 +  private void loadPropertiesImpl(String propsFile)
 +  {
      propertiesFile = propsFile;
      String releasePropertiesFile = null;
      boolean defaultProperties = false;
    {
      // consider returning more human friendly info
      // eg 'built from Source' or update channel
-     return jalview.bin.Cache.getDefault("INSTALLATION", "unknown");
 -    return Cache.getDefault("INSTALLATION", "unknown");
++        return Cache.getDefault("INSTALLATION", "unknown");
 +  }
 +
 +  /**
 +   * 
 +   * For AppletParams and Preferences ok_actionPerformed and
 +   * startupFileTextfield_mouseClicked
 +   * 
 +   * Sets a property value for the running application, without saving it to the
 +   * properties file
 +   * 
 +   * @param key
 +   * @param obj
 +   */
 +  public static void setPropertyNoSave(String key, String obj)
 +  {
 +    getInstance().setPropertyImpl(key, obj, false);
 +  }
 +
 +  /**
 +   * Sets a property value, and optionally also saves the current properties to
 +   * file
 +   * 
 +   * @param key
 +   * @param obj
 +   * @param andSave
 +   * @return
 +   */
 +  private Object setPropertyImpl(
 +          String key, String obj, boolean andSave)
 +  {
 +    Object oldValue = null;
 +    try
 +    {
 +      oldValue = applicationProperties.setProperty(key, obj);
 +      if (andSave && !propsAreReadOnly && propertiesFile != null)
 +      {
 +        FileOutputStream out = new FileOutputStream(propertiesFile);
 +        applicationProperties.store(out, "---JalviewX Properties File---");
 +        out.close();
 +      }
 +    } catch (Exception ex)
 +    {
 +      System.out.println(
 +              "Error setting property: " + key + " " + obj + "\n" + ex);
 +    }
 +    return oldValue;
    }
  
    public static String getStackTraceString(Throwable t)
   */
  package jalview.bin;
  
++import java.util.Locale;
++
 +import java.awt.GraphicsEnvironment;
+ import java.awt.Color;
 +
  import java.io.BufferedReader;
  import java.io.File;
  import java.io.FileOutputStream;
@@@ -49,17 -50,16 +54,20 @@@ import javax.swing.SwingUtilities
  import javax.swing.UIManager;
  import javax.swing.UIManager.LookAndFeelInfo;
  
+ import com.formdev.flatlaf.FlatLightLaf;
+ import com.formdev.flatlaf.util.SystemInfo;
  import com.threerings.getdown.util.LaunchUtil;
  
+ //import edu.stanford.ejalbert.launching.IBrowserLaunching;
  import groovy.lang.Binding;
  import groovy.util.GroovyScriptEngine;
 +import jalview.api.AlignCalcWorkerI;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.ext.so.SequenceOntology;
  import jalview.gui.AlignFrame;
 +import jalview.gui.AlignViewport;
  import jalview.gui.Desktop;
 +import jalview.gui.Preferences;
  import jalview.gui.PromptUserConfig;
  import jalview.io.AppletFormatAdapter;
  import jalview.io.BioJsHTMLOutput;
@@@ -96,71 -97,29 +105,80 @@@ import jalview.ws.jws2.Jws2Discoverer
   * @author $author$
   * @version $Revision$
   */
 -public class Jalview
 +public class Jalview implements ApplicationSingletonI
  {
 -  static
 +  // for testing those nasty messages you cannot ever find.
 +  // static
 +  // {
 +  // System.setOut(new PrintStream(new ByteArrayOutputStream())
 +  // {
 +  // @Override
 +  // public void println(Object o)
 +  // {
 +  // if (o != null)
 +  // {
 +  // System.err.println(o);
 +  // }
 +  // }
 +  //
 +  // });
 +  // }
 +  public static Jalview getInstance()
 +  {
 +    return (Jalview) ApplicationSingletonProvider
 +            .getInstance(Jalview.class);
 +  }
 +
 +  private Jalview()
    {
      Platform.getURLCommandArguments();
+     Platform.addJ2SDirectDatabaseCall("https://www.jalview.org");
+     Platform.addJ2SDirectDatabaseCall("http://www.jalview.org");
+     Platform.addJ2SDirectDatabaseCall("http://www.compbio.dundee.ac.uk");
+     Platform.addJ2SDirectDatabaseCall("https://www.compbio.dundee.ac.uk");
    }
  
 -  /*
 -   * singleton instance of this class
 -   */
 -  private static Jalview instance;
 +
++  private boolean headless;
    private Desktop desktop;
  
 -  public static AlignFrame currentAlignFrame;
 +  public AlignFrame currentAlignFrame;
 +
 +  public String appletResourcePath;
 +
 +  public String j2sAppletID;
 +
 +  private boolean noCalculation, noMenuBar, noStatus;
 +
 +  private boolean noAnnotation;
 +
 +  public boolean getStartCalculations()
 +  {
 +    return !noCalculation;
 +  }
 +
 +  public boolean getAllowMenuBar()
 +  {
 +    return !noMenuBar;
 +  }
 +
 +  public boolean getShowStatus()
 +  {
 +    return !noStatus;
 +  }
 +
 +  public boolean getShowAnnotation()
 +  {
 +    return !noAnnotation;
 +  }
  
    static
    {
--    if (!Platform.isJS())
++    if (Platform.isJS())
++    {
++       Platform.getURLCommandArguments();
++    } else
      /**
       * Java only
       * 
      }
  
      // report Jalview version
 -    Cache.loadBuildProperties(true);
 +    Cache.getInstance().loadBuildProperties(true);
  
      ArgsParser aparser = new ArgsParser(args);
--    boolean headless = false;
++    headless = false;
  
      String usrPropsFile = aparser.getValue("props");
      Cache.loadProperties(usrPropsFile); // must do this before
        }
        // anything else!
  
+       // allow https handshakes to download intermediate certs if necessary
+       System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
 -
 -      final String jabawsUrl = aparser.getValue("jabaws");
 -      if (jabawsUrl != null)
 +      final String jabawsUrl = aparser.getValue(ArgsParser.JABAWS);
 +      allowServices = !("none".equals(jabawsUrl));
 +      if (allowServices && jabawsUrl != null)
        {
          try
          {
                    "Invalid jabaws parameter: " + jabawsUrl + " ignored");
          }
        }
      }
 -    String defs = aparser.getValue("setprop");
 +    String defs = aparser.getValue(ArgsParser.SETPROP);
      while (defs != null)
      {
        int p = defs.indexOf('=');
      }
      System.setProperty("http.agent",
              "Jalview Desktop/" + Cache.getDefault("VERSION", "Unknown"));
 -
      try
      {
-       Cache.initLogger();
+       Console.initLogger();
      } catch (NoClassDefFoundError error)
      {
        error.printStackTrace();
       * configure 'full' SO model if preferences say to, else use the default (full SO)
       * - as JS currently doesn't have OBO parsing, it must use 'Lite' version
       */
 -    boolean soDefault = !Platform.isJS();
 +    boolean soDefault = !isJS;
      if (Cache.getDefault("USE_FULL_SO", soDefault))
      {
--      SequenceOntologyFactory.setInstance(new SequenceOntology());
++      SequenceOntologyFactory.setSequenceOntology(new SequenceOntology());
      }
  
      if (!headless)
         * @j2sIgnore
         */
        {
 -
+         /**
+          * Check to see that the JVM version being run is suitable for the Java
+          * version this Jalview was compiled for. Popup a warning if not.
+          */
+         if (!LaunchUtils.checkJavaVersion())
+         {
+           Console.warn("The Java version being used (Java "
+                   + LaunchUtils.getJavaVersion()
+                   + ") may lead to problems. This installation of Jalview should be used with Java "
+                   + LaunchUtils.getJavaCompileVersion() + ".");
+           if (!LaunchUtils
+                   .getBooleanUserPreference("IGNORE_JVM_WARNING_POPUP"))
+           {
+             Object[] options = {
+                 MessageManager.getString("label.continue") };
+             JOptionPane.showOptionDialog(null,
+                     MessageManager.formatMessage(
+                             "warning.wrong_jvm_version_message",
+                             LaunchUtils.getJavaVersion(),
+                             LaunchUtils.getJavaCompileVersion()),
+                     MessageManager
+                             .getString("warning.wrong_jvm_version_title"),
+                     JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
+                     null, options, options[0]);
+           }
+         }
 -
 -        if (!aparser.contains("nowebservicediscovery"))
 -        {
 -          desktop.startServiceDiscovery();
 -        }
          if (!aparser.contains("nousagestats"))
          {
            startUsageStats(desktop);
            desktop.checkForNews();
          }
  
-         BioJsHTMLOutput.updateBioJS();
+         if (!aparser.contains("nohtmltemplates")
+                 || Cache.getProperty("NOHTMLTEMPLATES") == null)
+         {
+           BioJsHTMLOutput.updateBioJS();
+         }
        }
      }
+     // Check if JVM and compile version might cause problems and log if it
+     // might.
+     if (headless && !Platform.isJS() && !LaunchUtils.checkJavaVersion())
+     {
+       Console.warn("The Java version being used (Java "
+               + LaunchUtils.getJavaVersion()
+               + ") may lead to problems. This installation of Jalview should be used with Java "
+               + LaunchUtils.getJavaCompileVersion() + ".");
+     }
 +    parseArguments(aparser, true);
 +  }
  
 -    // Move any new getdown-launcher-new.jar into place over old
 -    // getdown-launcher.jar
 -    String appdirString = System.getProperty("getdownappdir");
 -    if (appdirString != null && appdirString.length() > 0)
 +  /**
 +   * Parse all command-line String[] arguments as well as all JavaScript-derived
 +   * parameters from Info.
 +   * 
 +   * We allow for this method to be run from JavaScript. Basically allowing
 +   * simple scripting.
 +   * 
 +   * @param aparser
 +   * @param isStartup
 +   */
 +  public void parseArguments(ArgsParser aparser, boolean isStartup)
 +  {
 +
 +    String groovyscript = null; // script to execute after all loading is
 +    boolean isJS = Platform.isJS();
 +    if (!isJS)
 +    /** @j2sIgnore */
      {
 -      final File appdir = new File(appdirString);
 -      new Thread()
 +      // Move any new getdown-launcher-new.jar into place over old
 +      // getdown-launcher.jar
 +      String appdirString = System.getProperty("getdownappdir");
 +      if (appdirString != null && appdirString.length() > 0)
        {
 -        @Override
 -        public void run()
 +        final File appdir = new File(appdirString);
 +        new Thread()
          {
 -          LaunchUtil.upgradeGetdown(
 -                  new File(appdir, "getdown-launcher-old.jar"),
 -                  new File(appdir, "getdown-launcher.jar"),
 -                  new File(appdir, "getdown-launcher-new.jar"));
 -        }
 -      }.start();
 -    }
 +          @Override
 +          public void run()
 +          {
 +            LaunchUtil.upgradeGetdown(
 +                    new File(appdir, "getdown-launcher-old.jar"),
 +                    new File(appdir, "getdown-launcher.jar"),
 +                    new File(appdir, "getdown-launcher-new.jar"));
 +          }
 +        }.start();
 +      }
  
 -    String file = null, data = null;
 -    FileFormatI format = null;
 -    DataSourceType protocol = null;
 -    FileLoader fileLoader = new FileLoader(!headless);
 +      // completed one way or another
 +      // extract groovy argument and execute if necessary
 +      groovyscript = aparser.getValue("groovy", true);
 +    }
  
 -    String groovyscript = null; // script to execute after all loading is
 -    // completed one way or another
 -    // extract groovy argument and execute if necessary
 -    groovyscript = aparser.getValue("groovy", true);
 -    file = aparser.getValue("open", true);
 +    String file = aparser.getValue("open", true);
  
 -    if (file == null && desktop == null)
 +    if (!isJS && file == null && desktop == null)
      {
        System.out.println("No files to open!");
        System.exit(1);
      }
 +    setDisplayParameters(aparser);
 +    
 +    // time to open a file.
      long progress = -1;
 +    DataSourceType protocol = null;
 +    FileLoader fileLoader = new FileLoader(!headless);
 +    FileFormatI format = null;
      // Finally, deal with the remaining input data.
 -    if (file != null)
 +    AlignFrame af = null;
 +
 +    JalviewJSApp jsApp = (isJS ? new JalviewJSApp(this, aparser) : null);
 +
 +    if (file == null)
 +    {
 +      if (isJS)
 +      {
 +        // JalviewJS allows sequence1 sequence2 ....
 +        
 +      }
 +      else if (!headless && Cache.getDefault("SHOW_STARTUP_FILE", true))
-       /**
-        * Java only
-        * 
-        * @j2sIgnore
-        */
++    /**
++     * Java only
++     * 
++     * @j2sIgnore
++     */
++    {
++      file = Cache.getDefault("STARTUP_FILE",
++              Cache.getDefault("www.jalview.org", "https://www.jalview.org")
++                      + "/examples/exampleFile_2_7.jvp");
++      if (file.equals("http://www.jalview.org/examples/exampleFile_2_3.jar")
++              || file.equals(
++                      "http://www.jalview.org/examples/exampleFile_2_7.jar"))
 +      {
++        file.replace("http:", "https:");
++        // hardwire upgrade of the startup file
++        file.replace("_2_3", "_2_7");
++        file.replace("2_7.jar", "2_7.jvp");
++        // and remove the stale setting
++        Cache.removeProperty("STARTUP_FILE");
++      }
 +
-         // We'll only open the default file if the desktop is visible.
-         // And the user
-         // ////////////////////
-         file = Cache.getDefault("STARTUP_FILE",
-                 Cache.getDefault("www.jalview.org",
-                         "http://www.jalview.org")
-                         + "/examples/exampleFile_2_7.jar");
-         if (file.equals(
-                 "http://www.jalview.org/examples/exampleFile_2_3.jar"))
-         {
-           // hardwire upgrade of the startup file
-           file.replace("_2_3.jar", "_2_7.jar");
-           // and remove the stale setting
-           Cache.removeProperty("STARTUP_FILE");
-         }
-         protocol = DataSourceType.FILE;
-         if (file.indexOf("http:") > -1)
-         {
-           protocol = DataSourceType.URL;
-         }
++      protocol = AppletFormatAdapter.checkProtocol(file);
 +
-         if (file.endsWith(".jar"))
++      if (file.endsWith(".jar"))
++      {
++        format = FileFormat.Jalview;
++      }
++      else
++      {
++        try
 +        {
-           format = FileFormat.Jalview;
-         }
-         else
++          format = new IdentifyFile().identify(file, protocol);
++        } catch (FileFormatException e)
 +        {
-           try
-           {
-             format = new IdentifyFile().identify(file, protocol);
-           } catch (FileFormatException e)
-           {
-             // TODO what?
-           }
++          // TODO what?
 +        }
-         af = fileLoader.LoadFileWaitTillLoaded(file, protocol, format);
 +      }
++
++      af = fileLoader.LoadFileWaitTillLoaded(file, protocol, format);
++       }
 +    }
 +    else
      {
        if (!headless)
        {
        }
        else
        {
 -        setCurrentAlignFrame(af);
 -        data = aparser.getValue("colour", true);
 -        if (data != null)
 +        
 +        // JalviewLite interface for JavaScript allows second file open
 +        String file2 = aparser.getValue(ArgsParser.OPEN2, true);
 +        if (file2 != null)
          {
 -          data.replaceAll("%20", " ");
 -
 -          ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
 -                  af.getViewport(), af.getViewport().getAlignment(), data);
 -
 -          if (cs != null)
 +          protocol = AppletFormatAdapter.checkProtocol(file2);
 +          try
            {
 -            System.out.println(
 -                    "CMD [-color " + data + "] executed successfully!");
 +            format = new IdentifyFile().identify(file2, protocol);
 +          } catch (FileFormatException e1)
 +          {
 +            // TODO ?
            }
 -          af.changeColour(cs);
 -        }
 -
 -        // Must maintain ability to use the groups flag
 -        data = aparser.getValue("groups", true);
 -        if (data != null)
 -        {
 -          af.parseFeaturesFile(data,
 -                  AppletFormatAdapter.checkProtocol(data));
 -          // System.out.println("Added " + data);
 -          System.out.println(
 -                  "CMD groups[-" + data + "]  executed successfully!");
 -        }
 -        data = aparser.getValue("features", true);
 -        if (data != null)
 -        {
 -          af.parseFeaturesFile(data,
 -                  AppletFormatAdapter.checkProtocol(data));
 -          // System.out.println("Added " + data);
 -          System.out.println(
 -                  "CMD [-features " + data + "]  executed successfully!");
 -        }
 -
 -        data = aparser.getValue("annotations", true);
 -        if (data != null)
 -        {
 -          af.loadJalviewDataFile(data, null, null, null);
 -          // System.out.println("Added " + data);
 -          System.out.println(
 -                  "CMD [-annotations " + data + "] executed successfully!");
 -        }
 -        // set or clear the sortbytree flag.
 -        if (aparser.contains("sortbytree"))
 -        {
 -          af.getViewport().setSortByTree(true);
 -          if (af.getViewport().getSortByTree())
 +          AlignFrame af2 = new FileLoader(!headless)
 +                  .LoadFileWaitTillLoaded(file2, protocol, format);
 +          if (af2 == null)
            {
 -            System.out.println("CMD [-sortbytree] executed successfully!");
 +            System.out.println("error");
            }
 -        }
 -        if (aparser.contains("no-annotation"))
 -        {
 -          af.getViewport().setShowAnnotation(false);
 -          if (!af.getViewport().isShowAnnotation())
 +          else
            {
 -            System.out.println("CMD no-annotation executed successfully!");
 +            AlignViewport.openLinkedAlignmentAs(af,
 +                    af.getViewport().getAlignment(),
 +                    af2.getViewport().getAlignment(), "",
 +                    AlignViewport.SPLIT_FRAME);
 +            System.out.println(
 +                    "CMD [-open2 " + file2 + "] executed successfully!");
            }
          }
 -        if (aparser.contains("nosortbytree"))
 +        // af is loaded - so set it as current frame
 +        setCurrentAlignFrame(af);
 +
 +        setFrameDependentProperties(aparser, af);
 +        
 +        if (isJS)
          {
 -          af.getViewport().setSortByTree(false);
 -          if (!af.getViewport().getSortByTree())
 -          {
 -            System.out
 -                    .println("CMD [-nosortbytree] executed successfully!");
 -          }
 +          jsApp.initFromParams(af);
          }
 -        data = aparser.getValue("tree", true);
 -        if (data != null)
 +        else
 +        /**
 +         * Java only
 +         * 
 +         * @j2sIgnore
 +         */
          {
 -          try
 +          if (groovyscript != null)
            {
 -            System.out.println(
 -                    "CMD [-tree " + data + "] executed successfully!");
 -            NewickFile nf = new NewickFile(data,
 -                    AppletFormatAdapter.checkProtocol(data));
 -            af.getViewport()
 -                    .setCurrentTree(af.showNewickTree(nf, data).getTree());
 -          } catch (IOException ex)
 -          {
 -            System.err.println("Couldn't add tree " + data);
 -            ex.printStackTrace(System.err);
 +            // Execute the groovy script after we've done all the rendering
 +            // stuff
 +            // and before any images or figures are generated.
 +            System.out.println("Executing script " + groovyscript);
 +            executeGroovyScript(groovyscript, af);
 +            System.out.println("CMD groovy[" + groovyscript
 +                    + "] executed successfully!");
 +            groovyscript = null;
            }
          }
 -        // TODO - load PDB structure(s) to alignment JAL-629
 -        // (associate with identical sequence in alignment, or a specified
 -        // sequence)
 -        if (groovyscript != null)
 -        {
 -          // Execute the groovy script after we've done all the rendering stuff
 -          // and before any images or figures are generated.
 -          System.out.println("Executing script " + groovyscript);
 -          executeGroovyScript(groovyscript, af);
 -          System.out.println("CMD groovy[" + groovyscript
 -                  + "] executed successfully!");
 -          groovyscript = null;
 +        if (!isJS || !isStartup) {
 +          createOutputFiles(aparser, format);
          }
 -        String imageName = "unnamed.png";
 -        while (aparser.getSize() > 1)
 -        {
 -          String outputFormat = aparser.nextValue();
 -          file = aparser.nextValue();
 +      }
 +      if (headless)
 +      {
 +        af.getViewport().getCalcManager().shutdown();
 +      }
 +    }
 +    // extract groovy arguments before anything else.
 +    // Once all other stuff is done, execute any groovy scripts (in order)
 +    if (!isJS && groovyscript != null)
 +    {
 +      if (Cache.groovyJarsPresent())
 +      {
 +        // TODO: DECIDE IF THIS SECOND PASS AT GROOVY EXECUTION IS STILL REQUIRED !!
 +        System.out.println("Executing script " + groovyscript);
 +        executeGroovyScript(groovyscript, af);
 +        System.out.println("CMD groovy[" + groovyscript
 +                    + "] executed successfully!");
  
 -          if (outputFormat.equalsIgnoreCase("png"))
 -          {
 -            af.createPNG(new File(file));
 -            imageName = (new File(file)).getName();
 -            System.out.println("Creating PNG image: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("svg"))
 -          {
 -            File imageFile = new File(file);
 -            imageName = imageFile.getName();
 -            af.createSVG(imageFile);
 -            System.out.println("Creating SVG image: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("html"))
 -          {
 -            File imageFile = new File(file);
 -            imageName = imageFile.getName();
 -            HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
 -            htmlSVG.exportHTML(file);
 +      }
 +      else
 +      {
 +        System.err.println(
 +                "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
 +                        + groovyscript);
 +      }
 +    }
  
 -            System.out.println("Creating HTML image: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("biojsmsa"))
 -          {
 -            if (file == null)
 -            {
 -              System.err.println("The output html file must not be null");
 -              return;
 -            }
 -            try
 -            {
 -              BioJsHTMLOutput.refreshVersionInfo(
 -                      BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
 -            } catch (URISyntaxException e)
 -            {
 -              e.printStackTrace();
 -            }
 -            BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
 -            bjs.exportHTML(file);
 -            System.out
 -                    .println("Creating BioJS MSA Viwer HTML file: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("imgMap"))
 -          {
 -            af.createImageMap(new File(file), imageName);
 -            System.out.println("Creating image map: " + file);
 -            continue;
 -          }
 -          else if (outputFormat.equalsIgnoreCase("eps"))
 -          {
 -            File outputFile = new File(file);
 -            System.out.println(
 -                    "Creating EPS file: " + outputFile.getAbsolutePath());
 -            af.createEPS(outputFile);
 -            continue;
 -          }
 -          FileFormatI outFormat = null;
 -          try
 -          {
 -            outFormat = FileFormats.getInstance().forName(outputFormat);
 -          } catch (Exception formatP)
 -          {
 -            System.out.println("Couldn't parse " + outFormat
 -                    + " as a valid Jalview format string.");
 -          }
 -          if (outFormat != null)
 -          {
 -            if (!outFormat.isWritable())
 -            {
 -              System.out.println(
 -                      "This version of Jalview does not support alignment export as "
 -                              + outputFormat);
 -            }
 -            else
 -            {
 -              af.saveAlignment(file, outFormat);
 -              if (af.isSaveAlignmentSuccessful())
 -              {
 -                System.out.println("Written alignment in "
 -                        + outFormat.getName() + " format to " + file);
 -              }
 -              else
 -              {
 -                System.out.println("Error writing file " + file + " in "
 -                        + outFormat.getName() + " format!!");
 -              }
 -            }
 -          }
 +    // and finally, turn off batch mode indicator - if the desktop still exists
 +    if (desktop != null)
 +    {
 +      if (progress != -1)
 +      {
 +        desktop.setProgressBar(null, progress);
 +      }
 +      desktop.setInBatchMode(false);
 +    }
 +    
 +    if (jsApp != null) {
 +      jsApp.callInitCallback();
 +    }
 +  }
 +  
 +  /**
 +   * Set general display parameters irrespective of file loading or headlessness.
 +   * 
 +   * @param aparser
 +   */
 +  private void setDisplayParameters(ArgsParser aparser)
 +  {
 +    if (aparser.contains(ArgsParser.NOMENUBAR))
 +    {
 +      noMenuBar = true;
 +      System.out.println("CMD [nomenu] executed successfully!");
 +    }
  
 -        }
 +    if (aparser.contains(ArgsParser.NOSTATUS))
 +    {
 +      noStatus = true;
 +      System.out.println("CMD [nostatus] executed successfully!");
 +    }
  
 -        while (aparser.getSize() > 0)
 -        {
 -          System.out.println("Unknown arg: " + aparser.nextValue());
 -        }
 -      }
 +    if (aparser.contains(ArgsParser.NOANNOTATION)
 +            || aparser.contains(ArgsParser.NOANNOTATION2))
 +    {
 +      noAnnotation = true;
 +      System.out.println("CMD no-annotation executed successfully!");
      }
 -    AlignFrame startUpAlframe = null;
 -    // We'll only open the default file if the desktop is visible.
 -    // And the user
 -    // ////////////////////
 +    if (aparser.contains(ArgsParser.NOCALCULATION))
 +    {
 +      noCalculation = true;
 +      System.out.println("CMD [nocalculation] executed successfully!");
 +    }
 +  }
  
 -    if (!Platform.isJS() && !headless && file == null
 -            && Cache.getDefault("SHOW_STARTUP_FILE", true))
 -    /**
 -     * Java only
 -     * 
 -     * @j2sIgnore
 -     */
 +  private void setFrameDependentProperties(ArgsParser aparser,
 +          AlignFrame af)
 +  {
 +    String data = aparser.getValue(ArgsParser.COLOUR, true);
 +    if (data != null)
      {
 -      file = Cache.getDefault("STARTUP_FILE",
 -              Cache.getDefault("www.jalview.org", "https://www.jalview.org")
 -                      + "/examples/exampleFile_2_7.jvp");
 -      if (file.equals("http://www.jalview.org/examples/exampleFile_2_3.jar")
 -              || file.equals(
 -                      "http://www.jalview.org/examples/exampleFile_2_7.jar"))
 +      data.replaceAll("%20", " ");
 +
 +      ColourSchemeI cs = ColourSchemeProperty.getColourScheme(
 +              af.getViewport(), af.getViewport().getAlignment(), data);
 +
 +      if (cs != null)
        {
 -        file.replace("http:", "https:");
 -        // hardwire upgrade of the startup file
 -        file.replace("_2_3", "_2_7");
 -        file.replace("2_7.jar", "2_7.jvp");
 -        // and remove the stale setting
 -        Cache.removeProperty("STARTUP_FILE");
 +        System.out.println(
 +                "CMD [-color " + data + "] executed successfully!");
        }
 +      af.changeColour(cs);
 +    }
  
 -      protocol = AppletFormatAdapter.checkProtocol(file);
 +    // Must maintain ability to use the groups flag
 +    data = aparser.getValue(ArgsParser.GROUPS, true);
 +    if (data != null)
 +    {
 +      af.parseFeaturesFile(data,
 +              AppletFormatAdapter.checkProtocol(data));
 +      // System.out.println("Added " + data);
 +      System.out.println(
 +              "CMD groups[-" + data + "]  executed successfully!");
 +    }
 +    data = aparser.getValue(ArgsParser.FEATURES, true);
 +    if (data != null)
 +    {
 +      af.parseFeaturesFile(data,
 +              AppletFormatAdapter.checkProtocol(data));
 +      // System.out.println("Added " + data);
 +      System.out.println(
 +              "CMD [-features " + data + "]  executed successfully!");
 +    }
 +    data = aparser.getValue(ArgsParser.ANNOTATIONS, true);
 +    if (data != null)
 +    {
 +      af.loadJalviewDataFile(data, null, null, null);
 +      // System.out.println("Added " + data);
 +      System.out.println(
 +              "CMD [-annotations " + data + "] executed successfully!");
 +    }
  
 -      if (file.endsWith(".jar"))
 +    // JavaScript feature
 +
 +    if (aparser.contains(ArgsParser.SHOWOVERVIEW))
 +    {
 +      af.overviewMenuItem_actionPerformed(null);
 +      System.out.println("CMD [showoverview] executed successfully!");
 +    }
 +
 +    // set or clear the sortbytree flag.
 +    if (aparser.contains(ArgsParser.SORTBYTREE))
 +    {
 +      af.getViewport().setSortByTree(true);
 +      if (af.getViewport().getSortByTree())
        {
 -        format = FileFormat.Jalview;
 +        System.out.println("CMD [-sortbytree] executed successfully!");
        }
 -      else
 +    }
 +
 +    boolean doUpdateAnnotation = false;
 +    /**
 +     * we do this earlier in JalviewJS because of a complication with
 +     * SHOWOVERVIEW
 +     * 
 +     * For now, just fixing this in JalviewJS.
 +     *
 +     * 
 +     * @j2sIgnore
 +     * 
 +     */
 +    {
 +      if (noAnnotation)
        {
 -        try
 +        af.getViewport().setShowAnnotation(false);
 +        if (!af.getViewport().isShowAnnotation())
          {
 -          format = new IdentifyFile().identify(file, protocol);
 -        } catch (FileFormatException e)
 -        {
 -          // TODO what?
 +          doUpdateAnnotation = true;
          }
        }
 -      startUpAlframe = fileLoader.LoadFileWaitTillLoaded(file, protocol,
 -              format);
 -      // extract groovy arguments before anything else.
      }
  
 -    // Once all other stuff is done, execute any groovy scripts (in order)
 -    if (groovyscript != null)
 +    if (aparser.contains(ArgsParser.NOSORTBYTREE))
      {
 -      if (Cache.groovyJarsPresent())
 +      af.getViewport().setSortByTree(false);
 +      if (!af.getViewport().getSortByTree())
        {
 -        System.out.println("Executing script " + groovyscript);
 -        executeGroovyScript(groovyscript, startUpAlframe);
 +        doUpdateAnnotation = true;
 +        System.out
 +                .println("CMD [-nosortbytree] executed successfully!");
        }
 -      else
 +    }
 +    if (doUpdateAnnotation)
 +    { // BH 2019.07.24
 +      af.setMenusForViewport();
 +      af.alignPanel.updateLayout();
 +    }
 +
 +    data = aparser.getValue(ArgsParser.TREE, true);
 +    if (data != null)
 +    {
 +      try
        {
 -        System.err.println(
 -                "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
 -                        + groovyscript);
 +        NewickFile nf = new NewickFile(data,
 +                AppletFormatAdapter.checkProtocol(data));
 +        af.getViewport()
 +                .setCurrentTree(af.showNewickTree(nf, data).getTree());
 +        System.out.println(
 +                "CMD [-tree " + data + "] executed successfully!");
 +      } catch (IOException ex)
 +      {
 +        System.err.println("Couldn't add tree " + data);
 +        ex.printStackTrace(System.err);
        }
      }
 -    // and finally, turn off batch mode indicator - if the desktop still exists
 -    if (desktop != null)
 +    // TODO - load PDB structure(s) to alignment JAL-629
 +    // (associate with identical sequence in alignment, or a specified
 +    // sequence)
 +
 +  }
 +
 +  /**
 +   * Writes an output file for each format (if any) specified in the
 +   * command-line arguments. Supported formats are currently
 +   * <ul>
 +   * <li>png</li>
 +   * <li>svg</li>
 +   * <li>html</li>
 +   * <li>biojsmsa</li>
 +   * <li>imgMap</li>
 +   * <li>eps</li>
 +   * </ul>
 +   * A format parameter should be followed by a parameter specifying the output
 +   * file name. {@code imgMap} parameters should follow those for the
 +   * corresponding alignment image output.
 +   * 
 +   * @param aparser
 +   * @param format
 +   */
 +  private void createOutputFiles(ArgsParser aparser, FileFormatI format)
 +  {
 +    // logic essentially the same as 2.11.2/2.11.3 but uses a switch instead
 +    AlignFrame af = currentAlignFrame;
 +    while (aparser.getSize() >= 2)
      {
 -      if (progress != -1)
 +      String outputFormat = aparser.nextValue();
 +      File imageFile;
 +      String fname;
 +      switch (outputFormat.toLowerCase(Locale.ROOT))
        {
 -        desktop.setProgressBar(null, progress);
 +      case "png":
 +        imageFile = new File(aparser.nextValue());
 +        af.createPNG(imageFile);
 +        System.out.println(
 +                "Creating PNG image: " + imageFile.getAbsolutePath());
 +        continue;
 +      case "svg":
 +        imageFile = new File(aparser.nextValue());
 +        af.createSVG(imageFile);
 +        System.out.println(
 +                "Creating SVG image: " + imageFile.getAbsolutePath());
 +        continue;
 +      case "eps":
 +        imageFile = new File(aparser.nextValue());
 +        System.out.println(
 +                "Creating EPS file: " + imageFile.getAbsolutePath());
 +        af.createEPS(imageFile);
 +        continue;
 +      case "biojsmsa":
 +        fname = new File(aparser.nextValue()).getAbsolutePath();
 +        try
 +        {
 +          BioJsHTMLOutput.refreshVersionInfo(
 +                  BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
 +        } catch (URISyntaxException e)
 +        {
 +          e.printStackTrace();
 +        }
 +        BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
 +        bjs.exportHTML(fname);
 +        System.out.println("Creating BioJS MSA Viwer HTML file: " + fname);
 +        continue;
 +      case "html":
 +        fname = new File(aparser.nextValue()).getAbsolutePath();
 +        HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
 +        htmlSVG.exportHTML(fname);
 +        System.out.println("Creating HTML image: " + fname);
 +        continue;
 +      case "imgmap":
 +        imageFile = new File(aparser.nextValue());
 +        af.alignPanel.makePNGImageMap(imageFile, "unnamed.png");
 +        System.out.println(
 +                "Creating image map: " + imageFile.getAbsolutePath());
 +        continue;
 +      default:
 +        // fall through - try to parse as an alignment data export format
 +        FileFormatI outFormat = null;
 +        try
 +        {
 +          outFormat = FileFormats.getInstance().forName(outputFormat);
 +        } catch (Exception formatP)
 +        {
 +        }
 +        if (outFormat == null)
 +        {
 +          System.out.println("Couldn't parse " + outputFormat
 +                  + " as a valid Jalview format string.");
 +          continue;
 +        }
 +        if (!outFormat.isWritable())
 +        {
 +          System.out.println(
 +                  "This version of Jalview does not support alignment export as "
 +                          + outputFormat);
 +          continue;
 +        }
 +        // record file as it was passed to Jalview so it is recognisable to the CLI
 +        // caller
 +        String file;
 +        fname = new File(file = aparser.nextValue()).getAbsolutePath();
 +        // JBPNote - yuck - really wish we did have a bean returned from this which gave
 +        // success/fail like before !
 +        af.saveAlignment(fname, outFormat);
 +        if (!af.isSaveAlignmentSuccessful())
 +        {
 +          System.out.println("Written alignment in " + outputFormat
 +                  + " format to " + file);
 +          continue;
 +        }
 +        else
 +        {
 +          System.out.println("Error writing file " + file + " in "
 +                  + outputFormat + " format!!");
 +        }
        }
 -      desktop.setInBatchMode(false);
 +    }
 +    // ??? Should report - 'ignoring' extra args here...
 +    while (aparser.getSize() > 0)
 +    {
 +      System.out.println("Ignoring extra argument: " + aparser.nextValue());
      }
    }
  
              "javax.swing.plaf.nimbus.NimbusLookAndFeel", false);
    }
  
+   private static boolean setFlatLookAndFeel()
+   {
+     boolean set = setSpecificLookAndFeel("flatlaf light",
+             "com.formdev.flatlaf.FlatLightLaf", false);
+     if (set)
+     {
+       if (Platform.isMac())
+       {
+         System.setProperty("apple.laf.useScreenMenuBar", "true");
+         System.setProperty("apple.awt.application.name",
+                 ChannelProperties.getProperty("app_name"));
+         System.setProperty("apple.awt.application.appearance", "system");
+         if (SystemInfo.isMacFullWindowContentSupported
 -                && Desktop.desktop != null)
++            && Desktop.getInstance() != null)
+         {
 -          Desktop.desktop.getRootPane()
++        Desktop.getInstance().getRootPane()
+                   .putClientProperty("apple.awt.fullWindowContent", true);
 -          Desktop.desktop.getRootPane()
++          Desktop.getInstance().getRootPane()
+                   .putClientProperty("apple.awt.transparentTitleBar", true);
+         }
+         SwingUtilities.invokeLater(() -> {
+           FlatLightLaf.setup();
+         });
+       }
+       UIManager.put("TabbedPane.showTabSeparators", true);
+       UIManager.put("TabbedPane.tabSeparatorsFullHeight", true);
+       UIManager.put("TabbedPane.tabsOverlapBorder", true);
+       // UIManager.put("TabbedPane.hasFullBorder", true);
+       UIManager.put("TabbedPane.tabLayoutPolicy", "scroll");
+       UIManager.put("TabbedPane.scrollButtonsPolicy", "asNeeded");
+       UIManager.put("TabbedPane.smoothScrolling", true);
+       UIManager.put("TabbedPane.tabWidthMode", "compact");
+       UIManager.put("TabbedPane.selectedBackground", Color.white);
+     }
+     return set;
+   }
 -
    private static boolean setQuaquaLookAndFeel()
    {
      return setSpecificLookAndFeel("quaqua",
@@@ -18,44 -38,24 +38,45 @@@ import javax.swing.Timer
  public class JalviewJS2
  {
  
-   static {
+   static
+   {
      /**
 -     * @j2sNative
 +     * @ could do it this way:
       * 
 -     *            J2S.thisApplet.__Info.args =
 -     *            ["open","examples/uniref50.fa","features",
 -     *            "examples/exampleFeatures.txt"];
 +     * j2sNative
 +     * 
 +     * J2S.thisApplet.__Info.args = [ "open","examples/uniref50.fa",
 +     * "features","examples/exampleFeatures.txt", "noannotation" ];
       */
    }
  
    public static void main(String[] args) throws Exception
    {
 +    if (args.length == 0)
 +    {
 +      args = new String[] {
 +        //  "headless",
 +          "open", "examples/uniref50.fa",
 +//          "features",
 +//          "examples/exampleFeatures.txt"
 +//          , "noannotation"
 +          //, "showoverview"
 +          //, "png", "test-bh.png"
 +      };
 +    }
 +
 +    // String cmds = "nodisplay -open examples/uniref50.fa -sortbytree -props
 +    // test/jalview/io/testProps.jvprops -colour zappo "
 +    // + "-jabaws http://www.compbio.dundee.ac.uk/jabaws -nosortbytree "
 +    // + "-features examples/testdata/plantfdx.features -annotations
 +    // examples/testdata/plantfdx.annotations -tree
 +    // examples/testdata/uniref50_test_tree";
 +    // args = cmds.split(" ");
      Jalview.main(args);
-       //showFocusTimer();
- }
+     // showFocusTimer();
+   }
  
- protected static int focusTime = 0;
+   protected static int focusTime = 0;
  
    private static void showFocusTimer()
    {
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -48,10 -48,6 +48,9 @@@ public interface SequenceI extends ASeq
     */
    public void setName(String name);
  
 +  public HiddenMarkovModel getHMM();
 +
 +  public void setHMM(HiddenMarkovModel hmm);
    /**
     * Get the display name
     */
     *         list
     */
    public List<DBRefEntry> getPrimaryDBRefs();
 +  /**
 +   * Answers true if the sequence has annotation for Hidden Markov Model
 +   * information content, else false
 +   */
 +  boolean hasHMMAnnotation();
  
    /**
     * Returns a (possibly empty) list of sequence features that overlap the given
     *          iterator over regions
     * @return first residue not contained in regions
     */
    public int firstResidueOutsideIterator(Iterator<int[]> it);
  
 +
 +  /**
 +   * Answers true if this sequence has an associated Hidden Markov Model
 +   * 
 +   * @return
 +   */
 +  boolean hasHMMProfile();
  }
 +
  
  package jalview.fts.service.uniprot;
  
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
- import jalview.bin.Cache;
- import jalview.fts.api.FTSData;
- import jalview.fts.api.FTSDataColumnI;
- import jalview.fts.api.FTSRestClientI;
- import jalview.fts.core.FTSRestClient;
- import jalview.fts.core.FTSRestRequest;
- import jalview.fts.core.FTSRestResponse;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
+ import java.lang.invoke.MethodHandles;
+ import java.net.MalformedURLException;
+ import java.net.URL;
 +
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
@@@ -45,30 -36,67 +39,72 @@@ import com.sun.jersey.api.client.Client
  import com.sun.jersey.api.client.WebResource;
  import com.sun.jersey.api.client.config.DefaultClientConfig;
  
+ import jalview.bin.Cache;
+ import jalview.bin.Console;
+ import jalview.fts.api.FTSData;
+ import jalview.fts.api.FTSDataColumnI;
++import jalview.fts.api.FTSRestClientI;
+ import jalview.fts.core.FTSRestClient;
+ import jalview.fts.core.FTSRestRequest;
+ import jalview.fts.core.FTSRestResponse;
+ import jalview.util.ChannelProperties;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
+ /*
+  * 2022-07-20 bsoares
+  * See https://issues.jalview.org/browse/JAL-4036
+  * The new Uniprot API is not dissimilar to the old one, but has some important changes.
+  * Some group names have changed slightly, some old groups have gone and there are quite a few new groups.
+  * 
+  * Most changes are mappings of old column ids to new field ids. There are a handful of old
+  * columns not mapped to new fields, and new fields without an old column.
+  * [aside: not all possible columns were listed in the resources/fts/uniprot_data_columns.txt file.
+  * These were presumably additions after the file was created]
+  * For existing/mapped fields, the same preferences found in the resource file have been migrated to
+  * the new file with the new field name, id and group.
+  * 
+  * The new mapped groups and files are stored and read from resources/fts/uniprot_data_columns-2022.txt.
+  * 
+  * There is now no "sort" query string parameter.
+  * 
+  * See https://www.uniprot.org/help/api_queries
+  * 
+  * SIGNIFICANT CHANGE: Pagination is no longer performed using a record offset, but with a "cursor"
+  * query string parameter that is not really a cursor.  The value is an opaque string that is passed (or
+  * rather a whole URL is passed) in the "Link" header of the HTTP response of the previous page.
+  * Where such a link is passed it is put into the cursors ArrayList.
+  * There are @Overridden methods in UniprotFTSPanel.
+  */
 -
  public class UniProtFTSRestClient extends FTSRestClient
- implements ApplicationSingletonI
++    implements ApplicationSingletonI,FTSRestClientI
  {
- public static FTSRestClientI getInstance()
++public static UniProtFTSRestClient getInstance()
 +{
- return (FTSRestClientI) ApplicationSingletonProvider
++return (UniProtFTSRestClient) ApplicationSingletonProvider
 +    .getInstance(UniProtFTSRestClient.class);
 +}
+   private static final String DEFAULT_UNIPROT_DOMAIN = "https://rest.uniprot.org";
  
-   private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
+   private static final String USER_AGENT = ChannelProperties
+           .getProperty("app_name", "Jalview") + " "
+           + Cache.getDefault("VERSION", "Unknown") + " "
+           + MethodHandles.lookup().lookupClass() + " help@jalview.org";
  
    static
    {
      Platform.addJ2SDirectDatabaseCall(DEFAULT_UNIPROT_DOMAIN);
    }
  
 -  private static UniProtFTSRestClient instance = null;
    public final String uniprotSearchEndpoint;
  
 -  public UniProtFTSRestClient()
 +  private UniProtFTSRestClient()
    {
      super();
-     uniprotSearchEndpoint = Cache.getDefault("UNIPROT_DOMAIN",
-             DEFAULT_UNIPROT_DOMAIN) + "/uniprot/";    
+     this.clearCursors();
+     uniprotSearchEndpoint = Cache.getDefault("UNIPROT_2022_DOMAIN",
+             DEFAULT_UNIPROT_DOMAIN) + "/uniprotkb/search";
    }
  
    @SuppressWarnings("unchecked")
  
        WebResource webResource = null;
        webResource = client.resource(uniprotSearchEndpoint)
-               .queryParam("format", "tab")
-               .queryParam("columns", wantedFields)
-               .queryParam("limit", String.valueOf(responseSize))
-               .queryParam("offset", String.valueOf(offSet))
-               .queryParam("sort", "score").queryParam("query", query);
+               .queryParam("format", "tsv")
+               .queryParam("fields", wantedFields)
+               .queryParam("size", String.valueOf(responseSize))
+               /* 2022 new api has no "sort"
+                * .queryParam("sort", "score")
+                */
+               .queryParam("query", query);
+       if (offSet != 0 && cursor != null && cursor.length() > 0)
+       // 2022 new api does not do pagination with an offset, it requires a
+       // "cursor" parameter with a key (given for the next page).
+       // (see https://www.uniprot.org/help/pagination)
+       {
+         webResource = webResource.queryParam("cursor", cursor);
+       }
+       Console.debug(
+               "Uniprot FTS Request: " + webResource.getURI().toString());
        // Execute the REST request
-       ClientResponse clientResponse = webResource
-               .accept(MediaType.TEXT_PLAIN).get(clientResponseClass);
+       WebResource.Builder wrBuilder = webResource
+               .accept(MediaType.TEXT_PLAIN);
+       if (!Platform.isJS())
+       /**
+        * Java only
+        * 
+        * @j2sIgnore
+        */
+       {
+         wrBuilder.header("User-Agent", USER_AGENT);
+       }
+       ClientResponse clientResponse = wrBuilder.get(clientResponseClass);
+       if (!Platform.isJS())
+       /**
+        * Java only
+        * 
+        * @j2sIgnore
+        */
+       {
+         if (clientResponse.getHeaders().containsKey("Link"))
+         {
+           // extract the URL from the 'Link: <URL>; ref="stuff"' header
+           String linkHeader = clientResponse.getHeaders().get("Link")
+                   .get(0);
+           if (linkHeader.indexOf("<") > -1)
+           {
+             String temp = linkHeader.substring(linkHeader.indexOf("<") + 1);
+             if (temp.indexOf(">") > -1)
+             {
+               String nextUrl = temp.substring(0, temp.indexOf(">"));
+               // then get the cursor value from the query string parameters
+               String nextCursor = getQueryParam("cursor", nextUrl);
+               setCursor(cursorPage + 1, nextCursor);
+             }
+           }
+         }
+       }
 -
        String uniProtTabDelimittedResponseString = clientResponse
                .getEntity(String.class);
        // Make redundant objects eligible for garbage collection to conserve
      };
    }
  
 -  public static UniProtFTSRestClient getInstance()
 -  {
 -    if (instance == null)
 -    {
 -      instance = new UniProtFTSRestClient();
 -    }
 -    return instance;
 -  }
    @Override
    public String getColumnDataConfigFileName()
    {
-     return "/fts/uniprot_data_columns.txt";
+     return "/fts/uniprot_data_columns-2022.txt";
+   }
+   /* 2022-07-20 bsoares
+    * used for the new API "cursor" pagination. See https://www.uniprot.org/help/pagination
+    */
+   private ArrayList<String> cursors;
+   private int cursorPage = 0;
+   protected int getCursorPage()
+   {
+     return cursorPage;
+   }
+   protected void setCursorPage(int i)
+   {
+     cursorPage = i;
+   }
+   protected void setPrevCursorPage()
+   {
+     if (cursorPage > 0)
+       cursorPage--;
+   }
+   protected void setNextCursorPage()
+   {
+     cursorPage++;
+   }
+   protected void clearCursors()
+   {
+     cursors = new ArrayList(10);
    }
  
+   protected String getCursor(int i)
+   {
+     return cursors.get(i);
+   }
+   protected String getNextCursor()
+   {
+     if (cursors.size() < cursorPage + 2)
+       return null;
+     return cursors.get(cursorPage + 1);
+   }
+   protected String getPrevCursor()
+   {
+     if (cursorPage == 0)
+       return null;
+     return cursors.get(cursorPage - 1);
+   }
+   protected void setCursor(int i, String c)
+   {
+     cursors.ensureCapacity(i + 1);
+     while (cursors.size() <= i)
+     {
+       cursors.add(null);
+     }
+     cursors.set(i, c);
+     Console.debug(
+             "Set UniprotFRSRestClient cursors[" + i + "] to '" + c + "'");
+     // cursors.add(c);
+   }
+   public static String getQueryParam(String param, String u)
+   {
+     if (param == null || u == null)
+       return null;
+     try
+     {
+       URL url = new URL(u);
+       String[] kevs = url.getQuery().split("&");
+       for (int j = 0; j < kevs.length; j++)
+       {
+         String[] kev = kevs[j].split("=", 2);
+         if (param.equals(kev[0]))
+         {
+           return kev[1];
+         }
+       }
+     } catch (MalformedURLException e)
+     {
+       Console.warn("Could not obtain next page 'cursor' value from 'u");
+     }
+     return null;
+   }
 -}
 +}
@@@ -22,13 -22,6 +22,12 @@@ package jalview.gui
  
  import java.util.Locale;
  
 +import java.io.IOException;
 +import java.util.HashSet;
 +import java.util.Set;
 +
 +import javax.swing.JFileChooser;
 +import javax.swing.JOptionPane;
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -447,28 -350,8 +447,28 @@@ public class AlignFrame extends GAlignF
     */
    void init()
    {
 +    boolean newPanel = (alignPanel == null);
 +    viewport.setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
 +    if (newPanel)
 +    {
 +      if (Platform.isJS())
 +      {
 +        // need to set this up front if NOANNOTATION is
 +        // used in conjunction with SHOWOVERVIEW.
 +
 +        // I have not determined if this is appropriate for
 +        // Jalview/Java, as it means we are setting this flag
 +        // for all subsequent AlignFrames. For now, at least,
 +        // I am setting it to be JalviewJS-only.
 +
 +        boolean showAnnotation = Jalview.getInstance().getShowAnnotation();
 +        viewport.setShowAnnotation(showAnnotation);
 +      }
 +      alignPanel = new AlignmentPanel(this, viewport);
 +    }
 +    addAlignmentPanel(alignPanel, newPanel);
      // setBackground(Color.white); // BH 2019
      if (!Jalview.isHeadlessMode())
      {
        progressBar = new ProgressBar(this.statusPanel, this.statusBar);
        // modifyPID.setEnabled(false);
      }
  
-     String sortby = jalview.bin.Cache.getDefault(Preferences.SORT_ALIGNMENT,
 -    String sortby = Cache.getDefault("SORT_ALIGNMENT", "No sort");
++    String sortby = Cache.getDefault(Preferences.SORT_ALIGNMENT,
 +            "No sort");
  
      if (sortby.equals("Id"))
      {
        wrapMenuItem_actionPerformed(null);
      }
  
-     if (jalview.bin.Cache.getDefault(Preferences.SHOW_OVERVIEW, false))
 -    if (Cache.getDefault("SHOW_OVERVIEW", false))
++    if (Cache.getDefault(Preferences.SHOW_OVERVIEW, false))
      {
        this.overviewMenuItem_actionPerformed(null);
      }
        }
        lastSaveSuccessful = new Jalview2XML().saveAlignment(this, file,
                shortName);
        statusBar.setText(MessageManager.formatMessage(
                "label.successfully_saved_to_file_in_format", new Object[]
 -              { file, format }));
 +              { fileName, format }));
        return;
      }
  
  
    @Override
    public void associatedData_actionPerformed(ActionEvent e)
 +          throws IOException, InterruptedException
    {
      final JalviewFileChooser chooser = new JalviewFileChooser(
-             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
+             Cache.getProperty("LAST_DIRECTORY"));
      chooser.setFileView(new JalviewFileView());
      String tooltip = MessageManager
              .getString("label.load_jalview_annotations");
          viewport.sendSelection();
          viewport.getAlignment().deleteGroup(sg);
  
 -        viewport.firePropertyChange("alignment", null,
 -                viewport.getAlignment().getSequences());
 +        viewport.notifyAlignment();
          if (viewport.getAlignment().getHeight() < 1)
          {
            try
      return tp;
    }
  
 -  private boolean buildingMenu = false;
  
    /**
 -   * Generates menu items and listener event actions for web service clients
 -   * 
 +   * Schedule the web services menu rebuild to the event dispatch thread.
     */
 -  public void BuildWebServiceMenu()
 +  public void buildWebServicesMenu()
    {
 -    while (buildingMenu)
 -    {
 -      try
 +    SwingUtilities.invokeLater(() -> {
-       Cache.log.info("Rebuiling WS menu");
++      Console.info("Rebuiling WS menu");
 +      webService.removeAll();
 +      if (Cache.getDefault("SHOW_SLIVKA_SERVICES", true))
        {
-         Cache.log.info("Building web service menu for slivka");
 -        System.err.println("Waiting for building menu to finish.");
 -        Thread.sleep(10);
 -      } catch (Exception e)
++        Console.info("Building web service menu for slivka");
 +        SlivkaWSDiscoverer discoverer = SlivkaWSDiscoverer.getInstance();
 +        JMenu submenu = new JMenu("Slivka");
 +        buildWebServicesMenu(discoverer, submenu);
 +        webService.add(submenu);
 +      }
 +      if (Cache.getDefault("SHOW_JWS2_SERVICES", true))
        {
 +        WSDiscovererI jws2servs = Jws2Discoverer.getInstance();
 +        JMenu submenu = new JMenu("JABAWS");
 +        buildLegacyWebServicesMenu(submenu);
 +        buildWebServicesMenu(jws2servs, submenu);
 +        webService.add(submenu);
        }
 -    }
 -    final AlignFrame me = this;
 -    buildingMenu = true;
 -    new Thread(new Runnable()
 +      build_fetchdbmenu(webService);
 +    });
 +  }
 +
 +  private void buildLegacyWebServicesMenu(JMenu menu)
 +  {
 +    JMenu secstrmenu = new JMenu("Secondary Structure Prediction");
 +    if (Discoverer.getServices() != null && Discoverer.getServices().size() > 0) 
      {
 -      @Override
 -      public void run()
 +      var secstrpred = Discoverer.getServices().get("SecStrPred");
 +      if (secstrpred != null) 
        {
 -        final List<JMenuItem> legacyItems = new ArrayList<>();
 -        try
 -        {
 -          // System.err.println("Building ws menu again "
 -          // + Thread.currentThread());
 -          // TODO: add support for context dependent disabling of services based
 -          // on
 -          // alignment and current selection
 -          // TODO: add additional serviceHandle parameter to specify abstract
 -          // handler
 -          // class independently of AbstractName
 -          // TODO: add in rediscovery GUI function to restart discoverer
 -          // TODO: group services by location as well as function and/or
 -          // introduce
 -          // object broker mechanism.
 -          final Vector<JMenu> wsmenu = new Vector<>();
 -          final IProgressIndicator af = me;
 -
 -          /*
 -           * do not i18n these strings - they are hard-coded in class
 -           * compbio.data.msa.Category, Jws2Discoverer.isRecalculable() and
 -           * SequenceAnnotationWSClient.initSequenceAnnotationWSClient()
 -           */
 -          final JMenu msawsmenu = new JMenu("Alignment");
 -          final JMenu secstrmenu = new JMenu(
 -                  "Secondary Structure Prediction");
 -          final JMenu seqsrchmenu = new JMenu("Sequence Database Search");
 -          final JMenu analymenu = new JMenu("Analysis");
 -          final JMenu dismenu = new JMenu("Protein Disorder");
 -          // JAL-940 - only show secondary structure prediction services from
 -          // the legacy server
 -          if (// Cache.getDefault("SHOW_JWS1_SERVICES", true)
 -              // &&
 -          Discoverer.services != null && (Discoverer.services.size() > 0))
 -          {
 -            // TODO: refactor to allow list of AbstractName/Handler bindings to
 -            // be
 -            // stored or retrieved from elsewhere
 -            // No MSAWS used any more:
 -            // Vector msaws = null; // (Vector)
 -            // Discoverer.services.get("MsaWS");
 -            Vector<ServiceHandle> secstrpr = Discoverer.services
 -                    .get("SecStrPred");
 -            if (secstrpr != null)
 -            {
 -              // Add any secondary structure prediction services
 -              for (int i = 0, j = secstrpr.size(); i < j; i++)
 -              {
 -                final ext.vamsas.ServiceHandle sh = secstrpr.get(i);
 -                jalview.ws.WSMenuEntryProviderI impl = jalview.ws.jws1.Discoverer
 -                        .getServiceClient(sh);
 -                int p = secstrmenu.getItemCount();
 -                impl.attachWSMenuEntry(secstrmenu, me);
 -                int q = secstrmenu.getItemCount();
 -                for (int litm = p; litm < q; litm++)
 -                {
 -                  legacyItems.add(secstrmenu.getItem(litm));
 -                }
 -              }
 -            }
 -          }
 -
 -          // Add all submenus in the order they should appear on the web
 -          // services menu
 -          wsmenu.add(msawsmenu);
 -          wsmenu.add(secstrmenu);
 -          wsmenu.add(dismenu);
 -          wsmenu.add(analymenu);
 -          // No search services yet
 -          // wsmenu.add(seqsrchmenu);
 -
 -          javax.swing.SwingUtilities.invokeLater(new Runnable()
 -          {
 -            @Override
 -            public void run()
 -            {
 -              try
 -              {
 -                webService.removeAll();
 -                // first, add discovered services onto the webservices menu
 -                if (wsmenu.size() > 0)
 -                {
 -                  for (int i = 0, j = wsmenu.size(); i < j; i++)
 -                  {
 -                    webService.add(wsmenu.get(i));
 -                  }
 -                }
 -                else
 -                {
 -                  webService.add(me.webServiceNoServices);
 -                }
 -                // TODO: move into separate menu builder class.
 -                {
 -                  // logic for 2.11.1.4 is
 -                  // always look to see if there is a discover. if there isn't
 -                  // we can't show any Jws2 services
 -                  // if there are services available, show them - regardless of
 -                  // the 'show JWS2 preference'
 -                  // if the discoverer is running then say so
 -                  // otherwise offer to trigger discovery if 'show JWS2' is not
 -                  // enabled
 -                  Jws2Discoverer jws2servs = Jws2Discoverer.getDiscoverer();
 -                  if (jws2servs != null)
 -                  {
 -                    if (jws2servs.hasServices())
 -                    {
 -                      jws2servs.attachWSMenuEntry(webService, me);
 -                      for (Jws2Instance sv : jws2servs.getServices())
 -                      {
 -                        if (sv.description.toLowerCase(Locale.ROOT)
 -                                .contains("jpred"))
 -                        {
 -                          for (JMenuItem jmi : legacyItems)
 -                          {
 -                            jmi.setVisible(false);
 -                          }
 -                        }
 -                      }
 -                    }
 -
 -                    if (jws2servs.isRunning())
 -                    {
 -                      JMenuItem tm = new JMenuItem(
 -                              "Still discovering JABA Services");
 -                      tm.setEnabled(false);
 -                      webService.add(tm);
 -                    }
 -                    else if (!Cache.getDefault("SHOW_JWS2_SERVICES", true))
 -                    {
 -                      JMenuItem enableJws2 = new JMenuItem(
 -                              "Discover Web Services");
 -                      enableJws2.setToolTipText(
 -                              "Select to start JABA Web Service discovery (or enable option in Web Service preferences)");
 -                      enableJws2.setEnabled(true);
 -                      enableJws2.addActionListener(new ActionListener()
 -                      {
 -
 -                        @Override
 -                        public void actionPerformed(ActionEvent e)
 -                        {
 -                          // start service discoverer, but ignore preference
 -                          Desktop.instance.startServiceDiscovery(false,
 -                                  true);
 -                        }
 -                      });
 -                      webService.add(enableJws2);
 -                    }
 -                  }
 -                }
 -                build_urlServiceMenu(me.webService);
 -                build_fetchdbmenu(webService);
 -                for (JMenu item : wsmenu)
 -                {
 -                  if (item.getItemCount() == 0)
 -                  {
 -                    item.setEnabled(false);
 -                  }
 -                  else
 -                  {
 -                    item.setEnabled(true);
 -                  }
 -                }
 -              } catch (Exception e)
 -              {
 -                Console.debug(
 -                        "Exception during web service menu building process.",
 -                        e);
 -              }
 -            }
 -          });
 -        } catch (Exception e)
 +        for (ext.vamsas.ServiceHandle sh : secstrpred) 
          {
 +          var menuProvider = Discoverer.getServiceClient(sh);
 +          menuProvider.attachWSMenuEntry(secstrmenu, this);
          }
 -        buildingMenu = false;
        }
 -    }).start();
 +    }
 +    menu.add(secstrmenu);
 +  }
  
 +  /**
 +   * Constructs the web services menu for the given discoverer under the
 +   * specified menu. This method must be called on the EDT
 +   * 
 +   * @param discoverer
 +   *          the discoverer used to build the menu
 +   * @param menu
 +   *          parent component which the elements will be attached to
 +   */
 +  private void buildWebServicesMenu(WSDiscovererI discoverer, JMenu menu)
 +  {
 +    if (discoverer.hasServices())
 +    {
 +      PreferredServiceRegistry.getRegistry().populateWSMenuEntry(
 +              discoverer.getServices(), sv -> buildWebServicesMenu(), menu,
 +              this, null);
 +    }
 +    if (discoverer.isRunning())
 +    {
 +      JMenuItem item = new JMenuItem("Service discovery in progress.");
 +      item.setEnabled(false);
 +      menu.add(item);
 +    }
 +    else if (!discoverer.hasServices())
 +    {
 +      JMenuItem item = new JMenuItem("No services available.");
 +      item.setEnabled(false);
 +      menu.add(item);
 +    }
    }
  
    /**
@@@ -592,7 -578,7 +593,8 @@@ public void setNormaliseSequenceLogo(bo
    {
      return validCharWidth;
    }
 +  
    private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<>();
  
    public AutoCalcSetting getCalcIdSettingsFor(String calcId)
                @Override
                public void run()
                {
-                   addDataToAlignment(al);
+                 addDataToAlignment(al);
                }
 -            }).setResponseHandler(1, new Runnable()
 +            }).setResponseHandler(SPLIT_FRAME, new Runnable()
              {
                @Override
                public void run()
      {
        return;
      }
-     
 -
      FeatureRenderer fr = getAlignPanel().getSeqPanel().seqCanvas
              .getFeatureRenderer();
 -    List<String> origRenderOrder = new ArrayList<>();
 -    List<String> origGroups = new ArrayList<>();
 +    List<String> origRenderOrder = new ArrayList(),
 +            origGroups = new ArrayList();
-     // preserve original render order - allows differentiation between user configured colours and autogenerated ones
+     // preserve original render order - allows differentiation between user
+     // configured colours and autogenerated ones
      origRenderOrder.addAll(fr.getRenderOrder());
      origGroups.addAll(fr.getFeatureGroups());
  
@@@ -311,9 -297,10 +312,10 @@@ public class AlignmentPanel extends GAl
     * @return Dimension giving the maximum width of the alignment label panel
     *         that should be used.
     */
 -  protected Dimension calculateIdWidth(int maxwidth)
 +  public Dimension calculateIdWidth(int maxwidth)
    {
 -    Container c = new Container();
 +    Container c = this;// new Container();
      FontMetrics fm = c.getFontMetrics(
              new Font(av.font.getName(), Font.ITALIC, av.font.getSize()));
  
      int i = 0;
      int idWidth = 0;
  
 +    boolean withSuffix = av.getShowJVSuffix();
      while ((i < al.getHeight()) && (al.getSequenceAt(i) != null))
      {
        SequenceI s = al.getSequenceAt(i);
      Dimension e = idPanel.getSize();
      alabels.setSize(new Dimension(e.width, annotationHeight));
  
++
      annotationSpaceFillerHolder.setPreferredSize(new Dimension(
              annotationSpaceFillerHolder.getWidth(), annotationHeight));
      annotationScroller.validate();
      {
        annotationScroller.setVisible(true);
        annotationSpaceFillerHolder.setVisible(true);
 +    }
 +
 +    idSpaceFillerPanel1.setVisible(!wrap);
 +
 +    /*
 +     * defer dimension calculations if panel not yet added to a Window
 +     * BH 2020.06.09
 +     */
 +    if (getTopLevelAncestor() == null)
 +    {
 +      repaint();
 +      return;
 +    }
 +
 +    if (!wrap && av.isShowAnnotation())
 +    {
        validateAnnotationDimensions(false);
      }
      int canvasWidth = getSeqPanel().seqCanvas.getWidth();
      if (canvasWidth > 0)
      { // may not yet be laid out
      {
        int width = av.getAlignment().getVisibleWidth();
        int height = av.getAlignment().getHeight();
 -      hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
 -      vextent = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
 -
 -      if (hextent > width)
 -      {
 -        hextent = width;
 -      }
 -
 -      if (vextent > height)
 -      {
 -        vextent = height;
 -      }
 -
 -      if ((hextent + x) > width)
 -      {
 -        x = width - hextent;
 -      }
 -
 -      if ((vextent + y) > height)
 -      {
 -        y = height - vextent;
 -      }
 -
 -      if (y < 0)
 -      {
 -        y = 0;
 -      }
 -
 -      if (x < 0)
 -      {
 -        x = 0;
 -      }
 -
 -      // update the scroll values
 -      hscroll.setValues(x, hextent, 0, width);
 -      vscroll.setValues(y, vextent, 0, height);
 +      
 +      hextent = Math.min(getSeqPanel().seqCanvas.getWidth() / av.getCharWidth(),  width);
 +      vextent = Math.min(getSeqPanel().seqCanvas.getHeight() / av.getCharHeight(),  height);
 +  
 +      x = Math.max(0, Math.min(x,  width - hextent));
 +      y = Math.max(0, Math.min(y,  height - vextent));
 +      
 +      updateRanges(x, y);
 +      updateScrollBars(x, y, width, height);
      }
    }
  
 +  private void updateScrollBars(int x, int y, int width, int height) 
 +  {
 +    hscroll.setValues(x, hextent, 0, width);
 +    vscroll.setValues(y, vextent, 0, height);
 +  }
    /**
     * Respond to adjustment event when horizontal or vertical scrollbar is
     * changed
        return;
      }
  
 -    ViewportRanges ranges = av.getRanges();
      if (evt.getSource() == hscroll)
      {
 +      if (!updateRanges(hscroll.getValue(), Integer.MIN_VALUE))
 +        return;
 +    }
 +    else if (evt.getSource() == vscroll)
 +    {
 +      if (!updateRanges(Integer.MIN_VALUE, vscroll.getValue()))
 +        return;
 +    }
 +    repaint();
 +  }
 +
 +  private boolean updateRanges(int x, int y)
 +  {
 +    ViewportRanges ranges = av.getRanges();
 +    boolean isChanged = false;
 +    if (x != Integer.MIN_VALUE)
 +    {
        int oldX = ranges.getStartRes();
        int oldwidth = ranges.getViewportWidth();
 -      int x = hscroll.getValue();
        int width = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
  
        // if we're scrolling to the position we're already at, stop
      }
      if (updateOverview)
      {
 +      alignFrame.repaint();
        if (overviewPanel != null)
        {
          overviewPanel.updateOverviewImage();
      } catch (Exception ex)
      {
      }
 +
      if (b)
      {
-       alignFrame.setDisplayedView(this);
+       setAlignFrameView();
      }
    }
  
      }
    }
  
 +  void updateScrollBarsFromRanges()
 +  {
 +    ViewportRanges ranges = av.getRanges();
 +    setScrollValues(ranges.getStartRes(), ranges.getStartSeq());
 +  }
    /**
     * Set the reference to the PCA/Tree chooser dialog for this panel. This
     * reference should be nulled when the dialog is closed.
Simple merge
@@@ -732,8 -731,9 +732,9 @@@ public class AnnotationPanel extends JP
         * drag is diagonal - defer deciding whether to
         * treat as up/down or left/right
         */
 -      return;
 -    }
 +        return;
 +      }
      try
      {
        if (dragMode == DragMode.Resize)
    }
  
    private volatile boolean imageFresh = false;
--
    private Rectangle visibleRect = new Rectangle(),
            clipBounds = new Rectangle();
  
    @Override
    public void paintComponent(Graphics g)
    {
--
      // BH: note that this method is generally recommended to
      // call super.paintComponent(g). Otherwise, the children of this
      // component will not be rendered. That is not needed here
      // just a JPanel contained in a JViewPort.
  
      computeVisibleRect(visibleRect);
--
      g.setColor(Color.white);
      g.fillRect(0, 0, visibleRect.width, visibleRect.height);
  
                        .getClipBounds(clipBounds)).width)
                || (visibleRect.height != clipBounds.height))
        {
-         g.drawImage(image, 0, 0, this);
 -        g.drawImage(image, 0, 0, this);
++        
++        g.drawImage(image, 0, 0, this);
          fastPaint = false;
          return;
        }
      }
 -    imgWidth = (av.getRanges().getEndRes() - av.getRanges().getStartRes()
 -            + 1) * av.getCharWidth();
 +    imgWidth = (ranges.getEndRes() - ranges.getStartRes() + 1)
 +            * av.getCharWidth();
      if (imgWidth < 1)
      {
 +      fastPaint = false;
        return;
      }
      Graphics2D gg;
        gg = (Graphics2D) image.getGraphics();
  
      }
--
-     drawComponent(gg, ranges.getStartRes(), av.getRanges().getEndRes() + 1);
++    
+     drawComponent(gg, av.getRanges().getStartRes(),
+             av.getRanges().getEndRes() + 1);
      gg.dispose();
      imageFresh = false;
      g.drawImage(image, 0, 0, this);
      int er = av.getRanges().getEndRes() + 1;
      int transX = 0;
  
 +    if (er == sr + 1)
 +    {
 +      fastPaint = false;
 +      return;
 +    }
      Graphics2D gg = (Graphics2D) image.getGraphics();
  
-     gg.copyArea(0, 0, imgWidth, getHeight(),
-             -horizontal * av.getCharWidth(), 0);
-     if (horizontal > 0) // scrollbar pulled right, image to the left
-     {
-       transX = (er - sr - horizontal) * av.getCharWidth();
-       sr = er - horizontal;
-     }
-     else if (horizontal < 0)
-     {
-       er = sr - horizontal;
+     if (imgWidth>Math.abs(horizontal*av.getCharWidth())) {
+       //scroll is less than imgWidth away so can re-use buffered graphics
+       gg.copyArea(0, 0, imgWidth, getHeight(),
+               -horizontal * av.getCharWidth(), 0);
+       
+       if (horizontal > 0) // scrollbar pulled right, image to the left
+       {
+         transX = (er - sr - horizontal) * av.getCharWidth();
+         sr = er - horizontal;
+       }
+       else if (horizontal < 0)
+       {
+         er = sr - horizontal;
+       }
      }
 +
      gg.translate(transX, 0);
  
      drawComponent(gg, sr, er);
      gg.translate(-transX, 0);
  
      gg.dispose();
--
      fastPaint = true;
  
      // Call repaint on alignment panel so that repaints from other alignment
  
    private int[] bounds = new int[2];
  
 +  private boolean allowFastPaint;
    @Override
    public int[] getVisibleVRange()
    {
      }
      return annotationHeight;
    }
 +  
 +  /**
 +   * Clears the flag that allows a 'fast paint' on the next repaint, so
 +   * requiring a full repaint
 +   */
 +  public void setNoFastPaint()
 +  {
 +    allowFastPaint = false;
 +  }
  }
Simple merge
@@@ -282,7 -283,7 +283,7 @@@ public class AppJmolBinding extends Jal
      }
      if (errormsgs.length() > 0)
      {
--      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
++      JvOptionPane.showInternalMessageDialog(Desktop.getInstance(),
                MessageManager.formatMessage(
                        "label.pdb_entries_couldnt_be_retrieved", new String[]
                        { errormsgs.toString() }),
Simple merge
Simple merge
@@@ -450,11 -453,10 +453,10 @@@ public class CrossRefAction implements 
        copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
                xrefs.getSequencesArray(), dataset);
      }
-     copyAlignment
-             .setGapCharacter(alignFrame.viewport.getGapCharacter());
+     copyAlignment.setGapCharacter(alignFrame.viewport.getGapCharacter());
  
      StructureSelectionManager ssm = StructureSelectionManager
 -            .getStructureSelectionManager(Desktop.instance);
 +            .getStructureSelectionManager(Desktop.getInstance());
  
      /*
       * register any new mappings for sequence mouseover etc
@@@ -119,7 -113,6 +119,8 @@@ import jalview.io.FormatAdapter
  import jalview.io.IdentifyFile;
  import jalview.io.JalviewFileChooser;
  import jalview.io.JalviewFileView;
++import jalview.jbgui.APQHandlers;
 +import jalview.jbgui.GDesktop;
  import jalview.jbgui.GSplitFrame;
  import jalview.jbgui.GStructureViewer;
  import jalview.project.Jalview2XML;
@@@ -128,12 -121,12 +129,12 @@@ import jalview.urls.IdOrgSettings
  import jalview.util.BrowserLauncher;
  import jalview.util.ChannelProperties;
  import jalview.util.ImageMaker.TYPE;
+ import jalview.util.LaunchUtils;
  import jalview.util.MessageManager;
  import jalview.util.Platform;
--import jalview.util.ShortcutKeyMaskExWrapper;
  import jalview.util.UrlConstants;
  import jalview.viewmodel.AlignmentViewport;
 +import jalview.ws.WSDiscovererI;
  import jalview.ws.params.ParamManager;
  import jalview.ws.utils.UrlDownloadClient;
  
   * @author $author$
   * @version $Revision: 1.155 $
   */
 -public class Desktop extends jalview.jbgui.GDesktop
 +@SuppressWarnings("serial")
 +public class Desktop extends GDesktop
          implements DropTargetListener, ClipboardOwner, IProgressIndicator,
 -        jalview.api.StructureSelectionManagerProvider
 +        StructureSelectionManagerProvider, ApplicationSingletonI
 +
  {
    private static final String CITATION;
 -  static
 -  {
 -    URL bg_logo_url = ChannelProperties.getImageURL(
 -            "bg_logo." + String.valueOf(SplashScreen.logoSize));
 -    URL uod_logo_url = ChannelProperties.getImageURL(
 -            "uod_banner." + String.valueOf(SplashScreen.logoSize));
 +  static {
 +    URL bg_logo_url = ChannelProperties.getImageURL("bg_logo." + String.valueOf(SplashScreen.logoSize));
 +    URL uod_logo_url = ChannelProperties.getImageURL("uod_banner." + String.valueOf(SplashScreen.logoSize));
      boolean logo = (bg_logo_url != null || uod_logo_url != null);
      StringBuilder sb = new StringBuilder();
-     sb.append("<br><br>Development managed by The Barton Group, University of Dundee, Scotland, UK.");
-     if (logo) {
+     sb.append(
+             "<br><br>Jalview is free software released under GPLv3.<br><br>Development is managed by The Barton Group, University of Dundee, Scotland, UK.");
+     if (logo)
+     {
        sb.append("<br>");
      }
 -    sb.append(bg_logo_url == null ? ""
 -            : "<img alt=\"Barton Group logo\" src=\""
 -                    + bg_logo_url.toString() + "\">");
 +    sb.append(bg_logo_url == null ? "" : "<img alt=\"Barton Group logo\" src=\"" + bg_logo_url.toString() + "\">");
      sb.append(uod_logo_url == null ? ""
 -            : "&nbsp;<img alt=\"University of Dundee shield\" src=\""
 -                    + uod_logo_url.toString() + "\">");
 +        : "&nbsp;<img alt=\"University of Dundee shield\" src=\"" + uod_logo_url.toString() + "\">");
      sb.append(
-         "<br><br>For help, see the FAQ at <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and/or join the jalview-discuss@jalview.org mailing list");
+             "<br><br>For help, see <a href=\"https://www.jalview.org/faq\">www.jalview.org/faq</a> and join <a href=\"https://discourse.jalview.org\">discourse.jalview.org</a>");
      sb.append("<br><br>If  you use Jalview, please cite:"
-         + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
-         + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
-         + "<br>Bioinformatics doi: 10.1093/bioinformatics/btp033");
+             + "<br>Waterhouse, A.M., Procter, J.B., Martin, D.M.A, Clamp, M. and Barton, G. J. (2009)"
+             + "<br>Jalview Version 2 - a multiple sequence alignment editor and analysis workbench"
+             + "<br>Bioinformatics <a href=\"https://doi.org/10.1093/bioinformatics/btp033\">doi: 10.1093/bioinformatics/btp033</a>");
      CITATION = sb.toString();
    }
  
    }
  
    /**
 -   * Creates a new Desktop object.
 +   * Private constructor enforces singleton pattern. It is called by reflection
 +   * from ApplicationSingletonProvider.getInstance().
     */
 -  public Desktop()
 +  private Desktop()
    {
      super();
-     Cache.initLogger();
 -    /**
 -     * A note to implementors. It is ESSENTIAL that any activities that might
 -     * block are spawned off as threads rather than waited for during this
 -     * constructor.
 -     */
 -    instance = this;
 +    try
 +    {
 +      /**
 +       * A note to implementors. It is ESSENTIAL that any activities that might
 +       * block are spawned off as threads rather than waited for during this
 +       * constructor.
 +       */
  
 -    doConfigureStructurePrefs();
 -    setTitle(ChannelProperties.getProperty("app_name") + " "
 -            + Cache.getProperty("VERSION"));
 +      doConfigureStructurePrefs();
 +    setTitle(ChannelProperties.getProperty("app_name") + " " + Cache.getProperty("VERSION"));
  
      /**
       * Set taskbar "grouped windows" name for linux desktops (works in GNOME and
 -     * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not
 -     * officially documented or guaranteed to exist, so we access it via
 -     * reflection. There appear to be unfathomable criteria about what this
 -     * string can contain, and it if doesn't meet those criteria then "java"
 -     * (KDE) or "jalview-bin-Jalview" (GNOME) is used. "Jalview", "Jalview
 -     * Develop" and "Jalview Test" seem okay, but "Jalview non-release" does
 -     * not. The reflection access may generate a warning: WARNING: An illegal
 -     * reflective access operation has occurred WARNING: Illegal reflective
 -     * access by jalview.gui.Desktop () to field
 +     * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not officially
 +     * documented or guaranteed to exist, so we access it via reflection. There
 +     * appear to be unfathomable criteria about what this string can contain, and it
 +     * if doesn't meet those criteria then "java" (KDE) or "jalview-bin-Jalview"
 +     * (GNOME) is used. "Jalview", "Jalview Develop" and "Jalview Test" seem okay,
 +     * but "Jalview non-release" does not. The reflection access may generate a
 +     * warning: WARNING: An illegal reflective access operation has occurred
 +     * WARNING: Illegal reflective access by jalview.gui.Desktop () to field
       * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided.
       */
-     if (Platform.isLinux()) {
-       try {
+     if (Platform.isLinux())
+     {
+       if (LaunchUtils.getJavaVersion() >= 11)
+       {
+         jalview.bin.Console.info(
+                 "Linux platform only! You may have the following warning next: \"WARNING: An illegal reflective access operation has occurred\"\nThis is expected and cannot be avoided, sorry about that.");
+       }
+       try
+       {
          Toolkit xToolkit = Toolkit.getDefaultToolkit();
          Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
          Field awtAppClassNameField = null;
          }
  
          String title = ChannelProperties.getProperty("app_name");
 -        if (awtAppClassNameField != null)
 -        {
 +        if (awtAppClassNameField != null) {
            awtAppClassNameField.setAccessible(true);
            awtAppClassNameField.set(xToolkit, title);
-         } else {
-           Cache.log.debug("XToolkit: awtAppClassName not found");
          }
-       } catch (Exception e) {
-         Cache.debug("Error setting awtAppClassName");
-         Cache.trace(Cache.getStackTraceString(e));
+         else
+         {
+           jalview.bin.Console.debug("XToolkit: awtAppClassName not found");
+         }
+       } catch (Exception e)
+       {
+         jalview.bin.Console.debug("Error setting awtAppClassName");
+         jalview.bin.Console.trace(Cache.getStackTraceString(e));
        }
      }
  
 +    /**
 +     * APQHandlers sets handlers for About, Preferences and Quit actions peculiar to
 +     * macOS's application menu. APQHandlers will check to see if a handler is
 +     * supported before setting it.
 +     */
 +    try {
 +      APQHandlers.setAPQHandlers(this);
 +    } catch (Exception e) {
 +      System.out.println("Cannot set APQHandlers");
 +      // e.printStackTrace();
 +    } catch (Throwable t) {
-       Cache.warn("Error setting APQHandlers: " + t.toString());
-       Cache.trace(Cache.getStackTraceString(t));
++      jalview.bin.Console.warn("Error setting APQHandlers: " + t.toString());
++      jalview.bin.Console.trace(Cache.getStackTraceString(t));
 +    }
++
      setIconImages(ChannelProperties.getIconList());
  
 -    addWindowListener(new WindowAdapter()
 -    {
 +    addWindowListener(new WindowAdapter() {
  
        @Override
 -      public void windowClosing(WindowEvent ev)
 -      {
 +      public void windowClosing(WindowEvent ev) {
          quit();
        }
      });
  
 -    boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
 +      boolean selmemusage = Cache.getDefault("SHOW_MEMUSAGE", false);
  
 -    boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
 -    desktop = new MyDesktopPane(selmemusage);
 +      boolean showjconsole = Cache.getDefault("SHOW_JAVA_CONSOLE", false);
 +      desktopPane = new MyDesktopPane(selmemusage);
  
 -    showMemusage.setSelected(selmemusage);
 -    desktop.setBackground(Color.white);
 +      showMemusage.setSelected(selmemusage);
 +      desktopPane.setBackground(Color.white);
  
 -    getContentPane().setLayout(new BorderLayout());
 -    // alternate config - have scrollbars - see notes in JAL-153
 -    // JScrollPane sp = new JScrollPane();
 -    // sp.getViewport().setView(desktop);
 -    // getContentPane().add(sp, BorderLayout.CENTER);
 +      getContentPane().setLayout(new BorderLayout());
 +      // alternate config - have scrollbars - see notes in JAL-153
 +      // JScrollPane sp = new JScrollPane();
 +      // sp.getViewport().setView(desktop);
 +      // getContentPane().add(sp, BorderLayout.CENTER);
  
 -    // BH 2018 - just an experiment to try unclipped JInternalFrames.
 -    if (Platform.isJS())
 -    {
 -      getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 -    }
 +      // BH 2018 - just an experiment to try unclipped JInternalFrames.
 +      if (Platform.isJS())
 +      {
 +        getRootPane().putClientProperty("swingjs.overflow.hidden", "false");
 +      }
  
 -    getContentPane().add(desktop, BorderLayout.CENTER);
 -    desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
 +      getContentPane().add(desktopPane, BorderLayout.CENTER);
 +      desktopPane.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
  
 -    // This line prevents Windows Look&Feel resizing all new windows to maximum
 -    // if previous window was maximised
 -    desktop.setDesktopManager(new MyDesktopManager(
 -            (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 -                    : Platform.isAMacAndNotJS()
 -                            ? new AquaInternalFrameManager(
 -                                    desktop.getDesktopManager())
 -                            : desktop.getDesktopManager())));
  
 -    Rectangle dims = getLastKnownDimensions("");
 -    if (dims != null)
 -    {
 -      setBounds(dims);
 -    }
 -    else
 -    {
 -      Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 -      int xPos = Math.max(5, (screenSize.width - 900) / 2);
 -      int yPos = Math.max(5, (screenSize.height - 650) / 2);
 -      setBounds(xPos, yPos, 900, 650);
 -    }
 +      // This line prevents Windows Look&Feel resizing all new windows to
 +      // maximum
 +      // if previous window was maximised
 +      desktopPane.setDesktopManager(new MyDesktopManager(
 +              (Platform.isWindowsAndNotJS() ? new DefaultDesktopManager()
 +                      : Platform.isAMacAndNotJS()
 +                              ? new AquaInternalFrameManager(
 +                                      desktopPane.getDesktopManager())
 +                              : desktopPane.getDesktopManager())));
  
 -    if (!Platform.isJS())
 -    /**
 -     * Java only
 -     * 
 -     * @j2sIgnore
 -     */
 -    {
 -      jconsole = new Console(this, showjconsole);
 -      jconsole.setHeader(Cache.getVersionDetailsForConsole());
 -      showConsole(showjconsole);
 +      Rectangle dims = getLastKnownDimensions("");
 +      if (dims != null)
 +      {
 +        setBounds(dims);
 +      }
 +      else
 +      {
 +        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 +        int xPos = Math.max(5, (screenSize.width - 900) / 2);
 +        int yPos = Math.max(5, (screenSize.height - 650) / 2);
 +        setBounds(xPos, yPos, 900, 650);
 +      }
  
 -      showNews.setVisible(false);
 +      if (!Platform.isJS())
 +      /**
 +       * Java only
 +       * 
 +       * @j2sIgnore
 +       */
 +      {
 +        jconsole = new Console(this, showjconsole);
 +        jconsole.setHeader(Cache.getVersionDetailsForConsole());
 +        showConsole(showjconsole);
  
 -      experimentalFeatures.setSelected(showExperimental());
 +        showNews.setVisible(false); // not sure if we should only do this for interactive session?
  
 -      getIdentifiersOrgData();
 +        experimentalFeatures.setSelected(showExperimental());
  
 -      checkURLLinks();
 +        getIdentifiersOrgData();
  
 -      // Spawn a thread that shows the splashscreen
 -      if (!nosplash)
 -      {
 -        SwingUtilities.invokeLater(new Runnable()
 +        if (Jalview.isInteractive())
          {
 -          @Override
 -          public void run()
 -          {
 -            new SplashScreen(true);
 +          // disabled for SeqCanvasTest
 +          checkURLLinks();
 +
 +          // Spawn a thread that shows the splashscreen
 +          if (!nosplash) {
 +          SwingUtilities.invokeLater(new Runnable()
 +           {
 +             @Override
 +             public void run()
 +             {
 +               new SplashScreen(true);
 +             }
 +           });
            }
 -        });
 +
 +          // Thread off a new instance of the file chooser - this reduces the
 +          // time
 +          // it
 +          // takes to open it later on.
 +          new Thread(new Runnable()
 +          {
 +            @Override
 +            public void run()
 +            {
-               Cache.log.debug("Filechooser init thread started.");
++              jalview.bin.Console.debug("Filechooser init thread started.");
 +              String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 +              JalviewFileChooser.forRead(
 +                      Cache.getProperty("LAST_DIRECTORY"), fileFormat);
-               Cache.log.debug("Filechooser init thread finished.");
++              jalview.bin.Console.debug("Filechooser init thread finished.");
 +            }
 +          }).start();
 +          // Add the service change listener
 +          changeSupport.addJalviewPropertyChangeListener("services",
 +                  new PropertyChangeListener()
 +                  {
 +
 +                    @Override
 +                    public void propertyChange(PropertyChangeEvent evt)
 +                    {
-                       Cache.log.debug("Firing service changed event for "
++                      jalview.bin.Console.debug("Firing service changed event for "
 +                              + evt.getNewValue());
 +                      JalviewServicesChanged(evt);
 +                    }
 +                  });
 +        }
        }
  
 -      // Thread off a new instance of the file chooser - this reduces the time
 -      // it
 -      // takes to open it later on.
 -      new Thread(new Runnable()
 +      this.setDropTarget(new java.awt.dnd.DropTarget(desktopPane, this));
 +
 +      this.addWindowListener(new WindowAdapter()
        {
          @Override
 -        public void run()
 +        public void windowClosing(WindowEvent evt)
          {
 -          jalview.bin.Console.debug("Filechooser init thread started.");
 -          String fileFormat = Cache.getProperty("DEFAULT_FILE_FORMAT");
 -          JalviewFileChooser.forRead(Cache.getProperty("LAST_DIRECTORY"),
 -                  fileFormat);
 -          jalview.bin.Console.debug("Filechooser init thread finished.");
 +          quit();
          }
 -      }).start();
 -      // Add the service change listener
 -      changeSupport.addJalviewPropertyChangeListener("services",
 -              new PropertyChangeListener()
 -              {
 -
 -                @Override
 -                public void propertyChange(PropertyChangeEvent evt)
 -                {
 -                  jalview.bin.Console
 -                          .debug("Firing service changed event for "
 -                                  + evt.getNewValue());
 -                  JalviewServicesChanged(evt);
 -                }
 -              });
 -    }
 -
 -    this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
 -
 -    this.addWindowListener(new WindowAdapter()
 -    {
 -      @Override
 -      public void windowClosing(WindowEvent evt)
 -      {
 -        quit();
 -      }
 -    });
 +      });
  
 -    MouseAdapter ma;
 -    this.addMouseListener(ma = new MouseAdapter()
 -    {
 -      @Override
 -      public void mousePressed(MouseEvent evt)
 +      MouseAdapter ma;
 +      this.addMouseListener(ma = new MouseAdapter()
        {
 -        if (evt.isPopupTrigger()) // Mac
 +        @Override
 +        public void mousePressed(MouseEvent evt)
          {
 -          showPasteMenu(evt.getX(), evt.getY());
 +          if (evt.isPopupTrigger()) // Mac
 +          {
 +            showPasteMenu(evt.getX(), evt.getY());
 +          }
          }
 -      }
 -
 -      @Override
 -      public void mouseReleased(MouseEvent evt)
 -      {
 -        if (evt.isPopupTrigger()) // Windows
 +        @Override
 +        public void mouseReleased(MouseEvent evt)
          {
 -          showPasteMenu(evt.getX(), evt.getY());
 +          if (evt.isPopupTrigger()) // Windows
 +          {
 +            showPasteMenu(evt.getX(), evt.getY());
 +          }
          }
 -      }
 -    });
 -    desktop.addMouseListener(ma);
 +      });
 +      desktopPane.addMouseListener(ma);
 +    } catch (Throwable t)
 +    {
 +      t.printStackTrace();
 +    }
    }
  
    /**
      }
    }
  
 -  public void checkForNews()
 -  {
 +  public void checkForNews() {
      final Desktop me = this;
      // Thread off the news reader, in case there are connection problems.
 -    new Thread(new Runnable()
 -    {
 +    new Thread(new Runnable() {
        @Override
-       public void run() {
-         Cache.log.debug("Starting news thread.");
+       public void run()
+       {
+         jalview.bin.Console.debug("Starting news thread.");
          jvnews = new BlogReader(me);
          showNews.setVisible(true);
-         Cache.log.debug("Completed news thread.");
+         jalview.bin.Console.debug("Completed news thread.");
        }
      }).start();
    }
  
 -  public void getIdentifiersOrgData()
 -  {
 -    if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null)
 -    {// Thread off the identifiers fetcher
 -      new Thread(new Runnable()
 -      {
 +  public void getIdentifiersOrgData() {
 +    if (Cache.getProperty("NOIDENTIFIERSSERVICE") == null) {
 +      // Thread off the identifiers fetcher
 +      new Thread(new Runnable() {
          @Override
-         public void run() {
-           Cache.log.debug("Downloading data from identifiers.org");
-           try {
-             UrlDownloadClient.download(IdOrgSettings.getUrl(), IdOrgSettings.getDownloadLocation());
-           } catch (IOException e) {
-             Cache.log.debug("Exception downloading identifiers.org data" + e.getMessage());
+         public void run()
+         {
 -          jalview.bin.Console
 -                  .debug("Downloading data from identifiers.org");
++          jalview.bin.Console.debug("Downloading data from identifiers.org");
+           try
+           {
+             UrlDownloadClient.download(IdOrgSettings.getUrl(),
+                     IdOrgSettings.getDownloadLocation());
+           } catch (IOException e)
+           {
 -            jalview.bin.Console
 -                    .debug("Exception downloading identifiers.org data"
++            jalview.bin.Console.debug("Exception downloading identifiers.org data"
+                             + e.getMessage());
            }
          }
        }).start();
            frame.setIcon(false);
          } catch (java.beans.PropertyVetoException ex)
          {
 +          // System.err.println(ex.toString());
          }
        }
      });
      /*
       * set up key bindings for Ctrl-W and Cmd-W, with the same (Close) action
       */
 -    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            InputEvent.CTRL_DOWN_MASK);
 -    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
 +    KeyStroke ctrlWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK);
-     KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx());
++    KeyStroke cmdWKey = KeyStroke.getKeyStroke(KeyEvent.VK_W, Platform.SHORTCUT_KEY_MASK);
  
      InputMap inputMap = frame
              .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
      panel.add(label);
  
      /*
 -     * the URL to fetch is input in Java: an editable combobox with history JS:
 -     * (pending JAL-3038) a plain text field
 +     * the URL to fetch is
 +     * Java: an editable combobox with history
 +     * JS: (pending JAL-3038) a plain text field
       */
      JComponent history;
-     String urlBase = "http://www.";
+     String urlBase = "https://www.";
      if (Platform.isJS())
      {
        history = new JTextField(urlBase, 35);
          approveSave = true;
        }
      }
 -    if (approveSave || autoSave)
 -    {
 +    if (approveSave || autoSave) {
        final Desktop me = this;
        final java.io.File chosenFile = projectFile;
        new Thread(new Runnable()
                new OOMWarning("Whilst loading project from " + choice, oom);
              } catch (Exception ex)
              {
-               Cache.log.error(
+               jalview.bin.Console.error(
                        "Problems whilst loading project from " + choice, ex);
 -              JvOptionPane.showMessageDialog(Desktop.desktop,
 +              JvOptionPane.showMessageDialog(getDesktopPane(),
                        MessageManager.formatMessage(
                                "label.error_whilst_loading_project_from",
                                new Object[]
                    10, getHeight() - fm.getHeight());
          }
        }
        // output debug scale message. Important for jalview.bin.HiDPISettingTest2
--      Desktop.debugScaleMessage(Desktop.getDesktop().getGraphics());
++      Desktop.debugScaleMessage(Desktop.getDesktopPane().getGraphics());
      }
    }
  
        openGroovyConsole();
      } catch (Exception ex)
      {
-       Cache.log.error("Groovy Shell Creation failed.", ex);
+       jalview.bin.Console.error("Groovy Shell Creation failed.", ex);
 -      JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 +      JvOptionPane.showInternalMessageDialog(desktopPane,
  
                MessageManager.getString("label.couldnt_create_groovy_shell"),
                MessageManager.getString("label.groovy_support_failed"),
      startServiceDiscovery(false);
    }
  
+   /**
+    * start service discovery threads - blocking or non-blocking
+    * 
+    * @param blocking
+    */
    public void startServiceDiscovery(boolean blocking)
    {
-     System.out.println("Starting service discovery");
 -    startServiceDiscovery(blocking, false);
 -  }
++    jalview.bin.Console.debug("Starting service discovery");
 -  /**
 -   * start service discovery threads
 -   * 
 -   * @param blocking
 -   *          - false means call returns immediately
 -   * @param ignore_SHOW_JWS2_SERVICES_preference
 -   *          - when true JABA services are discovered regardless of user's JWS2
 -   *          discovery preference setting
 -   */
 -  public void startServiceDiscovery(boolean blocking,
 -          boolean ignore_SHOW_JWS2_SERVICES_preference)
 -  {
 -    boolean alive = true;
 -    Thread t0 = null, t1 = null, t2 = null;
 +    var tasks = new ArrayList<Future<?>>();
      // JAL-940 - JALVIEW 1 services are now being EOLed as of JABA 2.1 release
 -    if (true)
 +
 +    System.out.println("loading services");
 +    
 +    /** @j2sIgnore */
      {
        // todo: changesupport handlers need to be transferred
        if (discoverer == null)
        {
          if (url != null)
          {
-           if (Cache.log != null)
-           {
-             Cache.log.error("Couldn't handle string " + url + " as a URL.");
-           }
-           else
-           {
-             System.err.println(
-                     "Couldn't handle string " + url + " as a URL.");
-           }
 -          jalview.bin.Console
 -                  .error("Couldn't handle string " + url + " as a URL.");
++          // TODO does error send to stderr if no log exists ?
++          jalview.bin.Console.error("Couldn't handle string " + url + " as a URL.");
          }
          // ignore any exceptions due to dud links.
        }
            SwingUtilities.invokeAndWait(prompter);
          } catch (Exception q)
          {
-           Cache.log.warn("Unexpected Exception in dialog thread.", q);
 -          jalview.bin.Console.warn("Unexpected Exception in dialog thread.",
 -                  q);
++          jalview.bin.Console.warn("Unexpected Exception in dialog thread.", q);
          }
        }
      });
      if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
      {
        // Works on Windows and MacOSX
-       Cache.log.debug("Drop handled as javaFileListFlavor");
+       jalview.bin.Console.debug("Drop handled as javaFileListFlavor");
 -      for (Object file : (List) t
 +      for (File file : (List<File>) t
                .getTransferData(DataFlavor.javaFileListFlavor))
        {
          files.add(file);
            }
            else
            {
-             Cache.log.debug("Couldn't resolve dataflavor for drop: "
-                     + t.toString());
 -            jalview.bin.Console
 -                    .debug("Couldn't resolve dataflavor for drop: "
++            jalview.bin.Console.debug("Couldn't resolve dataflavor for drop: "
+                             + t.toString());
            }
          }
        }
      }
      if (Platform.isWindowsAndNotJS())
      {
-       Cache.log.debug("Scanning dropped content for Windows Link Files");
 -      jalview.bin.Console
 -              .debug("Scanning dropped content for Windows Link Files");
++      jalview.bin.Console.debug("Scanning dropped content for Windows Link Files");
  
        // resolve any .lnk files in the file drop
        for (int f = 0; f < files.size(); f++)
@@@ -1,16 -1,25 +1,36 @@@
+ /*
+  * 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.api.FeatureColourI;
 +import jalview.datamodel.SearchResults;
 +import jalview.datamodel.SearchResultsI;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.JalviewColourChooser.ColourChooserListener;
 +import jalview.io.FeaturesFile;
 +import jalview.schemes.FeatureColour;
 +import jalview.util.ColorUtils;
 +import jalview.util.MessageManager;
 +
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
@@@ -536,8 -559,9 +558,9 @@@ public class FeatureEdito
     */
    protected Runnable getDeleteAction()
    {
 -    Runnable deleteAction = new Runnable()
 +        Runnable deleteAction = new Runnable()
      {
+       @Override
        public void run()
        {
          SequenceFeature sf = features.get(featureIndex);
     */
    protected Runnable getAmendAction()
    {
 -    Runnable okAction = new Runnable()
 +      Runnable okAction = new Runnable()
      {
        boolean useLastDefaults = features.get(0).getType() == null;
 -
 +  
        String featureType = name.getText();
 -
 +  
        String featureGroup = group.getText();
-   
 -
+       @Override
        public void run()
        {
          final String enteredType = name.getText().trim();
Simple merge
@@@ -195,7 -195,7 +195,7 @@@ public class Finder extends GFinde
     */
    boolean getFocusedViewport()
    {
-     if (focusfixed || Desktop.getDesktopPane() == null)
 -    if (focusFixed || Desktop.desktop == null)
++    if (focusFixed || Desktop.getDesktopPane() == null)
      {
        if (ap != null && av != null)
        {
@@@ -66,8 -66,6 +66,7 @@@ public class IdCanvas extends JPanel im
    AnnotationPanel ap;
  
    private Font idfont;
 +  private boolean allowFastPaint;
  
    /**
     * Creates a new IdCanvas object.
    @Override
    public void paintComponent(Graphics g)
    {
 +    if (av.getAlignPanel().getHoldRepaint())
 +    {
 +      return;
 +    }
      g.setColor(Color.white);
      g.fillRect(0, 0, getWidth(), getHeight());
 -
 -    if (fastPaint)
 +    
 +    if (allowFastPaint && fastPaint)
      {
        fastPaint = false;
        g.drawImage(image, 0, 0, this);
      int alignmentWidth = alignViewport.getAlignment().getWidth();
      final int alheight = alignViewport.getAlignment().getHeight();
  
-     
- //    int annotationHeight = 0;
 -    /*
 +
 +    /* (former)
       * assumption: SeqCanvas.calculateWrappedGeometry has been called
 +     * 
 +     * was based on the fact that SeqCanvas was added as a child prior to IdCanvas, 
 +     * and children are processed in order of addition.
 +     * 
 +     * It's probably fine. But...
 +     * 
       */
      SeqCanvas seqCanvas = alignViewport.getAlignPanel()
              .getSeqPanel().seqCanvas;
      AnnotationLabels labels = null;
      if (alignViewport.isShowAnnotation())
      {
 +      // BH when was ap == null?
 +      if (ap == null)
 +      {
 +        ap = new AnnotationPanel(alignViewport);
 +      }
- //      annotationHeight = ap.adjustPanelHeight();
        labels = new AnnotationLabels(alignViewport);
      }
  
Simple merge
@@@ -53,11 -56,6 +55,10 @@@ import jalview.util.Platform
   */
  public final class JvSwingUtils
  {
 +  static final String HTML_PREFIX = (Platform.isJS() ? 
 +          "<html><div style=\"max-width:350px;overflow-wrap:break-word;display:inline-block\">"
 +          : "<html><div style=\"width:350; text-align: justify; word-wrap: break-word;\">"
 +            );
    /**
     * wrap a bare html safe string to around 60 characters per line using a CSS
     * style class specifying word-wrap and break-word
    {
      Objects.requireNonNull(ttext,
              "Tootip text to format must not be null!");
 -    ttext = ttext.trim();
 +    ttext = ttext.trim().replaceAll("<br/>", "<br>");
      boolean maxLengthExceeded = false;
 -    if (ttext.contains("<br>"))
 +    boolean isHTML = ttext.startsWith("<html>");
 +    if (isHTML)
      {
 -      String[] htmllines = ttext.split("<br>");
 -      for (String line : htmllines)
 -      {
 -        maxLengthExceeded = line.length() > 60;
 -        if (maxLengthExceeded)
 -        {
 +      ttext = ttext.substring(6);
 +    }
 +    if (ttext.endsWith("</html>"))
 +    {
 +      isHTML = true;
 +      ttext = ttext.substring(0, ttext.length() - 7);
 +    }
 +    boolean hasBR = ttext.contains("<br>");
 +    enclose |= isHTML || hasBR;
 +    if (hasBR)
 +    {  
 +      int pt = -1, ptlast = -4;
 +      while ((pt = ttext.indexOf("<br>", pt + 1)) >= 0) {
 +        if (pt - ptlast - 4 > 60) {
 +          maxLengthExceeded = true;
            break;
          }
        }
    }
  
    /**
 +   * A convenience method that that adds a component with label to a container,
 +   * sets a tooltip on both component and label, and optionally specifies layout
 +   * constraints for the added component (but not the label)
     * 
 -   * @param panel
 +   * @param container
     * @param tooltip
     * @param label
 -   * @param valBox
 -   * @return the GUI element created that was added to the layout so it's
 -   *         attributes can be changed.
 +   * @param comp
 +   * @param constraints
     */
 -  public static JPanel addtoLayout(JPanel panel, String tooltip,
 -          JComponent label, JComponent valBox)
 +  public static void addtoLayout(Container container, String tooltip,
 +          JComponent label, JComponent comp, String constraints)
    {
 -    JPanel laypanel = new JPanel(new GridLayout(1, 2));
 -    JPanel labPanel = new JPanel(new BorderLayout());
 -    JPanel valPanel = new JPanel();
 -    labPanel.setBounds(new Rectangle(7, 7, 158, 23));
 -    valPanel.setBounds(new Rectangle(172, 7, 270, 23));
 -    labPanel.add(label, BorderLayout.WEST);
 -    valPanel.add(valBox);
 -    laypanel.add(labPanel);
 -    laypanel.add(valPanel);
 -    valPanel.setToolTipText(tooltip);
 -    labPanel.setToolTipText(tooltip);
 -    valBox.setToolTipText(tooltip);
 -    panel.add(laypanel);
 -    panel.validate();
 -    return laypanel;
 +    container.add(label);
 +    container.add(comp, constraints);
 +    comp.setToolTipText(tooltip); // this doesn't seem to show?
 +    label.setToolTipText(tooltip);
    }
  
++  // From 2.11.2 merge
+   public static void mgAddtoLayout(JPanel cpanel, String tooltip,
+           JLabel jLabel, JComponent name)
+   {
+     mgAddtoLayout(cpanel, tooltip, jLabel, name, null);
+   }
+   public static void mgAddtoLayout(JPanel cpanel, String tooltip,
+           JLabel jLabel, JComponent name, String params)
+   {
+     cpanel.add(jLabel);
+     if (params == null)
+     {
+       cpanel.add(name);
+     }
+     else
+     {
+       cpanel.add(name, params);
+     }
+     name.setToolTipText(tooltip);
+     jLabel.setToolTipText(tooltip);
+   }
    /**
     * standard font for labels and check boxes in dialog boxes
     * 
Simple merge
   */
  package jalview.gui;
  
 +import jalview.bin.Cache;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileLoader;
 +import jalview.io.JalviewFileChooser;
 +import jalview.io.JalviewFileView;
 +import jalview.util.MessageManager;
 +import jalview.ws.jws2.dm.JabaOption;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.OptionI;
 +import jalview.ws.params.ParameterI;
 +import jalview.ws.params.ValueConstrainI;
 +import jalview.ws.params.ValueConstrainI.ValueType;
 +import jalview.ws.params.simple.FileParameter;
 +import jalview.ws.params.simple.LogarithmicParameter;
 +import jalview.ws.params.simple.RadioChoiceParameter;
 +import jalview.ws.params.simple.StringParameter;
  import java.awt.BorderLayout;
 +import java.awt.Color;
  import java.awt.Component;
 +import java.awt.Container;
  import java.awt.Dimension;
 +import java.awt.FlowLayout;
  import java.awt.Font;
  import java.awt.Rectangle;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
+ import java.awt.event.FocusAdapter;
+ import java.awt.event.FocusEvent;
 +import java.awt.event.KeyAdapter;
  import java.awt.event.KeyEvent;
 -import java.awt.event.KeyListener;
 +import java.awt.event.MouseAdapter;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
 +import java.io.File;
  import java.net.URL;
  import java.util.ArrayList;
 +import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  
@@@ -603,6 -475,17 +604,7 @@@ public class OptsAndParamsPag
        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
      public int getBaseline(int width, int height)
      {
            }
            else
            {
 -            slider.setVisible(false);
 +            return getSelectedValue(this.parameter,
 +                    choicebox.getSelectedIndex());
            }
          }
 +        slider.setVisible(false);
 +        return valueField.getText().trim();
 +      }
 +
 +      if (validator.getMin() == null || validator.getMax() == null)
 +      {
 +        slider.setVisible(false);
 +      }
 +
 +      valueField.setText(valueField.getText().trim());
 +
 +      /*
 +       * ensure not outside min-max range
 +       * TODO: provide some visual indicator if limit reached
 +       */
 +      try
 +      {
 +        valueField.setBackground(Color.WHITE);
 +        double d = Double.parseDouble(valueField.getText());
 +        if (validator.getMin() != null
 +                && validator.getMin().doubleValue() > d)
 +        {
 +          valueField.setText(formatNumber(validator.getMin()));
 +        }
 +        if (validator.getMax() != null
 +                && validator.getMax().doubleValue() < d)
 +        {
 +          valueField.setText(formatNumber(validator.getMax()));
 +        }
 +      } catch (NumberFormatException e)
 +      {
 +        valueField.setBackground(Color.yellow);
 +        return Float.NaN;
 +      }
 +      if (isIntegerParameter)
 +      {
 +        int iVal = 0;
 +        try
 +        {
 +          iVal = Integer.valueOf(valueField.getText());
 +        } catch (Exception e)
 +        {
 +          valueField.setBackground(Color.yellow);
 +          return Integer.valueOf(0);
 +        }
 +
 +        if (validator.getMin() != null && validator.getMax() != null)
 +        {
 +          slider.getModel().setRangeProperties(iVal, 1,
 +                  validator.getMin().intValue(),
 +                  validator.getMax().intValue() + 1, true);
 +        }
          else
          {
 -          float fVal = 0f;
 -          try
 -          {
 -            valueField.setText(valueField.getText().trim());
 -            fVal = Float.valueOf(valueField.getText());
 -            if (minValue != null && minValue.floatValue() > fVal)
 -            {
 -              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 (maxValue != null && maxValue.floatValue() < fVal)
 -            {
 -              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 (NumberFormatException e)
 -          {
 -            System.err.println(e.toString());
 -          }
 -          if (minValue != null && maxValue != null)
 -          {
 -            slider.setSliderModel(minValue.floatValue(),
 -                    maxValue.floatValue(), fVal);
 -          }
 -          else
 -          {
 -            slider.setVisible(false);
 -          }
 +          slider.setVisible(false);
          }
 +        return Integer.valueOf(iVal);
        }
 -      else
 +      if (isLogarithmicParameter)
        {
 -        if (!choice)
 +        double dVal = 0d;
 +        try
 +        {
 +          double eValue = Double.valueOf(valueField.getText());
 +          dVal = Math.log(eValue);
 +        } catch (Exception e)
 +        {
 +          // shouldn't be possible here
 +          valueField.setBackground(Color.yellow);
 +          return Double.NaN;
 +        }
 +        if (validator.getMin() != null && validator.getMax() != null)
 +        {
 +          double scaleMin = Math.log(validator.getMin().doubleValue())
 +                  * sliderScaleFactor;
 +          double scaleMax = Math.log(validator.getMax().doubleValue())
 +                  * sliderScaleFactor;
 +          slider.getModel().setRangeProperties(
 +                  (int) (sliderScaleFactor * dVal), 1,
 +                  (int) scaleMin, 1 + (int) scaleMax, true);
 +        }
 +        else
          {
            slider.setVisible(false);
          }
 +        return Double.valueOf(dVal);
        }
 +      float fVal = 0f;
 +      try
 +      {
 +        fVal = Float.valueOf(valueField.getText());
 +      } catch (Exception e)
 +      {
 +        return Float.valueOf(0f); // shouldn't happen
 +      }
 +      if (validator.getMin() != null && validator.getMax() != null)
 +      {
 +        float scaleMin = validator.getMin().floatValue()
 +                * sliderScaleFactor;
 +        float scaleMax = validator.getMax().floatValue()
 +                * sliderScaleFactor;
 +        slider.getModel().setRangeProperties(
 +                (int) (fVal * sliderScaleFactor), 1, (int) scaleMin,
 +                1 + (int) scaleMax, true);
 +      }
 +      else
 +      {
 +        slider.setVisible(false);
 +      }
 +      return Float.valueOf(fVal);
      }
    }
  
@@@ -74,24 -73,26 +74,23 @@@ public class OverviewPanel extends JPan
  
    protected boolean draggingBox = false;
  
 +  private Dimension dim;
 +  
 +  private boolean showProgress = !Platform.isJS();
    protected ProgressPanel progressPanel;
  
 +  
    /**
 -   * Creates a new OverviewPanel object.
 -   * 
 -   * @param alPanel
 -   *          The alignment panel which is shown in the overview panel
 +   * Creates the appropriate type of OverviewDimensions, with the desired size
     */
 -  public OverviewPanel(AlignmentPanel alPanel)
 +  private void createOverviewDimensions()
    {
 -    this.av = alPanel.av;
 -    this.ap = alPanel;
 -
 -    showHidden = Cache.getDefault(Preferences.SHOW_OV_HIDDEN_AT_START,
 -            false);
 +    boolean showAnnotation = (av.isShowAnnotation()
 +            && av.getAlignmentConservationAnnotation() != null);
      if (showHidden)
      {
 -      od = new OverviewDimensionsShowHidden(av.getRanges(),
 -              (av.isShowAnnotation()
 -                      && av.getAlignmentConservationAnnotation() != null));
 +      od = new OverviewDimensionsShowHidden(av.getRanges(), showAnnotation,
 +              dim);
      }
      else
      {
Simple merge
@@@ -91,9 -89,6 +91,8 @@@ import jalview.util.StringUtils
  import jalview.util.UrlLink;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  
 +import java.io.IOException;
 +import java.net.MalformedURLException;
  /**
   * The popup menu that is displayed on right-click on a sequence id, or in the
   * sequence alignment.
@@@ -552,36 -548,6 +551,35 @@@ public class PopupMenu extends JPopupMe
          }
        }
  
 +      if (seq.hasHMMProfile())
 +      {
 +        menuItem = new JMenuItem(MessageManager
 +                .getString("action.add_background_frequencies"));
 +        menuItem.addActionListener(new ActionListener()
 +        {
 +          @Override
 +          public void actionPerformed(ActionEvent e)
 +          {
 +            try
 +            {
 +              ResidueCount counts = CountReader.getBackgroundFrequencies(ap,
 +                      seq);
 +              if (counts != null)
 +              {
 +                seq.getHMM().setBackgroundFrequencies(counts);
 +                ap.alignFrame.buildColourMenu();
 +              }
 +            } catch (MalformedURLException e1)
 +            {
 +              e1.printStackTrace();
 +            } catch (IOException e1)
 +            {
 +              e1.printStackTrace();
 +            }
 +          }
 +        });
 +        add(menuItem);
 +      }
        menuItem = new JMenuItem(
                MessageManager.getString("action.hide_sequences"));
        menuItem.addActionListener(new ActionListener()
        {
          buildGroupURLMenu(sg, groupLinks);
        }
++      // TODO REMOVE FOR 2.12 ?
        // Add a 'show all structures' for the current selection
 -      Hashtable<String, PDBEntry> pdbe = new Hashtable<>(),
 -              reppdb = new Hashtable<>();
 +      Hashtable<String, PDBEntry> pdbe = new Hashtable<>();
 +      Hashtable<String, PDBEntry> reppdb = new Hashtable<>();
  
        SequenceI sqass = null;
        for (SequenceI sq : alignPanel.av.getSequenceSelection())
@@@ -55,11 -49,12 +54,15 @@@ import javax.swing.table.TableColumn
  import javax.swing.table.TableModel;
  import javax.swing.table.TableRowSorter;
  
 -//import edu.stanford.ejalbert.launching.IBrowserLaunching;
 +import jalview.hmmer.HmmerCommand;
 +import jalview.util.FileUtils;
  import ext.edu.ucsf.rbvi.strucviz2.StructureManager;
  import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
++import jalview.bin.ApplicationSingletonProvider;
++import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.bin.Cache;
+ import jalview.bin.Console;
+ import jalview.bin.MemorySetting;
  import jalview.ext.pymol.PymolManager;
  import jalview.gui.Help.HelpId;
  import jalview.gui.StructureViewer.ViewerType;
@@@ -88,19 -83,21 +91,18 @@@ import jalview.ws.sifts.SiftsSettings
   * @author $author$
   * @version $Revision$
   */
 -/*
 - * for merge with Jalview-JS
 - public class Preferences extends GPreferences implements ApplicationSingletonI
 - */
 -public class Preferences extends GPreferences
 +public class Preferences extends GPreferences implements ApplicationSingletonI
  {
 -  public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
 +  // suggested list delimiter character
 +  public static final String COMMA = ",";
  
 -  public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
 -
 -  public static final String DEFAULT_COLOUR = "DEFAULT_COLOUR";
 +  public static final String HMMSEARCH_SEQCOUNT = "HMMSEARCH_SEQCOUNT";
  
 -  public static final String DEFAULT_COLOUR_PROT = "DEFAULT_COLOUR_PROT";
 +  public static final String HMMINFO_GLOBAL_BACKGROUND = "HMMINFO_GLOBAL_BACKGROUND";
  
 -  public static final String DEFAULT_COLOUR_NUC = "DEFAULT_COLOUR_NUC";
 +  public static final String HMMALIGN_TRIM_TERMINI = "HMMALIGN_TRIM_TERMINI";
 +  
 +  public static final String ADD_SS_ANN = "ADD_SS_ANN";
  
    public static final String ADD_TEMPFACT_ANN = "ADD_TEMPFACT_ANN";
  
  
    public static final String CHIMERAX_PATH = "CHIMERAX_PATH";
  
 +  public static final String DBREFFETCH_USEPICR = "DBREFFETCH_USEPICR";
 +
 +  public static final String DEFAULT_COLOUR = "DEFAULT_COLOUR";
 +
 +  public static final String DEFAULT_COLOUR_NUC = "DEFAULT_COLOUR_NUC";
 +  public static final String DEFAULT_COLOUR_PROT = "DEFAULT_COLOUR_PROT";
 +
 +  public static final String ENABLE_SPLIT_FRAME = "ENABLE_SPLIT_FRAME";
 +
 +  public static final String FIGURE_AUTOIDWIDTH = "FIGURE_AUTOIDWIDTH";
 +
 +  public static final String FIGURE_FIXEDIDWIDTH = "FIGURE_FIXEDIDWIDTH";
 +
 +  public static final String FOLLOW_SELECTIONS = "FOLLOW_SELECTIONS";
 +
 +  public static final String FONT_NAME = "FONT_NAME";
 +
 +  public static final String FONT_SIZE = "FONT_SIZE";
 +
 +  public static final String FONT_STYLE = "FONT_STYLE";
 +  
 +  public static final String HMMER_PATH = "HMMER_PATH";
 +
 +  public static final String CYGWIN_PATH = "CYGWIN_PATH";
 +
 +  public static final String HMMSEARCH_DBS = "HMMSEARCH_DBS";
 +
 +  public static final String GAP_COLOUR = "GAP_COLOUR";
 +
 +  public static final String GAP_SYMBOL = "GAP_SYMBOL";
 +
 +  public static final String HIDDEN_COLOUR = "HIDDEN_COLOUR";
 +
 +  public static final String HIDE_INTRONS = "HIDE_INTRONS";
 +
 +  public static final String ID_ITALICS = "ID_ITALICS";
 +
 +  public static final String ID_ORG_HOSTURL = "ID_ORG_HOSTURL";
 +
 +  public static final String MAP_WITH_SIFTS = "MAP_WITH_SIFTS";
 +
 +  public static final String NOQUESTIONNAIRES = "NOQUESTIONNAIRES";
 +
 +  public static final String NORMALISE_CONSENSUS_LOGO = "NORMALISE_CONSENSUS_LOGO";
 +
 +  public static final String NORMALISE_LOGO = "NORMALISE_LOGO";
 +
 +  public static final String PAD_GAPS = "PAD_GAPS";
 +
 +  public static final String PDB_DOWNLOAD_FORMAT = "PDB_DOWNLOAD_FORMAT";
    public static final String PYMOL_PATH = "PYMOL_PATH";
  
 -  public static final String SORT_ANNOTATIONS = "SORT_ANNOTATIONS";
 +  public static final String QUESTIONNAIRE = "QUESTIONNAIRE";
 +
 +  public static final String RELAXEDSEQIDMATCHING = "RELAXEDSEQIDMATCHING";
 +
 +  public static final String RIGHT_ALIGN_IDS = "RIGHT_ALIGN_IDS";
 +
 +  public static final String SCALE_PROTEIN_TO_CDNA = "SCALE_PROTEIN_TO_CDNA";
 +
 +  public static final String SHOW_ANNOTATIONS = "SHOW_ANNOTATIONS";
  
    public static final String SHOW_AUTOCALC_ABOVE = "SHOW_AUTOCALC_ABOVE";
  
  
      annotations_actionPerformed(null); // update the display of the annotation
                                         // settings
 +    
 +    
      /*
       * Set Backups tab defaults
       */
      comboBox.addItem(promptEachTimeOpt);
      comboBox.addItem(lineArtOpt);
      comboBox.addItem(textOpt);
 +    
      /*
       * JalviewJS doesn't support Lineart so force it to Text
       */
      /*
       * Save Backups settings
       */
 -    Cache.applicationProperties.setProperty(BackupFiles.ENABLED,
 +    Cache.setPropertyNoSave(BackupFiles.ENABLED,
              Boolean.toString(enableBackupFiles.isSelected()));
      int preset = getComboIntStringKey(backupfilesPresetsCombo);
-     Cache.applicationProperties.setProperty(BackupFiles.NS + "_PRESET", Integer.toString(preset));
 -    Cache.applicationProperties.setProperty(BackupFiles.NS + "_PRESET",
 -            Integer.toString(preset));
++    Cache.setPropertyNoSave(BackupFiles.NS + "_PRESET", Integer.toString(preset));
  
      if (preset == BackupFilesPresetEntry.BACKUPFILESSCHEMECUSTOM)
      {
        BackupFilesPresetEntry customBFPE = getBackupfilesCurrentEntry();
        BackupFilesPresetEntry.backupfilesPresetEntriesValues.put(
                BackupFilesPresetEntry.BACKUPFILESSCHEMECUSTOM, customBFPE);
-       Cache.applicationProperties
-               .setProperty(BackupFilesPresetEntry.CUSTOMCONFIG,
 -      Cache.applicationProperties.setProperty(
 -              BackupFilesPresetEntry.CUSTOMCONFIG, customBFPE.toString());
++      Cache.setPropertyNoSave(BackupFilesPresetEntry.CUSTOMCONFIG,
 +                      customBFPE.toString());
      }
  
      BackupFilesPresetEntry savedBFPE = BackupFilesPresetEntry.backupfilesPresetEntriesValues
              .get(preset);
 -    Cache.applicationProperties.setProperty(
 +    Cache.setPropertyNoSave(
              BackupFilesPresetEntry.SAVEDCONFIG, savedBFPE.toString());
  
+     /*
+      * Save Memory Settings
+      */
 -    Cache.applicationProperties.setProperty(
++    Cache.setPropertyNoSave(
+             MemorySetting.CUSTOMISED_SETTINGS,
+             Boolean.toString(customiseMemorySetting.isSelected()));
 -    Cache.applicationProperties.setProperty(MemorySetting.MEMORY_JVMMEMPC,
++    Cache.setPropertyNoSave(MemorySetting.MEMORY_JVMMEMPC,
+             Integer.toString(jvmMemoryPercentSlider.getValue()));
 -    Cache.applicationProperties.setProperty(MemorySetting.MEMORY_JVMMEMMAX,
++    Cache.setPropertyNoSave(MemorySetting.MEMORY_JVMMEMMAX,
+             jvmMemoryMaxTextField.getText());
+     /*
+      * save and close Preferences
+      */
 -
      Cache.saveProperties();
 -    Desktop.instance.doConfigureStructurePrefs();
 +    Desktop.getInstance().doConfigureStructurePrefs();
      try
      {
        frame.setClosed(true);
              || !newProxyType.equals(previousProxyType))
      {
        // force a re-lookup of ws if proxytype is custom or has changed
--      wsPrefs.update++;
++      wsPrefs.refreshWs_actionPerformed(null);
      }
      previousProxyType = newProxyType;
    }
Simple merge
@@@ -309,7 -335,7 +335,7 @@@ public class PymolViewer extends Struct
  
      if (!binding.launchPymol())
      {
--      JvOptionPane.showMessageDialog(Desktop.desktop,
++      JvOptionPane.showMessageDialog(Desktop.getInstance(),
                MessageManager.formatMessage("label.open_viewer_failed",
                        getViewerName()),
                MessageManager.getString("label.error_loading_file"),
Simple merge
@@@ -208,6 -203,7 +208,7 @@@ public class SeqCanvas extends JPanel i
      int yPos = ypos + charHeight;
      int startX = startx;
      int endX = endx;
 -
++    
      if (av.hasHiddenColumns())
      {
        HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
  
        if (av.getWrapAlignment())
        {
-         drawWrappedPanel(gg, width, height, ranges.getStartRes());
 -        drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
++        drawWrappedPanel(gg, availWidth, availHeight, ranges.getStartRes());
        }
        else
        {
@@@ -2940,45 -2942,4 +2940,44 @@@ public class SeqPanel extends JPane
    {
      return lastSearchResults;
    }
 +  
 +  /**
 +   * scroll to the given row/column - or nearest visible location
 +   * 
 +   * @param row
 +   * @param column
 +   */
 +  public void scrollTo(int row, int column)
 +  {
 +
 +    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
 +    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
 +    ap.scrollTo(column, column, row, true, true);
 +  }
 +
 +  /**
 +   * scroll to the given row - or nearest visible location
 +   * 
 +   * @param row
 +   */
 +  public void scrollToRow(int row)
 +  {
 +
 +    row = row < 0 ? ap.av.getRanges().getStartSeq() : row;
 +    ap.scrollTo(ap.av.getRanges().getStartRes(),
 +            ap.av.getRanges().getStartRes(), row, true, true);
 +  }
 +
 +  /**
 +   * scroll to the given column - or nearest visible location
 +   * 
 +   * @param column
 +   */
 +  public void scrollToColumn(int column)
 +  {
 +
 +    column = column < 0 ? ap.av.getRanges().getStartRes() : column;
 +    ap.scrollTo(column, column, ap.av.getRanges().getStartSeq(), true,
 +            true);
 +  }
  }
Simple merge
Simple merge
index 89ff3e4,0000000..6fc14b0
mode 100644,000000..100644
--- /dev/null
@@@ -1,381 -1,0 +1,382 @@@
 +package jalview.gui;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.util.MessageManager;
 +import jalview.ws.WSDiscovererI;
 +import jalview.ws.slivkaws.SlivkaWSDiscoverer;
 +
 +import java.awt.BorderLayout;
 +import java.awt.Color;
 +import java.awt.Component;
 +import java.awt.Dimension;
 +import java.awt.Font;
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
 +import java.awt.event.MouseAdapter;
 +import java.awt.event.MouseEvent;
 +import java.awt.event.MouseListener;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.Map;
 +import java.util.NoSuchElementException;
 +import java.util.concurrent.CompletableFuture;
 +
 +import javax.swing.BorderFactory;
 +import javax.swing.Box;
 +import javax.swing.BoxLayout;
 +import javax.swing.JButton;
 +import javax.swing.JOptionPane;
 +import javax.swing.JPanel;
 +import javax.swing.JProgressBar;
 +import javax.swing.JScrollPane;
 +import javax.swing.JTable;
 +import javax.swing.SwingUtilities;
 +import javax.swing.UIManager;
 +import javax.swing.table.AbstractTableModel;
 +import javax.swing.table.DefaultTableCellRenderer;
 +
 +@SuppressWarnings("serial")
 +public class SlivkaPreferences extends JPanel
 +{
 +  {
 +    setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
 +    setPreferredSize(new Dimension(500, 450));
 +  }
 +
 +  WSDiscovererI discoverer;
 +
 +  private final ArrayList<String> urls = new ArrayList<>();
 +
 +  private final Map<String, Integer> statuses = new HashMap<>();
 +
 +  private final AbstractTableModel urlTableModel = new AbstractTableModel()
 +  {
 +    final String[] columnNames = { "Service URL", "Status" };
 +
 +    @Override
 +    public String getColumnName(int col)
 +    {
 +      return columnNames[col];
 +    }
 +
 +    @Override
 +    public Object getValueAt(int rowIndex, int columnIndex)
 +    {
 +      switch (columnIndex)
 +      {
 +      case 0:
 +        return urls.get(rowIndex);
 +      case 1:
 +        return statuses.getOrDefault(urls.get(rowIndex), WSDiscovererI.STATUS_UNKNOWN);
 +      default:
 +        throw new NoSuchElementException();
 +      }
 +    }
 +
 +    @Override
 +    public int getRowCount()
 +    {
 +      return urls.size();
 +    }
 +
 +    @Override
 +    public int getColumnCount()
 +    {
 +      return 2;
 +    }
 +  };
 +
 +  private class WSStatusCellRenderer extends DefaultTableCellRenderer
 +  {
 +    @Override
 +    public Component getTableCellRendererComponent(JTable table,
 +        Object value, boolean isSelected, boolean hasFocus, int row,
 +        int column)
 +    {
 +      setHorizontalAlignment(CENTER);
 +      super.getTableCellRendererComponent(table, "\u25CF", isSelected,
 +          hasFocus, row, column);
 +      switch ((Integer) value)
 +      {
 +      case WSDiscovererI.STATUS_NO_SERVICES:
 +        setForeground(Color.ORANGE);
 +        break;
 +      case WSDiscovererI.STATUS_OK:
 +        setForeground(Color.GREEN);
 +        break;
 +      case WSDiscovererI.STATUS_INVALID:
 +        setForeground(Color.RED);
 +        break;
 +      case WSDiscovererI.STATUS_UNKNOWN:
 +      default:
 +        setForeground(Color.LIGHT_GRAY);
 +      }
 +      return this;
 +    }
 +  }
 +
 +  private JTable urlListTable = new JTable(urlTableModel);
 +  {
 +    urlListTable.getColumnModel().getColumn(1).setMaxWidth(60);
 +    urlListTable.getColumnModel().getColumn(1)
 +        .setCellRenderer(new WSStatusCellRenderer());
 +  }
 +
 +  // URL control panel buttons
 +  JButton newWsUrl = new JButton(
 +      MessageManager.getString("label.new_service_url"));
 +
 +  JButton editWsUrl = new JButton(
 +      MessageManager.getString("label.edit_service_url"));
 +
 +  JButton deleteWsUrl = new JButton(
 +      MessageManager.getString("label.delete_service_url"));
 +
 +  JButton moveUrlUp = new JButton(
 +      MessageManager.getString("action.move_up"));
 +
 +  JButton moveUrlDown = new JButton(
 +      MessageManager.getString("action.move_down"));
 +
 +  private String showEditUrlDialog(String oldUrl)
 +  {
 +    String input = (String) JvOptionPane
 +        .showInternalInputDialog(
 +            this,
 +            MessageManager.getString("label.url:"),
 +            UIManager.getString("OptionPane.inputDialogTitle", MessageManager.getLocale()),
 +            JOptionPane.QUESTION_MESSAGE,
 +            null,
 +            null,
 +            oldUrl);
 +    if (input == null)
 +    {
 +      return null;
 +    }
 +    try
 +    {
 +      new URL(input);
 +    } catch (MalformedURLException ex)
 +    {
 +      JvOptionPane.showInternalMessageDialog(this,
 +          MessageManager.getString("label.invalid_url"),
 +          UIManager.getString("OptionPane.messageDialogTitle",
 +              MessageManager.getLocale()),
 +          JOptionPane.WARNING_MESSAGE);
 +      return null;
 +    }
 +    return input;
 +  }
 +
 +  // Button Action Listeners
 +  private ActionListener newUrlAction = (ActionEvent e) -> {
 +    final String input = showEditUrlDialog("");
 +    if (input != null)
 +    {
 +      urls.add(input);
 +      reloadStatusForUrl(input);
 +      urlTableModel.fireTableRowsInserted(urls.size(), urls.size());
 +      discoverer.setServiceUrls(urls);
 +    }
 +  };
 +
 +  private ActionListener editUrlAction = (ActionEvent e) -> {
 +    final int i = urlListTable.getSelectedRow();
 +    if (i >= 0)
 +    {
 +      final String input = showEditUrlDialog(urls.get(i));
 +      if (input != null)
 +      {
 +        urls.set(i, input);
 +        statuses.remove(input);
 +        reloadStatusForUrl(input);
 +        urlTableModel.fireTableRowsUpdated(i, i);
 +        discoverer.setServiceUrls(urls);
 +      }
 +    }
 +  };
 +
 +  private ActionListener deleteUrlAction = (ActionEvent e) -> {
 +    final int i = urlListTable.getSelectedRow();
 +    if (i >= 0)
 +    {
 +      urls.remove(i);
 +      statuses.remove(i);
 +      urlTableModel.fireTableRowsDeleted(i, i);
 +      discoverer.setServiceUrls(urls);
 +    }
 +  };
 +
 +  private ActionListener moveUrlUpAction = (ActionEvent e) -> {
 +    final int i = urlListTable.getSelectedRow();
 +    if (i > 0)
 +    {
 +      moveTableRow(i, i - 1);
 +      discoverer.setServiceUrls(urls);
 +    }
 +  };
 +
 +  private ActionListener moveUrlDownAction = (ActionEvent e) -> {
 +    final int i = urlListTable.getSelectedRow();
 +    if (i >= 0 && i < urls.size() - 1)
 +    {
 +      moveTableRow(i, i + 1);
 +      discoverer.setServiceUrls(urls);
 +    }
 +  };
 +
 +  private MouseListener tableClickListener = new MouseAdapter()
 +  {
 +    final ActionEvent actionEvent = new ActionEvent(urlListTable,
 +        ActionEvent.ACTION_PERFORMED, "edit");
 +
 +    @Override
 +    public void mouseClicked(MouseEvent e)
 +    {
 +      if (e.getClickCount() > 1)
 +      {
 +        editUrlAction.actionPerformed(actionEvent);
 +      }
 +    }
 +  };
 +
 +  // Setting up URL list Pane
 +  {
 +    Font font = new Font("Verdana", Font.PLAIN, 10);
 +    JPanel urlPaneContainer = new JPanel(new BorderLayout(5, 5));
 +    urlPaneContainer.setBorder(BorderFactory.createCompoundBorder(
 +        BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),
 +            "Slivka Web Services"),
 +        BorderFactory.createEmptyBorder(10, 5, 5, 5)));
 +
 +    newWsUrl.setFont(font);
 +    editWsUrl.setFont(font);
 +    deleteWsUrl.setFont(font);
 +    moveUrlUp.setFont(font);
 +    moveUrlDown.setFont(font);
 +    JPanel editContainer = new JPanel();
 +    editContainer.add(newWsUrl);
 +    editContainer.add(editWsUrl);
 +    editContainer.add(deleteWsUrl);
 +    urlPaneContainer.add(editContainer, BorderLayout.PAGE_END);
 +
 +    JPanel moveContainer = new JPanel();
 +    moveContainer
 +        .setLayout(new BoxLayout(moveContainer, BoxLayout.PAGE_AXIS));
 +    moveContainer.add(moveUrlUp);
 +    moveContainer.add(Box.createRigidArea(new Dimension(0, 5)));
 +    moveContainer.add(moveUrlDown);
 +    urlPaneContainer.add(moveContainer, BorderLayout.LINE_START);
 +
 +    urlPaneContainer.add(new JScrollPane(urlListTable),
 +        BorderLayout.CENTER);
 +    this.add(urlPaneContainer);
 +
 +    // Connecting action listeners
 +    urlListTable.addMouseListener(tableClickListener);
 +    newWsUrl.addActionListener(newUrlAction);
 +    editWsUrl.addActionListener(editUrlAction);
 +    deleteWsUrl.addActionListener(deleteUrlAction);
 +    moveUrlUp.addActionListener(moveUrlUpAction);
 +    moveUrlDown.addActionListener(moveUrlDownAction);
 +  }
 +
 +  private void moveTableRow(int fromIndex, int toIndex)
 +  {
 +    String url = urls.get(fromIndex);
 +    int status = statuses.get(fromIndex);
 +    urls.set(fromIndex, urls.get(toIndex));
 +    urls.set(toIndex, url);
 +    if (urlListTable.getSelectedRow() == fromIndex)
 +    {
 +      urlListTable.setRowSelectionInterval(toIndex, toIndex);
 +    }
 +    int firstRow = Math.min(toIndex, fromIndex);
 +    int lastRow = Math.max(fromIndex, toIndex);
 +    urlTableModel.fireTableRowsUpdated(firstRow, lastRow);
 +  }
 +
 +  // Discoverer reloading buttons
 +  JButton refreshServices = new JButton(
 +      MessageManager.getString("action.refresh_services"));
 +
 +  JButton resetServices = new JButton(
 +      MessageManager.getString("action.reset_services"));
 +
 +  JProgressBar progressBar = new JProgressBar();
 +
 +  // Discoverer buttons action listeners
 +  private ActionListener refreshServicesAction = (ActionEvent e) -> {
 +    progressBar.setVisible(true);
-     Cache.log.info("Requesting service reload");
++    Console.info("Requesting service reload");
 +    discoverer.startDiscoverer().handle((_discoverer, exception) -> {
 +      if (exception == null)
 +      {
-         Cache.log.info("Reloading done");
++        Console.info("Reloading done");
 +      }
 +      else
 +      {
-         Cache.log.error("Reloading failed", exception);
++        Console.error("Reloading failed", exception);
 +      }
 +      SwingUtilities.invokeLater(() -> progressBar.setVisible(false));
 +      return null;
 +    });
 +  };
 +
 +  private ActionListener resetServicesAction = (ActionEvent e) -> {
 +    discoverer.setServiceUrls(null);
 +    urls.clear();
 +    statuses.clear();
 +    urls.addAll(discoverer.getServiceUrls());
 +    for (String url : urls)
 +    {
 +      reloadStatusForUrl(url);
 +    }
 +    urlTableModel.fireTableDataChanged();
 +  };
 +
 +  {
 +    Font font = new Font("Verdana", Font.PLAIN, 11);
 +    refreshServices.setFont(font);
 +    resetServices.setFont(font);
 +    JPanel container = new JPanel();
 +    container.add(refreshServices);
 +    container.add(resetServices);
 +    this.add(container);
 +
 +    // Connecting action listeners
 +    refreshServices.addActionListener(refreshServicesAction);
 +    resetServices.addActionListener(resetServicesAction);
 +  }
 +
 +  {
 +    progressBar.setVisible(false);
 +    progressBar.setIndeterminate(true);
 +    add(progressBar);
 +  }
 +
 +  SlivkaPreferences()
 +  {
 +    // Initial URLs loading
 +    discoverer = SlivkaWSDiscoverer.getInstance();
 +    urls.addAll(discoverer.getServiceUrls());
 +    for (String url : urls)
 +    {
 +      reloadStatusForUrl(url);
 +    }
 +  }
 +
 +  private void reloadStatusForUrl(String url)
 +  {
 +    CompletableFuture.supplyAsync(() -> discoverer.getServerStatusFor(url))
 +        .thenAccept((status) -> {
 +          statuses.put(url, status);
 +          int row = urls.indexOf(url);
 +          if (row >= 0)
 +            urlTableModel.fireTableCellUpdated(row, 1);
 +        });
 +  }
 +}
@@@ -42,26 -41,19 +41,29 @@@ import javax.swing.event.HyperlinkListe
  
  import jalview.util.ChannelProperties;
  import jalview.util.Platform;
++import javajs.async.SwingJSUtils.StateHelper;
++import javajs.async.SwingJSUtils.StateMachine;
++
  /**
   * DOCUMENT ME!
   * 
   * @author $author$
   * @version $Revision$
   */
 +@SuppressWarnings("serial")
  public class SplashScreen extends JPanel
 -        implements Runnable, HyperlinkListener
 +        implements HyperlinkListener, StateMachine
  {
 +  
 +  private static final int STATE_INIT = 0;
 +
 +  private static final int STATE_LOOP = 1;
 +
 +  private static final int STATE_DONE = 2;
    private static final int SHOW_FOR_SECS = 5;
  
 -  private static final int FONT_SIZE = 11;
 +  private static final int FONT_SIZE = (Platform.isJS() ? 14 : 11);
  
    private boolean visible = true;
  
  
    private static Color fg = Color.BLACK;
  
+   private static Font font = new Font("SansSerif", Font.PLAIN, FONT_SIZE);
++  private JPanel imgPanel = new JPanel(new BorderLayout());
++
    /*
     * as JTextPane in Java, JLabel in javascript
     */
  
    private JInternalFrame iframe;
  
--  private Image image;
++  private Image image, logo;
  
    private boolean transientDialog = false;
  
    private long oldTextLength = -1;
  
++  private StateHelper helper;
+   public static int logoSize = 32;
    /*
     * allow click in the initial splash screen to dismiss it
     * immediately (not if opened from About menu)
    }
  
    /**
 -   * ping the jalview version page then create and display the jalview
 -   * splashscreen window.
 +   * Both Java and JavaScript have to wait for images, but this method will
 +   * accomplish nothing for JavaScript. We have already taken care of image
 +   * loading with our state loop in JavaScript.
 +   * 
     */
 -  void initSplashScreenWindow()
 +  private void waitForImages()
    {
 -    addMouseListener(closer);
 -
 -    try
 +    if (Platform.isJS())
 +      return;
 +    MediaTracker mt = new MediaTracker(this);
 +    mt.addImage(image, 0);
 +    mt.addImage(logo, 1);
 +    do
      {
 -      if (!Platform.isJS())
 +      try
 +      {
 +        mt.waitForAll();
 +      } catch (InterruptedException x)
        {
 -        image = ChannelProperties.getImage("banner");
 -        Image logo = ChannelProperties.getImage("logo.48");
 -        MediaTracker mt = new MediaTracker(this);
 -        if (image != null)
 -        {
 -          mt.addImage(image, 0);
 -        }
 -        if (logo != null)
 -        {
 -          mt.addImage(logo, 1);
 -        }
 -        do
 -        {
 -          try
 -          {
 -            mt.waitForAll();
 -          } catch (InterruptedException x)
 -          {
 -          }
 -          if (mt.isErrorAny())
 -          {
 -            System.err.println("Error when loading images!");
 -          }
 -        } while (!mt.checkAll());
 -        Desktop.instance.setIconImages(ChannelProperties.getIconList());
        }
 -    } catch (Exception ex)
 +      if (mt.isErrorAny())
 +      {
 +        System.err.println("Error when loading images!");
 +        break;
 +      }
 +    } while (!mt.checkAll());
 +    if (logo != null)
      {
 +      Desktop.getInstance().setIconImage(logo);
      }
      this.setBackground(bg);
      this.setForeground(fg);
      this.setFont(font);
       * @j2sIgnore
       */
      {
 -      ((JTextPane) splashText).setEditable(false);
 -      splashText.setBackground(bg);
 -      splashText.setForeground(fg);
 -      splashText.setFont(font);
 -
 -      SplashImage splashimg = new SplashImage(image);
 -      iconimg.add(splashimg, BorderLayout.LINE_START);
 -      iconimg.setBackground(bg);
 -      add(iconimg, BorderLayout.NORTH);
 +      JTextPane jtp = new JTextPane();
 +      jtp.setEditable(false);
 +      jtp.setBackground(bg);
 +      jtp.setForeground(fg);
 +      jtp.setFont(font);
 +      jtp.setContentType("text/html");
 +      jtp.setText("<html>" + newtext + "</html>");
 +      jtp.addHyperlinkListener(this);
 +      splashText = jtp;
      }
 -    add(splashText, BorderLayout.CENTER);
      splashText.addMouseListener(closer);
 -    Desktop.desktop.add(iframe);
 -    refreshText();
 +
 +    splashText.setVisible(true);
 +    splashText.setSize(new Dimension(750,
 +            375 + logoSize + (Platform.isJS() ? 40 : 0)));
 +    splashText.setBackground(bg);
 +    splashText.setForeground(fg);
 +    splashText.setFont(font);
 +    add(splashText, BorderLayout.CENTER);
 +    revalidate();
 +    int width = Math.max(splashText.getWidth(), iconimg.getWidth());
 +    int height = splashText.getHeight() + iconimg.getHeight();
 +    iframe.setBounds((iframe.getParent().getWidth() - width) / 2,
-             (iframe.getParent().getHeight() - height) / 2, 750,
++            (iframe.getParent().getHeight() - height) / 2,
 +            width,height);
 +    iframe.validate();
 +    iframe.setVisible(true);
 +    return true;
    }
  
 -  /**
 -   * update text in author text panel reflecting current version information
 -   */
 -  protected boolean refreshText()
 +  protected void closeSplash()
    {
 -    String newtext = Desktop.instance.getAboutMessage();
 -    // System.err.println("Text found: \n"+newtext+"\nEnd of newtext.");
 -    if (oldTextLength != newtext.length())
 +    try
 +    {
 +
 +      iframe.setClosed(true);
 +    } catch (Exception ex)
      {
 -      iframe.setVisible(false);
 -      oldTextLength = newtext.length();
 -      if (Platform.isJS()) // BH 2019
 -      {
 -        /*
 -         * SwingJS doesn't have HTMLEditorKit, required for a JTextPane
 -         * to display formatted html, so we use a simple alternative
 -         */
 -        String text = "<html><br><img src=\""
 -                + ChannelProperties.getImageURL("banner") + "\"/>" + newtext
 -                + "<br></html>";
 -        JLabel ta = new JLabel(text);
 -        ta.setOpaque(true);
 -        ta.setBackground(Color.white);
 -        splashText = ta;
 -      }
 -      else
 -      /**
 -       * Java only
 -       *
 -       * @j2sIgnore
 -       */
 -      {
 -        JTextPane jtp = new JTextPane();
 -        jtp.setEditable(false);
 -        jtp.setBackground(bg);
 -        jtp.setForeground(fg);
 -        jtp.setFont(font);
 -        jtp.setContentType("text/html");
 -        jtp.setText("<html>" + newtext + "</html>");
 -        jtp.addHyperlinkListener(this);
 -        splashText = jtp;
 -      }
 -      splashText.addMouseListener(closer);
 -
 -      splashText.setVisible(true);
 -      splashText.setSize(new Dimension(750,
 -              425 + logoSize + (Platform.isJS() ? 40 : 0)));
 -      splashText.setBackground(bg);
 -      splashText.setForeground(fg);
 -      splashText.setFont(font);
 -      add(splashText, BorderLayout.CENTER);
 -      revalidate();
 -      int width = Math.max(splashText.getWidth(), iconimg.getWidth());
 -      int height = splashText.getHeight() + iconimg.getHeight();
 -      iframe.setBounds(
 -              Math.max(0, (Desktop.instance.getWidth() - width) / 2),
 -              Math.max(0, (Desktop.instance.getHeight() - height) / 2),
 -              width, height);
 -      iframe.validate();
 -      iframe.setVisible(true);
 -      return true;
      }
    }
  
    /**
 -   * Create splash screen, display it and clear it off again.
 +   * A simple state machine with just three states: init, loop, and done. Ideal
 +   * for a simple while/sleep loop that works in Java and JavaScript
 +   * identically.
 +   * 
     */
    @Override
 -  public void run()
 +  public boolean stateLoop()
    {
 -    initSplashScreenWindow();
 -
 -    long startTime = System.currentTimeMillis() / 1000;
 -
 -    while (visible)
 +    while (true)
      {
 -      iframe.repaint();
 -      try
 +      switch (helper.getState())
        {
 -        Thread.sleep(500);
 -      } catch (Exception ex)
 -      {
 -      }
 -
 -      if (transientDialog && ((System.currentTimeMillis() / 1000)
 -              - startTime) > SHOW_FOR_SECS)
 -      {
 -        visible = false;
 -      }
 -
 -      if (visible && refreshText())
 -      {
 -        iframe.repaint();
 -      }
 -      if (!transientDialog)
 -      {
 -        return;
 +      case STATE_INIT:
 +        initSplashScreenWindow();
 +        helper.setState(STATE_LOOP);
 +        continue;
 +      case STATE_LOOP:
 +        if (!isVisible())
 +        {
 +          helper.setState(STATE_DONE);
 +          continue;
 +        }
 +        if (refreshText())
 +        {
 +          iframe.repaint();
 +        }
-         if (isStartup)
++        if (transientDialog)
 +          helper.delayedState(SHOW_FOR_SECS * 1000, STATE_DONE);
 +        return true;
 +      default:
 +      case STATE_DONE:
 +        setVisible(false);
 +        closeSplash();
 +        Desktop.getInstance().startDialogQueue();
 +        return true;
        }
      }
 -    closeSplash();
 -    Desktop.instance.startDialogQueue();
 -  }
 -
 -  /**
 -   * DOCUMENT ME!
 -   */
 -  public void closeSplash()
 -  {
 -    try
 -    {
 -
 -      iframe.setClosed(true);
 -    } catch (Exception ex)
 -    {
 -    }
    }
  
 -  public class SplashImage extends JPanel
 +  private class SplashImage extends JPanel
    {
      Image image;
  
   */
  package jalview.gui;
  
 +import jalview.api.AlignViewportI;
 +import jalview.api.AlignViewControllerGuiI;
 +import jalview.api.FeatureSettingsControllerI;
 +import jalview.api.SplitContainerI;
 +import jalview.controller.FeatureSettingsControllerGuiI;
 +import jalview.datamodel.AlignmentI;
 +import jalview.jbgui.GAlignFrame;
 +import jalview.jbgui.GSplitFrame;
 +import jalview.structure.StructureSelectionManager;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.viewmodel.AlignmentViewport;
  import java.awt.BorderLayout;
  import java.awt.Component;
  import java.awt.Dimension;
@@@ -62,6 -49,18 +61,7 @@@ import javax.swing.event.ChangeListener
  import javax.swing.event.InternalFrameAdapter;
  import javax.swing.event.InternalFrameEvent;
  
 -import jalview.api.AlignViewControllerGuiI;
 -import jalview.api.FeatureSettingsControllerI;
 -import jalview.api.SplitContainerI;
 -import jalview.controller.FeatureSettingsControllerGuiI;
 -import jalview.datamodel.AlignmentI;
 -import jalview.jbgui.GAlignFrame;
 -import jalview.jbgui.GSplitFrame;
 -import jalview.structure.StructureSelectionManager;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
 -import jalview.viewmodel.AlignmentViewport;
  /**
   * An internal frame on the desktop that hosts a horizontally split view of
   * linked DNA and Protein alignments. Additional views can be created in linked
@@@ -78,8 -79,13 +80,12 @@@ import jalview.ws.sifts.SiftsSettings
  public class StructureChooser extends GStructureChooser
          implements IProgressIndicator
  {
--  private static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
++  protected static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
  
+   /**
+    * warn user if need to fetch more than this many uniprot records at once
+    */
+   private static final int THRESHOLD_WARN_UNIPROT_FETCH_NEEDED = 20;
 -
    private SequenceI selectedSequence;
  
    private SequenceI[] selectedSequences;
  
    List<SequenceI> seqsWithoutSourceDBRef = null;
  
--  private static StructureViewer lastTargetedView = null;
++  protected static StructureViewer lastTargetedView = null;
  
    public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
            AlignmentPanel ap)
          populateSeqsWithoutSourceDBRef();
          // redo initial discovery - this time with 3d beacons
          // Executors.
-         previousWantedFields=null;
-         lastSelected=(FilterOption) cmb_filterOption.getSelectedItem();
+         previousWantedFields = null;
+         lastSelected = (FilterOption) cmb_filterOption.getSelectedItem();
          cmb_filterOption.setSelectedItem(null);
 -        cachedPDBExists = false; // reset to initial
 +        cachedPDBExists=false; // reset to initial
          initialStructureDiscovery();
          if (!isStructuresDiscovered())
          {
                    { new jalview.ws.dbsources.Uniprot() }, null, false);
            dbRefFetcher.addListener(afterDbRefFetch);
            // ideally this would also gracefully run with callbacks
 -
            dbRefFetcher.fetchDBRefs(true);
-         } else {
+         }
+         else
+         {
            // call finished action directly
            afterDbRefFetch.finished();
          }
          }
        };
      };
-     if (ignoreGui)
+     int threshold = Cache.getDefault("UNIPROT_AUTOFETCH_THRESHOLD",
+             THRESHOLD_WARN_UNIPROT_FETCH_NEEDED);
+     Console.debug("Using Uniprot fetch threshold of " + threshold);
+     if (ignoreGui || seqsWithoutSourceDBRef.size() < threshold)
      {
 -      Executors.defaultThreadFactory().newThread(discoverCanonicalDBrefs)
 -              .start();
 +      Executors.defaultThreadFactory().newThread(discoverCanonicalDBrefs).start();
        return;
      }
      // need cancel and no to result in the discoverPDB action - mocked is
      // 'cancel' TODO: mock should be OK
-     JvOptionPane.newOptionDialog(this)
 -
+     StructureChooser thisSC = this;
+     JvOptionPane.newOptionDialog(thisSC.getFrame())
              .setResponseHandler(JvOptionPane.OK_OPTION,
                      discoverCanonicalDBrefs)
              .setResponseHandler(JvOptionPane.CANCEL_OPTION, revertview)
      FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
              .getSelectedItem());
      String currentView = selectedFilterOpt.getView();
-      
-     if (currentView == VIEWS_FILTER && data instanceof ThreeDBStructureChooserQuerySource)
 -
+     if (currentView == VIEWS_FILTER
+             && data instanceof ThreeDBStructureChooserQuerySource)
      {
-       
-       TDB_FTSData row=((ThreeDBStructureChooserQuerySource)data).getFTSDataFor(getResultTable(), selectedRow, discoveredStructuresSet);
-       String pageUrl = row.getModelViewUrl(); 
 -
+       TDB_FTSData row = ((ThreeDBStructureChooserQuerySource) data)
+               .getFTSDataFor(getResultTable(), selectedRow,
+                       discoveredStructuresSet);
+       String pageUrl = row.getModelViewUrl();
        JPopupMenu popup = new JPopupMenu("3D Beacons");
        JMenuItem viewUrl = new JMenuItem("View model web page");
-       viewUrl.addActionListener(
-               new ActionListener() {
-                 @Override
-                 public void actionPerformed(ActionEvent e)
-                 {
-                   Desktop.showUrl(pageUrl);
-                 }
-               }
-               );
+       viewUrl.addActionListener(new ActionListener()
+       {
+         @Override
+         public void actionPerformed(ActionEvent e)
+         {
+           Desktop.showUrl(pageUrl);
+         }
+       });
        popup.add(viewUrl);
-       SwingUtilities.invokeLater(new Runnable()  {
-         public void run() { popup.show(getResultTable(), x, y); }
+       SwingUtilities.invokeLater(new Runnable()
+       {
+         @Override
+         public void run()
+         {
+           popup.show(getResultTable(), x, y);
+         }
        });
        return true;
      }
    {
      validateSelections();
    }
--
 -  private FilterOption lastSelected = null;
 -
 +  private FilterOption lastSelected=null;
    /**
     * Handles the state change event for the 'filter' combo-box and 'invert'
     * check-box
    {
  
      final StructureSelectionManager ssm = ap.getStructureSelectionManager();
--
++    final StructureViewer theViewer = getTargetedStructureViewer(ssm);
++    boolean superimpose = chk_superpose.isSelected(); 
      final int preferredHeight = pnl_filter.getHeight();
  
      Runnable viewStruc = new Runnable()
  
            SequenceI[] selectedSeqs = selectedSeqsToView
                    .toArray(new SequenceI[selectedSeqsToView.size()]);
--          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
--                  selectedSeqs);
++          sViewer = StructureViewer.launchStructureViewer(ap, pdbEntriesToView,
++                  selectedSeqs, superimpose, theViewer, progressBar);
          }
          else if (currentView == VIEWS_LOCAL_PDB)
          {
            }
            SequenceI[] selectedSeqs = selectedSeqsToView
                    .toArray(new SequenceI[selectedSeqsToView.size()]);
--          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
--                  selectedSeqs);
++          sViewer = StructureViewer.launchStructureViewer(ap, pdbEntriesToView,
++                  selectedSeqs, superimpose, theViewer, progressBar);
          }
          else if (currentView == VIEWS_ENTER_ID)
          {
            }
  
            PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
--          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
--                  new SequenceI[]
--                  { selectedSequence });
++          sViewer = StructureViewer.launchStructureViewer(ap, pdbEntriesToView,
++                   new SequenceI[]
++                  { selectedSequence }, superimpose, theViewer,
++                  progressBar);
          }
          else if (currentView == VIEWS_FROM_FILE)
          {
            {
              selectedSequence = userSelectedSeq;
            }
--          PDBEntry fileEntry = new AssociatePdbFileWithSeq()
--                  .associatePdbWithSeq(selectedPdbFileName,
--                          DataSourceType.FILE, selectedSequence, true,
-                           Desktop.getInstance());
-           sViewer = launchStructureViewer(ssm, new PDBEntry[] { fileEntry },
-                   ap, new SequenceI[]
-                   { selectedSequence });
 -                          Desktop.instance);
 -
 -          sViewer = launchStructureViewer(ssm, new PDBEntry[] { fileEntry },
 -                  ap, new SequenceI[]
 -                  { selectedSequence });
++          PDBEntry fileEntry =  AssociatePdbFileWithSeq.associatePdbWithSeq(selectedPdbFileName,
++                          DataSourceType.FILE, selectedSequence, true);
++
++          sViewer = StructureViewer.launchStructureViewer(ap,  new PDBEntry[] { fileEntry },
++                new SequenceI[] { selectedSequence }, superimpose, theViewer,
++                  progressBar);
          }
          SwingUtilities.invokeLater(new Runnable()
          {
    }
  
    /**
--   * Adds PDB structures to a new or existing structure viewer
--   * 
--   * @param ssm
--   * @param pdbEntriesToView
--   * @param alignPanel
--   * @param sequences
--   * @return
--   */
--  private StructureViewer launchStructureViewer(
--          StructureSelectionManager ssm, final PDBEntry[] pdbEntriesToView,
--          final AlignmentPanel alignPanel, SequenceI[] sequences)
--  {
--    long progressId = sequences.hashCode();
--    setProgressBar(MessageManager
--            .getString("status.launching_3d_structure_viewer"), progressId);
--    final StructureViewer theViewer = getTargetedStructureViewer(ssm);
--    boolean superimpose = chk_superpose.isSelected();
--    theViewer.setSuperpose(superimpose);
--
--    /*
--     * remember user's choice of superimpose or not
--     */
--    Cache.setProperty(AUTOSUPERIMPOSE,
--            Boolean.valueOf(superimpose).toString());
--
--    setProgressBar(null, progressId);
--    if (SiftsSettings.isMapWithSifts())
--    {
--      List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
--      int p = 0;
--      // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
--      // real PDB ID. For moment, we can also safely do this if there is already
--      // a known mapping between the PDBEntry and the sequence.
--      for (SequenceI seq : sequences)
--      {
--        PDBEntry pdbe = pdbEntriesToView[p++];
--        if (pdbe != null && pdbe.getFile() != null)
--        {
--          StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
--          if (smm != null && smm.length > 0)
--          {
--            for (StructureMapping sm : smm)
--            {
--              if (sm.getSequence() == seq)
--              {
--                continue;
--              }
--            }
--          }
--        }
--        if (seq.getPrimaryDBRefs().isEmpty())
--        {
--          seqsWithoutSourceDBRef.add(seq);
--          continue;
--        }
--      }
--      if (!seqsWithoutSourceDBRef.isEmpty())
--      {
--        int y = seqsWithoutSourceDBRef.size();
--        setProgressBar(MessageManager.formatMessage(
--                "status.fetching_dbrefs_for_sequences_without_valid_refs",
--                y), progressId);
--        SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
--                .toArray(new SequenceI[y]);
--        DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
--        dbRefFetcher.fetchDBRefs(true);
--
--        setProgressBar("Fetch complete.", progressId); // todo i18n
--      }
--    }
--    if (pdbEntriesToView.length > 1)
--    {
--      setProgressBar(
--              MessageManager.getString(
--                      "status.fetching_3d_structures_for_selected_entries"),
--              progressId);
--      theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
--    }
--    else
--    {
--      setProgressBar(MessageManager.formatMessage(
--              "status.fetching_3d_structures_for",
--              pdbEntriesToView[0].getId()), progressId);
--      theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
--    }
--    setProgressBar(null, progressId);
--    // remember the last viewer we used...
--    lastTargetedView = theViewer;
--    return theViewer;
--  }
--
--  /**
     * Populates the combo-box used in associating manually fetched structures to
     * a unique sequence when more than one sequence selection is made.
     */
@@@ -26,13 -26,14 +26,21 @@@ import java.util.LinkedHashMap
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
 -
  import jalview.api.structures.JalviewStructureDisplayI;
  import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.datamodel.PDBEntry;
  import jalview.datamodel.SequenceI;
  import jalview.datamodel.StructureViewerModel;
++import jalview.structure.StructureMapping;
  import jalview.structure.StructureSelectionManager;
++import jalview.util.MessageManager;
++import jalview.util.Platform;
++import jalview.ws.DBRefFetcher;
++import jalview.ws.seqfetcher.DbSourceProxy;
++import jalview.ws.sifts.SiftsSettings;
++
 +
  
  /**
   * A proxy for handling structure viewers, that orchestrates adding selected
@@@ -422,4 -410,4 +431,119 @@@ public class StructureViewe
      superposeAdded = alignAddedStructures;
    }
  
++  /**
++   * Launch a minimal implementation of a StructureViewer.
++   * 
++   * @param alignPanel
++   * @param pdb
++   * @param seqs
++   * @return
++   */
++  public static StructureViewer launchStructureViewer(
++          AlignmentPanel alignPanel, PDBEntry pdb, SequenceI[] seqs)
++  {
++    return launchStructureViewer(alignPanel, new PDBEntry[] { pdb }, seqs,
++            false, null, null);
++  }
++
++  /**
++   * Adds PDB structures to a new or existing structure viewer
++   * 
++   * @param ssm
++   * @param pdbEntriesToView
++   * @param alignPanel
++   * @param sequences
++   * @return
++   */
++  protected static StructureViewer launchStructureViewer(
++          final AlignmentPanel ap, final PDBEntry[] pdbEntriesToView,
++          SequenceI[] sequences, boolean superimpose,
++          StructureViewer theViewer, IProgressIndicator pb)
++  {
++    final StructureSelectionManager ssm = ap.getStructureSelectionManager();
++    if (theViewer == null)
++      theViewer = new StructureViewer(ssm);
++    long progressId = sequences.hashCode();
++    if (pb != null)
++      pb.setProgressBar(MessageManager.getString(
++              "status.launching_3d_structure_viewer"), progressId);
++    theViewer.setSuperpose(superimpose);
++  
++    /*
++     * remember user's choice of superimpose or not
++     */
++    Cache.setProperty(StructureChooser.AUTOSUPERIMPOSE,
++            Boolean.valueOf(superimpose).toString());
++  
++    if (pb != null)
++      pb.setProgressBar(null, progressId);
++    if (SiftsSettings.isMapWithSifts())
++    {
++      List<SequenceI> seqsWithoutSourceDBRef = new ArrayList<>();
++      int p = 0;
++      // TODO: skip PDBEntry:Sequence pairs where PDBEntry doesn't look like a
++      // real PDB ID. For moment, we can also safely do this if there is already
++      // a known mapping between the PDBEntry and the sequence.
++      for (SequenceI seq : sequences)
++      {
++        PDBEntry pdbe = pdbEntriesToView[p++];
++        if (pdbe != null && pdbe.getFile() != null)
++        {
++          StructureMapping[] smm = ssm.getMapping(pdbe.getFile());
++          if (smm != null && smm.length > 0)
++          {
++            for (StructureMapping sm : smm)
++            {
++              if (sm.getSequence() == seq)
++              {
++                continue;
++              }
++            }
++          }
++        }
++        if (seq.getPrimaryDBRefs().isEmpty())
++        {
++          seqsWithoutSourceDBRef.add(seq);
++          continue;
++        }
++      }
++      if (!seqsWithoutSourceDBRef.isEmpty())
++      {
++        int y = seqsWithoutSourceDBRef.size();
++        if (pb != null)
++          pb.setProgressBar(MessageManager.formatMessage(
++                  "status.fetching_dbrefs_for_sequences_without_valid_refs",
++                  y), progressId);
++        SequenceI[] seqWithoutSrcDBRef = seqsWithoutSourceDBRef
++                .toArray(new SequenceI[y]);
++        DBRefFetcher dbRefFetcher = new DBRefFetcher(seqWithoutSrcDBRef);
++        dbRefFetcher.fetchDBRefs(true);
++  
++        if (pb != null)
++          pb.setProgressBar("Fetch complete.", progressId); // todo i18n
++      }
++    }
++    if (pdbEntriesToView.length > 1)
++    {
++      if (pb != null)
++        pb.setProgressBar(MessageManager.getString(
++                "status.fetching_3d_structures_for_selected_entries"),
++                progressId);
++      theViewer.viewStructures(pdbEntriesToView, sequences, ap);
++    }
++    else
++    {
++      if (pb != null)
++        pb.setProgressBar(MessageManager.formatMessage(
++                "status.fetching_3d_structures_for",
++                pdbEntriesToView[0].getId()), progressId);
++      theViewer.viewStructures(pdbEntriesToView[0], sequences, ap);
++    }
++    if (pb != null)
++      pb.setProgressBar(null, progressId);
++    // remember the last viewer we used...
++    StructureChooser.lastTargetedView = theViewer;
++    return theViewer;
++  }
++
  }
Simple merge
Simple merge
   */
  package jalview.gui;
  
 -import jalview.bin.Cache;
 -import jalview.bin.Console;
 -import jalview.util.MessageManager;
  import java.io.BufferedReader;
  import java.io.InputStreamReader;
  import java.net.URL;
  
+ import javax.swing.JOptionPane;
++import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.util.MessageManager;
 +
  public class UserQuestionnaireCheck implements Runnable
  {
    /**
        {
          String qurl = url + (url.indexOf('?') > -1 ? "&" : "?") + "qid="
                  + qid + "&rid=" + rid;
-         jalview.bin.Cache.log
-                 .info("Prompting user for questionnaire at " + qurl);
+         Console.info("Prompting user for questionnaire at " + qurl);
 -        int reply = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
 +        int reply = JvOptionPane.showInternalConfirmDialog(Desktop.getDesktopPane(),
                  MessageManager.getString("label.jalview_new_questionnaire"),
                  MessageManager.getString("label.jalview_user_survey"),
                  JvOptionPane.YES_NO_OPTION, JvOptionPane.QUESTION_MESSAGE);
@@@ -173,7 -174,7 +174,7 @@@ public class VamsasApplication implemen
            }
          } catch (InvalidSessionDocumentException e)
          {
-           JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
 -          JvOptionPane.showInternalMessageDialog(Desktop.desktop,
++          JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
  
                    MessageManager.getString(
                            "label.vamsas_doc_couldnt_be_opened_as_new_session"),
                {
                  if (client.promptUser)
                  {
-                   Cache.log.debug(
+                   Console.debug(
                            "Asking user if the vamsas session should be stored.");
--                  int reply = JvOptionPane.showInternalConfirmDialog(
-                           Desktop.getDesktopPane(),
 -                          Desktop.desktop,
++                  int reply = JvOptionPane.showInternalConfirmDialog(Desktop.getDesktopPane(),
                            "The current VAMSAS session has unsaved data - do you want to save it ?",
                            "VAMSAS Session Shutdown",
                            JvOptionPane.YES_NO_OPTION,
  
                    if (reply == JvOptionPane.YES_OPTION)
                    {
-                     Cache.log.debug("Prompting for vamsas store filename.");
+                     Console.debug("Prompting for vamsas store filename.");
 -                    Desktop.instance.vamsasSave_actionPerformed(null);
 +                    Desktop.getInstance().vamsasSave_actionPerformed(null);
-                     Cache.log
-                             .debug("Finished attempt at storing document.");
+                     Console.debug("Finished attempt at storing document.");
                    }
-                   Cache.log.debug(
+                   Console.debug(
                            "finished dealing with REQUESTTOCLOSE event.");
                  }
                  else
        {
          final IPickManager pm = vclient.getPickManager();
          final StructureSelectionManager ssm = StructureSelectionManager
-                 .getStructureSelectionManager(Desktop.getInstance());
 -                .getStructureSelectionManager(Desktop.instance);
++          .getStructureSelectionManager(Desktop.getInstance());
          final VamsasApplication me = this;
          pm.registerMessageHandler(new IMessageHandler()
          {
   */
  package jalview.gui;
  
 -import java.util.Locale;
 +import jalview.jbgui.GWebserviceInfo;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
++import jalview.util.ChannelProperties;
 +import jalview.ws.WSClientI;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
   */
  package jalview.gui;
  
++import jalview.bin.Console;
 +import jalview.gui.OptsAndParamsPage.OptionBox;
 +import jalview.gui.OptsAndParamsPage.ParamBox;
 +import jalview.util.MessageManager;
 +import jalview.ws.api.UIinfo;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.OptionI;
 +import jalview.ws.params.ParamDatastoreI;
 +import jalview.ws.params.ParameterI;
 +import jalview.ws.params.WsParamSetI;
  import java.awt.BorderLayout;
  import java.awt.Component;
  import java.awt.Dimension;
@@@ -210,17 -212,15 +210,18 @@@ public class WsJobParameters extends JP
     * 
     * @return
     */
 -  public boolean showRunDialog()
 +  public CompletionStage<Boolean> showRunDialog()
    {
 -    frame = new JDialog(Desktop.instance, true);
 -
 -    frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
 -            new String[]
 -            { service.getActionText() }));
 -    Rectangle deskr = Desktop.instance.getBounds();
 +    // Should JFrame hahve a parent of getDesktop ?
 +    frame = new JFrame();
 +    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
 +    if (service != null)
 +    {
 +      frame.setTitle(MessageManager.formatMessage("label.edit_params_for",
 +              new String[] { service.getActionText() }));
 +    }
 +    Rectangle deskr = Desktop.getInstance().getBounds();
      Dimension pref = this.getPreferredSize();
      frame.setBounds(
              new Rectangle((int) (deskr.getCenterX() - pref.width / 2),
    @Override
    public void itemStateChanged(ItemEvent e)
    {
 -    if (e.getSource() == setName && e.getStateChange() == e.SELECTED)
 +    if (e.getSource() == setName
 +            && e.getStateChange() == ItemEvent.SELECTED)
      {
        final String setname = (String) setName.getSelectedItem();
-       System.out.println("Item state changed for " + setname
-               + " (handling ? " + !settingDialog + ")");
+       if (Console.isDebugEnabled())
+       {
+         Console.debug("Item state changed for " + setname + " (handling ? "
+                 + !settingDialog + ")");
+       }
        if (settingDialog)
        {
          // ignore event
@@@ -230,8 -228,7 +229,7 @@@ public class WsParamSetManager implemen
          }
          paramFiles = paramFiles.concat(filename);
        }
 -      Cache.setProperty("WS_PARAM_FILES", paramFiles);
 +      Cache.setProperty(WS_PARAM_FILES, paramFiles);
  
        WebServiceParameterSet paramxml = new WebServiceParameterSet();
  
      {
        return;
      }
-     String paramFiles = jalview.bin.Cache.getDefault(WS_PARAM_FILES, "");
 -    String paramFiles = Cache.getDefault("WS_PARAM_FILES", "");
++    String paramFiles = Cache.getDefault(WS_PARAM_FILES, "");
      if (paramFiles.indexOf(filename) > -1)
      {
        String nparamFiles = new String();
            nparamFiles = nparamFiles.concat("|").concat(fl);
          }
        }
-       jalview.bin.Cache.setProperty(WS_PARAM_FILES, nparamFiles);
 -      Cache.setProperty("WS_PARAM_FILES", nparamFiles);
++      Cache.setProperty(WS_PARAM_FILES, nparamFiles);
      }
  
      try
index fa9cd92,0000000..cb087fa
mode 100644,000000..100644
--- /dev/null
@@@ -1,372 -1,0 +1,373 @@@
 +package jalview.hmmer;
 +
 +import jalview.analysis.SeqsetUtils.SequenceInfo;
 +import jalview.api.AlignViewportI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.AnnotatedCollectionI;
 +import jalview.datamodel.ResidueCount;
 +import jalview.datamodel.SequenceGroup;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.JvOptionPane;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileParse;
 +import jalview.io.HMMFile;
 +import jalview.util.FileUtils;
 +import jalview.util.MessageManager;
 +import jalview.ws.params.ArgumentI;
 +
 +import java.io.File;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Map;
 +
 +/**
 + * A class that runs the hmmbuild command as a separate process.
 + * 
 + * @author gmcarstairs
 + *
 + */
 +public class HMMBuild extends HmmerCommand
 +{
 +  static final String ARG_AMINO = "--amino";
 +
 +  static final String ARG_DNA = "--dna";
 +
 +  static final String ARG_RNA = "--rna";
 +
 +  /**
 +   * Constructor
 +   * 
 +   * @param alignFrame
 +   * @param args
 +   */
 +  public HMMBuild(AlignFrame alignFrame, List<ArgumentI> args)
 +  {
 +    super(alignFrame, args);
 +  }
 +
 +  /**
 +   * Builds a HMM from an alignment (and/or groups), then imports and adds it to
 +   * the alignment (and/or groups). Call this method directly to execute
 +   * synchronously, or via start() in a new Thread for asynchronously.
 +   */
 +  @Override
 +  public void run()
 +  {
 +    if (params == null || params.isEmpty())
 +    {
-       Cache.log.error("No parameters to HMMBuild!|");
++      Console.error("No parameters to HMMBuild!|");
 +      return;
 +    }
 +
 +    long msgID = System.currentTimeMillis();
 +    af.setProgressBar(MessageManager.getString("status.running_hmmbuild"),
 +            msgID);
 +
 +    AlignViewportI viewport = af.getViewport();
 +    try
 +    {
 +      /*
 +       * run hmmbuild for alignment and/or groups as selected
 +       */
 +      List<AnnotatedCollectionI> runBuildFor = parseParameters(viewport);
 +
 +      for (AnnotatedCollectionI grp : runBuildFor)
 +      {
 +        runHMMBuild(grp);
 +      }
 +    } finally
 +    {
 +      af.setProgressBar("", msgID);
 +      viewport.alignmentChanged(af.alignPanel);
 +      af.buildColourMenu(); // to enable HMMER colour schemes
 +    }
 +  }
 +
 +  /**
 +   * Scans the parameters to determine whether to run hmmmbuild for the whole
 +   * alignment or specified subgroup(s) or both
 +   * 
 +   * @param viewport
 +   * @return
 +   */
 +  protected List<AnnotatedCollectionI> parseParameters(
 +          AlignViewportI viewport)
 +  {
 +    List<AnnotatedCollectionI> runBuildFor = new ArrayList<>();
 +    boolean foundArg = false;
 +
 +    for (ArgumentI arg : params)
 +    {
 +      String name = arg.getName();
 +      if (MessageManager.getString("label.hmmbuild_for").equals(name))
 +      {
 +        foundArg = true;
 +        String value = arg.getValue();
 +
 +        if (MessageManager.getString("label.alignment").equals(value))
 +        {
 +          runBuildFor.add(viewport.getAlignmentView(false)
 +                  .getVisibleAlignment('-'));
 +        }
 +        else if (MessageManager.getString("label.groups_and_alignment")
 +                .equals(value))
 +        {
 +          runBuildFor.add(viewport.getAlignmentView(false)
 +                  .getVisibleAlignment('-'));
 +          runBuildFor.addAll(viewport.getAlignment().getGroups());
 +        }
 +        else if (MessageManager.getString("label.groups").equals(value))
 +        {
 +          runBuildFor.addAll(viewport.getAlignment().getGroups());
 +        }
 +        else if (MessageManager.getString("label.selected_group")
 +                .equals(value))
 +        {
 +          runBuildFor.add(viewport.getSelectionGroup());
 +        }
 +      }
 +      else if (MessageManager.getString("label.use_reference")
 +              .equals(name))
 +      {
 +        // todo disable this option if no RF annotation on alignment
 +        if (!af.getViewport().hasReferenceAnnotation())
 +        {
 +          JvOptionPane.showInternalMessageDialog(af, MessageManager
 +                  .getString("warn.no_reference_annotation"));
 +          // return;
 +        }
 +      }
 +    }
 +
 +    /*
 +     * default is to build for the whole alignment
 +     */
 +    if (!foundArg)
 +    {
 +      runBuildFor.add(alignment);
 +    }
 +
 +    return runBuildFor;
 +  }
 +
 +  /**
 +   * Runs hmmbuild on the given sequences (alignment or group)
 +   * 
 +   * @param grp
 +   */
 +  private void runHMMBuild(AnnotatedCollectionI ac)
 +  {
 +    File hmmFile = null;
 +    File alignmentFile = null;
 +    try
 +    {
 +      hmmFile = FileUtils.createTempFile("hmm", ".hmm");
 +      alignmentFile = FileUtils.createTempFile("output", ".sto");
 +
 +      if (ac instanceof Alignment)
 +      {
 +        AlignmentI al = (Alignment) ac;
 +        // todo pad gaps in an unaligned SequenceGroup as well?
 +        if (!al.isAligned())
 +        {
 +          al.padGaps();
 +        }
 +      }
 +
 +      deleteHmmSequences(ac);
 +
 +      List<SequenceI> copy = new ArrayList<>();
 +      if (ac instanceof Alignment)
 +      {
 +        copy.addAll(ac.getSequences());
 +      }
 +      else
 +      {
 +        SequenceI[] sel = ((SequenceGroup) ac)
 +                                              .getSelectionAsNewSequences((AlignmentI) ac.getContext());
 +        for (SequenceI seq : sel)
 +        {
 +          if (seq != null)
 +          {
 +            copy.add(seq);
 +          }
 +        }
 +      }
 +      // TODO rather than copy alignment data we should anonymize in situ -
 +      // export/File import could use anonymization hash to reinstate references
 +      // at import level ?
 +
 +      SequenceI[] copyArray = copy.toArray(new SequenceI[copy.size()]);
 +      Map<String, SequenceInfo> sequencesHash = stashSequences(copyArray);
 +
 +      exportStockholm(copyArray, alignmentFile, ac);
 +
 +      recoverSequences(sequencesHash, copy.toArray(new SequenceI[] {}));
 +
 +      boolean ran = runCommand(alignmentFile, hmmFile, ac);
 +      if (!ran)
 +      {
 +        JvOptionPane.showInternalMessageDialog(af, MessageManager
 +                .formatMessage("warn.command_failed", "hmmbuild"));
 +        return;
 +      }
 +      importData(hmmFile, ac);
 +    } catch (Exception e)
 +    {
 +      e.printStackTrace();
 +    } finally
 +    {
 +      if (hmmFile != null)
 +      {
 +        hmmFile.delete();
 +      }
 +      if (alignmentFile != null)
 +      {
 +        alignmentFile.delete();
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Constructs and executes the hmmbuild command as a separate process
 +   * 
 +   * @param sequencesFile
 +   *          the alignment from which the HMM is built
 +   * @param hmmFile
 +   *          the output file to which the HMM is written
 +   * @param group
 +   *          alignment or group for which the hmm is generated
 +   * 
 +   * @return
 +   * @throws IOException
 +   */
 +  private boolean runCommand(File sequencesFile, File hmmFile,
 +          AnnotatedCollectionI group) throws IOException
 +  {
 +    String cmd = getCommandPath(HMMBUILD);
 +    if (cmd == null)
 +    {
 +      return false; // executable not found
 +    }
 +    List<String> args = new ArrayList<>();
 +    args.add(cmd);
 +
 +    /*
 +     * HMM name (will be given to consensus sequence) is
 +     * - as specified by an input parameter if set
 +     * - else group name with _HMM appended (if for a group)
 +     * - else align frame title with _HMM appended (if title is not too long)
 +     * - else "Alignment_HMM" 
 +     */
 +    String name = "";
 +
 +    if (params != null)
 +    {
 +      for (ArgumentI arg : params)
 +      {
 +        String argName = arg.getName();
 +        switch (argName)
 +        {
 +        case "HMM Name":
 +          name = arg.getValue().trim();
 +          break;
 +        case "Use Reference Annotation":
 +          args.add("--hand");
 +          break;
 +        }
 +      }
 +    }
 +
 +    if (group instanceof SequenceGroup)
 +    {
 +      name = ((SequenceGroup) group).getName() + "_HMM";
 +    }
 +
 +    if ("".equals(name))
 +    {
 +      if (af != null && af.getTitle().length() < 15)
 +      {
 +        name = af.getTitle();
 +      }
 +      else
 +      {
 +        name = "Alignment_HMM";
 +      }
 +    }
 +
 +    args.add("-n");
 +    args.add(name.replace(' ', '_'));
 +    if (!alignment.isNucleotide())
 +    {
 +      args.add(ARG_AMINO); // TODO check for rna
 +    }
 +    else
 +    {
 +      args.add(ARG_DNA);
 +    }
 +
 +    args.add(getFilePath(hmmFile, true));
 +    args.add(getFilePath(sequencesFile, true));
 +
 +    return runCommand(args);
 +  }
 +
 +  /**
 +   * Imports the .hmm file produced by hmmbuild, and inserts the HMM consensus
 +   * sequence (with attached HMM profile) as the first sequence in the alignment
 +   * or group for which it was generated
 +   * 
 +   * @param hmmFile
 +   * @param ac
 +   *          (optional) the group for which the hmm was generated
 +   * @throws IOException
 +   */
 +  private void importData(File hmmFile, AnnotatedCollectionI ac)
 +          throws IOException
 +  {
 +    if (hmmFile.length() == 0L)
 +    {
-       Cache.log.error("Error: hmmbuild produced empty hmm file");
++      Console.error("Error: hmmbuild produced empty hmm file");
 +      return;
 +    }
 +
 +    HMMFile file = new HMMFile(
 +            new FileParse(hmmFile.getAbsolutePath(), DataSourceType.FILE));
 +    SequenceI hmmSeq = file.getHMM().getConsensusSequence();
 +
 +
 +
 +    ResidueCount counts = new ResidueCount(alignment.getSequences());
 +    hmmSeq.getHMM().setBackgroundFrequencies(counts);
 +
 +    if (hmmSeq == null)
 +    {
 +      // hmmbuild failure not detected earlier
 +      return;
 +    }
 +
 +    if (ac instanceof SequenceGroup)
 +    {
 +      SequenceGroup grp = (SequenceGroup) ac;
 +      char gapChar = alignment.getGapCharacter();
 +      hmmSeq.insertCharAt(0, ac.getStartRes(), gapChar);
 +      hmmSeq.insertCharAt(ac.getEndRes() + 1,
 +              alignment.getWidth() - ac.getEndRes() - 1, gapChar);
 +      SequenceI topSeq = grp.getSequencesInOrder(alignment)[0];
 +      int topIndex = alignment.findIndex(topSeq);
 +      alignment.insertSequenceAt(topIndex, hmmSeq);
 +      ac.setSeqrep(hmmSeq);
 +      grp.addSequence(hmmSeq, false);
 +    }
 +    else
 +    {
 +      alignment.insertSequenceAt(0, hmmSeq);
 +    }
 +  }
 +}
index f05823e,0000000..767a56e
mode 100644,000000..100644
--- /dev/null
@@@ -1,318 -1,0 +1,319 @@@
 +package jalview.hmmer;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.HiddenMarkovModel;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.gui.JvOptionPane;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileParse;
 +import jalview.io.StockholmFile;
 +import jalview.util.FileUtils;
 +import jalview.util.MessageManager;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.simple.BooleanOption;
 +import jalview.ws.params.simple.Option;
 +
 +import java.io.BufferedReader;
 +import java.io.File;
 +import java.io.FileReader;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.Collections;
 +import java.util.List;
 +
 +import javax.swing.JOptionPane;
 +
 +public class HMMSearch extends Search
 +{
 +
 +  boolean realign = false;
 +
 +  boolean trim = false;
 +
 +  boolean returnNoOfNewSeqs = false;
 +
 +  int seqsToReturn = Integer.MAX_VALUE;
 +
 +
 +  /**
 +   * Constructor for the HMMSearchThread
 +   * 
 +   * @param af
 +   */
 +  public HMMSearch(AlignFrame af, List<ArgumentI> args)
 +  {
 +    super(af, args);
 +  }
 +
 +  /**
 +   * Runs the HMMSearchThread: the data on the alignment or group is exported,
 +   * then the command is executed in the command line and then the data is
 +   * imported and displayed in a new frame. Call this method directly to execute
 +   * synchronously, or via start() in a new Thread for asynchronously.
 +   */
 +  @Override
 +  public void run()
 +  {
 +    HiddenMarkovModel hmm = getHmmProfile();
 +    if (hmm == null)
 +    {
 +      // shouldn't happen if we got this far
-       Cache.log.error("Error: no hmm for hmmsearch");
++      Console.error("Error: no hmm for hmmsearch");
 +      return;
 +    }
 +
 +    SequenceI hmmSeq = hmm.getConsensusSequence();
 +    long msgId = System.currentTimeMillis();
 +    af.setProgressBar(MessageManager.getString("status.running_search"),
 +            msgId);
 +
 +    try
 +    {
 +      File hmmFile = FileUtils.createTempFile("hmm", ".hmm");
 +      File hitsAlignmentFile = FileUtils.createTempFile("hitAlignment",
 +              ".sto");
 +      File searchOutputFile = FileUtils.createTempFile("searchOutput",
 +              ".sto");
 +
 +      exportHmm(hmm, hmmFile.getAbsoluteFile());
 +
 +      boolean ran = runCommand(searchOutputFile, hitsAlignmentFile, hmmFile);
 +      if (!ran)
 +      {
 +        JvOptionPane.showInternalMessageDialog(af, MessageManager
 +                .formatMessage("warn.command_failed", "hmmsearch"));
 +        return;
 +      }
 +
 +      importData(hmmSeq, hitsAlignmentFile, hmmFile, searchOutputFile);
 +      // TODO make realignment of search results a step at this level
 +      // and make it conditional on this.realign
 +    } catch (IOException | InterruptedException e)
 +    {
 +      e.printStackTrace();
 +    }
 +    finally
 +    {
 +      af.setProgressBar("", msgId);
 +    }
 +  }
 +
 +  /**
 +   * Executes an hmmsearch with the given hmm as input. The database to be
 +   * searched is a local file as specified by the 'Database' parameter, or the
 +   * current alignment (written to file) if none is specified.
 +   * 
 +   * @param searchOutputFile
 +   * @param hitsAlignmentFile
 +   * @param hmmFile
 +   * 
 +   * @return
 +   * @throws IOException
 +   */
 +  private boolean runCommand(File searchOutputFile, File hitsAlignmentFile,
 +          File hmmFile) throws IOException
 +  {
 +    String command = getCommandPath(HMMSEARCH);
 +    if (command == null)
 +    {
 +      return false;
 +    }
 +
 +    List<String> args = new ArrayList<>();
 +    args.add(command);
 +    buildArguments(args, searchOutputFile, hitsAlignmentFile, hmmFile);
 +
 +    return runCommand(args);
 +  }
 +
 +
 +  /**
 +   * Imports the data from the temporary file to which the output of hmmsearch
 +   * was directed. The results are optionally realigned using hmmalign.
 +   * 
 +   * @param hmmSeq
 +   */
 +  private void importData(SequenceI hmmSeq, File inputAlignmentTemp,
 +          File hmmTemp, File searchOutputFile)
 +          throws IOException, InterruptedException
 +  {
 +    BufferedReader br = new BufferedReader(
 +            new FileReader(inputAlignmentTemp));
 +    try
 +    {
 +      if (br.readLine() == null)
 +      {
 +        JOptionPane.showMessageDialog(af,
 +                MessageManager.getString("label.no_sequences_found"));
 +        return;
 +      }
 +      StockholmFile file = new StockholmFile(new FileParse(
 +              inputAlignmentTemp.getAbsolutePath(), DataSourceType.FILE));
 +      seqs = file.getSeqsAsArray();
 +
 +      readDomainTable(searchOutputFile, false);
 +
 +      if (searchAlignment)
 +      {
 +        recoverSequences(sequencesHash, seqs);
 +      }
 +
 +      // look for PP cons and ref seq in alignment only annotation
 +      AlignmentAnnotation modelpos = null, ppcons = null;
 +      for (AlignmentAnnotation aa : file.getAnnotations())
 +      {
 +        if (aa.sequenceRef == null)
 +        {
 +          if (aa.label.equals("Reference Positions")) // RF feature type in
 +                                                      // stockholm parser
 +          {
 +            modelpos = aa;
 +          }
 +          if (aa.label.equals("Posterior Probability"))
 +          {
 +            ppcons = aa;
 +          }
 +        }
 +      }
 +
 +
 +      int seqCount = Math.min(seqs.length, seqsToReturn);
 +      SequenceI[] hmmAndSeqs = new SequenceI[seqCount + 1];
 +      hmmSeq = hmmSeq.deriveSequence(); // otherwise all bad things happen
 +      hmmAndSeqs[0] = hmmSeq;
 +      System.arraycopy(seqs, 0, hmmAndSeqs, 1, seqCount);
 +      if (modelpos != null)
 +      {
 +        // TODO need - get ungapped sequence method
 +        hmmSeq.setSequence(
 +                hmmSeq.getDatasetSequence().getSequenceAsString());
 +        Annotation[] refpos = modelpos.annotations;
 +        // insert gaps to match with refseq positions
 +        int gc = 0, lcol = 0;
 +        for (int c = 0; c < refpos.length; c++)
 +        {
 +          if (refpos[c] != null && ("x".equals(refpos[c].displayCharacter)))
 +          {
 +            if (gc > 0)
 +            {
 +              hmmSeq.insertCharAt(lcol + 1, gc, '-');
 +            }
 +            gc = 0;
 +            lcol = c;
 +          }
 +          else
 +          {
 +            gc++;
 +          }
 +        }
 +      }
 +
 +      if (realign)
 +      {
 +        realignResults(hmmAndSeqs);
 +      }
 +      else
 +      {
 +        AlignmentI al = new Alignment(hmmAndSeqs);
 +        if (ppcons != null)
 +        {
 +          al.addAnnotation(ppcons);
 +        }
 +        if (modelpos != null)
 +        {
 +          al.addAnnotation(modelpos);
 +        }
 +        AlignFrame alignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
 +                AlignFrame.DEFAULT_HEIGHT);
 +        String ttl = "hmmSearch of " + databaseName + " using "
 +                + hmmSeq.getName();
 +        Desktop.addInternalFrame(alignFrame, ttl, AlignFrame.DEFAULT_WIDTH,
 +                AlignFrame.DEFAULT_HEIGHT);
 +
 +        if (returnNoOfNewSeqs)
 +        {
 +          int nNew = checkForNewSequences();
 +          JvOptionPane.showMessageDialog(af.alignPanel, nNew + " "
 +                  + MessageManager.getString("label.new_returned"));
 +        }
 +
 +      }
 +
 +
 +      hmmTemp.delete();
 +      inputAlignmentTemp.delete();
 +      searchOutputFile.delete();
 +    } finally
 +    {
 +      if (br != null)
 +      {
 +        br.close();
 +      }
 +    }
 +  }
 +
 +  private int checkForNewSequences()
 +  {
 +    int nNew = seqs.length;
 +
 +    for (SequenceI resultSeq : seqs)
 +    {
 +      for (SequenceI aliSeq : alignment.getSequencesArray())
 +      {
 +        if (resultSeq.getName().equals(aliSeq.getName()))
 +        {
 +          nNew--;
 +          break;
 +        }
 +      }
 +    }
 +
 +    return nNew;
 +
 +  }
 +
 +  /**
 +   * Realigns the given sequences using hmmalign, to the HMM profile sequence
 +   * which is the first in the array, and opens the results in a new frame
 +   * 
 +   * @param hmmAndSeqs
 +   */
 +  protected void realignResults(SequenceI[] hmmAndSeqs)
 +  {
 +    /*
 +     * and align the search results to the HMM profile
 +     */
 +    AlignmentI al = new Alignment(hmmAndSeqs);
 +    AlignFrame frame = new AlignFrame(al, 1, 1);
 +    List<ArgumentI> alignArgs = new ArrayList<>();
 +    String alignTo = hmmAndSeqs[0].getName();
 +    List<String> options = Collections.singletonList(alignTo);
 +    Option option = new Option(MessageManager.getString("label.use_hmm"),
 +            "", true, alignTo, alignTo, options, null);
 +    alignArgs.add(option);
 +    if (trim)
 +    {
 +      alignArgs.add(new BooleanOption(
 +              MessageManager.getString(TRIM_TERMINI_KEY),
 +              MessageManager.getString("label.trim_termini_desc"), true,
 +              true, true, null));
 +    }
 +    HmmerCommand hmmalign = new HMMAlign(frame, alignArgs);
 +    hmmalign.run();
 +
 +    if (returnNoOfNewSeqs)
 +    {
 +      int nNew = checkForNewSequences();
 +      JvOptionPane.showMessageDialog(frame.alignPanel,
 +              nNew + " " + MessageManager.getString("label.new_returned"));
 +    }
 +  }
 +
 +}
index e241008,0000000..9db0ae1
mode 100644,000000..100644
--- /dev/null
@@@ -1,538 -1,0 +1,539 @@@
 +package jalview.hmmer;
 +
 +import jalview.analysis.SeqsetUtils;
 +import jalview.analysis.SeqsetUtils.SequenceInfo;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.AnnotatedCollectionI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.HiddenMarkovModel;
 +import jalview.datamodel.SequenceGroup;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.JvOptionPane;
 +import jalview.gui.Preferences;
 +import jalview.io.FastaFile;
 +import jalview.io.HMMFile;
 +import jalview.io.StockholmFile;
 +import jalview.util.FileUtils;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
 +import jalview.ws.params.ArgumentI;
 +
 +import java.io.BufferedReader;
 +import java.io.File;
 +import java.io.IOException;
 +import java.io.InputStreamReader;
 +import java.io.PrintWriter;
 +import java.nio.file.Paths;
 +import java.util.ArrayList;
 +import java.util.Hashtable;
 +import java.util.List;
 +import java.util.Map;
 +
 +/**
 + * Base class for hmmbuild, hmmalign and hmmsearch
 + * 
 + * @author TZVanaalten
 + *
 + */
 +public abstract class HmmerCommand implements Runnable
 +{
 +  public static final String HMMBUILD = "hmmbuild";
 +
 +  protected final AlignFrame af;
 +
 +  protected final AlignmentI alignment;
 +
 +  protected final List<ArgumentI> params;
 +
 +  /*
 +   * constants for i18n lookup of passed parameter names
 +   */
 +  static final String DATABASE_KEY = "label.database";
 +
 +  static final String THIS_ALIGNMENT_KEY = "label.this_alignment";
 +
 +  static final String USE_ACCESSIONS_KEY = "label.use_accessions";
 +
 +  static final String AUTO_ALIGN_SEQS_KEY = "label.auto_align_seqs";
 +
 +  static final String NUMBER_OF_RESULTS_KEY = "label.number_of_results";
 +
 +  static final String NUMBER_OF_ITERATIONS = "label.number_of_iterations";
 +
 +  static final String TRIM_TERMINI_KEY = "label.trim_termini";
 +
 +  static final String RETURN_N_NEW_SEQ = "label.check_for_new_sequences";
 +
 +  static final String REPORTING_CUTOFF_KEY = "label.reporting_cutoff";
 +
 +  static final String CUTOFF_NONE = "label.default";
 +
 +  static final String CUTOFF_SCORE = "label.score";
 +
 +  static final String CUTOFF_EVALUE = "label.evalue";
 +
 +  static final String REPORTING_SEQ_EVALUE_KEY = "label.reporting_seq_evalue";
 +
 +  static final String REPORTING_DOM_EVALUE_KEY = "label.reporting_dom_evalue";
 +
 +  static final String REPORTING_SEQ_SCORE_KEY = "label.reporting_seq_score";
 +
 +  static final String REPORTING_DOM_SCORE_KEY = "label.reporting_dom_score";
 +
 +  static final String INCLUSION_SEQ_EVALUE_KEY = "label.inclusion_seq_evalue";
 +
 +  static final String INCLUSION_DOM_EVALUE_KEY = "label.inclusion_dom_evalue";
 +
 +  static final String INCLUSION_SEQ_SCORE_KEY = "label.inclusion_seq_score";
 +
 +  static final String INCLUSION_DOM_SCORE_KEY = "label.inclusion_dom_score";
 +
 +  static final String ARG_TRIM = "--trim";
 +
 +  static final String INCLUSION_THRESHOLD_KEY = "label.inclusion_threshold";
 +
 +  /**
 +   * Constructor
 +   * 
 +   * @param alignFrame
 +   * @param args
 +   */
 +  public HmmerCommand(AlignFrame alignFrame, List<ArgumentI> args)
 +  {
 +    af = alignFrame;
 +    alignment = af.getViewport().getAlignment();
 +    params = args;
 +  }
 +
 +  /**
 +   * Answers true if preference HMMER_PATH is set, and its value is the path to
 +   * a directory that contains an executable <code>hmmbuild</code> or
 +   * <code>hmmbuild.exe</code>, else false
 +   * 
 +   * @return
 +   */
 +  public static boolean isHmmerAvailable()
 +  {
 +    File exec = FileUtils.getExecutable(HMMBUILD,
 +            Cache.getProperty(Preferences.HMMER_PATH));
 +    return exec != null;
 +  }
 +
 +  /**
 +   * Uniquifies the sequences when exporting and stores their details in a
 +   * hashtable
 +   * 
 +   * @param seqs
 +   */
 +  protected Map<String, SequenceInfo> stashSequences(SequenceI[] seqs)
 +  {
 +    return SeqsetUtils.uniquify(seqs, true);
 +  }
 +
 +  /**
 +   * Restores the sequence data lost by uniquifying
 +   * 
 +   * @param sequencesHash
 +   * @param seqs
 +   */
 +  protected void recoverSequences(Map<String, SequenceInfo> sequencesHash, SequenceI[] seqs)
 +  {
 +    SeqsetUtils.deuniquify(sequencesHash, seqs);
 +  }
 +
 +  /**
 +   * Runs a command as a separate process and waits for it to complete. Answers
 +   * true if the process return status is zero, else false.
 +   * 
 +   * @param commands
 +   *          the executable command and any arguments to it
 +   * @throws IOException
 +   */
 +  public boolean runCommand(List<String> commands)
 +          throws IOException
 +  {
 +    List<String> args = Platform.isWindowsAndNotJS() ? wrapWithCygwin(commands)
 +            : commands;
 +
 +    try
 +    {
 +      ProcessBuilder pb = new ProcessBuilder(args);
 +      pb.redirectErrorStream(true); // merge syserr to sysout
 +      if (Platform.isWindowsAndNotJS())
 +      {
 +        String path = pb.environment().get("Path");
 +        path = jalview.bin.Cache.getProperty("CYGWIN_PATH") + ";" + path;
 +        pb.environment().put("Path", path);
 +      }
 +      final Process p = pb.start();
 +      new Thread(new Runnable()
 +      {
 +        @Override
 +        public void run()
 +        {
 +          BufferedReader input = new BufferedReader(
 +                  new InputStreamReader(p.getInputStream()));
 +          try
 +          {
 +            String line = input.readLine();
 +            while (line != null)
 +            {
 +              System.out.println(line);
 +              line = input.readLine();
 +            }
 +          } catch (IOException e)
 +          {
 +            e.printStackTrace();
 +          }
 +        }
 +      }).start();
 +
 +      p.waitFor();
 +      int exitValue = p.exitValue();
 +      if (exitValue != 0)
 +      {
-         Cache.log.error("Command failed, return code = " + exitValue);
-         Cache.log.error("Command/args were: " + args.toString());
++        Console.error("Command failed, return code = " + exitValue);
++        Console.error("Command/args were: " + args.toString());
 +      }
 +      return exitValue == 0; // 0 is success, by convention
 +    } catch (Exception e)
 +    {
 +      e.printStackTrace();
 +      return false;
 +    }
 +  }
 +
 +  /**
 +   * Converts the given command to a Cygwin "bash" command wrapper. The hmmer
 +   * command and any arguments to it are converted into a single parameter to the
 +   * bash command.
 +   * 
 +   * @param commands
 +   */
 +  protected List<String> wrapWithCygwin(List<String> commands)
 +  {
 +    File bash = FileUtils.getExecutable("bash",
 +            Cache.getProperty(Preferences.CYGWIN_PATH));
 +    if (bash == null)
 +    {
-       Cache.log.error("Cygwin shell not found");
++      Console.error("Cygwin shell not found");
 +      return commands;
 +    }
 +
 +    List<String> wrapped = new ArrayList<>();
 +    // wrapped.add("C:\Users\tva\run");
 +    wrapped.add(bash.getAbsolutePath());
 +    wrapped.add("-c");
 +
 +    /*
 +     * combine hmmbuild/search/align and arguments to a single string
 +     */
 +    StringBuilder sb = new StringBuilder();
 +    for (String cmd : commands)
 +    {
 +      sb.append(" ").append(cmd);
 +    }
 +    wrapped.add(sb.toString());
 +
 +    return wrapped;
 +  }
 +
 +  /**
 +   * Exports an alignment, and reference (RF) annotation if present, to the
 +   * specified file, in Stockholm format, removing all HMM sequences
 +   * 
 +   * @param seqs
 +   * @param toFile
 +   * @param annotated
 +   * @throws IOException
 +   */
 +  public void exportStockholm(SequenceI[] seqs, File toFile,
 +          AnnotatedCollectionI annotated)
 +          throws IOException
 +  {
 +    if (seqs == null)
 +    {
 +      return;
 +    }
 +    AlignmentI newAl = new Alignment(seqs);
 +
 +    if (!newAl.isAligned())
 +    {
 +      newAl.padGaps();
 +    }
 +
 +    if (toFile != null && annotated != null)
 +    {
 +      AlignmentAnnotation[] annots = annotated.getAlignmentAnnotation();
 +      if (annots != null)
 +      {
 +        for (AlignmentAnnotation annot : annots)
 +        {
 +          if (annot.label.contains("Reference") || "RF".equals(annot.label))
 +          {
 +            AlignmentAnnotation newRF;
 +            if (annot.annotations.length > newAl.getWidth())
 +            {
 +              Annotation[] rfAnnots = new Annotation[newAl.getWidth()];
 +              System.arraycopy(annot.annotations, 0, rfAnnots, 0,
 +                      rfAnnots.length);
 +              newRF = new AlignmentAnnotation("RF", "Reference Positions",
 +                      rfAnnots);
 +            }
 +            else
 +            {
 +              newRF = new AlignmentAnnotation(annot);
 +            }
 +            newAl.addAnnotation(newRF);
 +          }
 +        }
 +      }
 +    }
 +
 +    for (SequenceI seq : newAl.getSequencesArray())
 +    {
 +      if (seq.getAnnotation() != null)
 +      {
 +        for (AlignmentAnnotation ann : seq.getAnnotation())
 +        {
 +          seq.removeAlignmentAnnotation(ann);
 +        }
 +      }
 +    }
 +
 +    StockholmFile file = new StockholmFile(newAl);
 +    String output = file.print(seqs, false);
 +    PrintWriter writer = new PrintWriter(toFile);
 +    writer.println(output);
 +    writer.close();
 +  }
 +
 +  /**
 +   * Answers the full path to the given hmmer executable, or null if file cannot
 +   * be found or is not executable
 +   * 
 +   * @param cmd
 +   *          command short name e.g. hmmalign
 +   * @return
 +   * @throws IOException
 +   */
 +  protected String getCommandPath(String cmd)
 +          throws IOException
 +  {
 +    String binariesFolder = Cache.getProperty(Preferences.HMMER_PATH);
 +    // ensure any symlink to the directory is resolved:
 +    binariesFolder = Paths.get(binariesFolder).toRealPath().toString();
 +    File file = FileUtils.getExecutable(cmd, binariesFolder);
 +    if (file == null && af != null)
 +    {
 +      JvOptionPane.showInternalMessageDialog(af, MessageManager
 +              .formatMessage("label.executable_not_found", cmd));
 +    }
 +
 +    return file == null ? null : getFilePath(file, true);
 +  }
 +
 +  /**
 +   * Exports an HMM to the specified file
 +   * 
 +   * @param hmm
 +   * @param hmmFile
 +   * @throws IOException
 +   */
 +  public void exportHmm(HiddenMarkovModel hmm, File hmmFile)
 +          throws IOException
 +  {
 +    if (hmm != null)
 +    {
 +      HMMFile file = new HMMFile(hmm);
 +      PrintWriter writer = new PrintWriter(hmmFile);
 +      writer.print(file.print());
 +      writer.close();
 +    }
 +  }
 +
 +  // TODO is needed?
 +  /**
 +   * Exports a sequence to the specified file
 +   * 
 +   * @param hmm
 +   * @param hmmFile
 +   * @throws IOException
 +   */
 +  public void exportSequence(SequenceI seq, File seqFile) throws IOException
 +  {
 +    if (seq != null)
 +    {
 +      FastaFile file = new FastaFile();
 +      PrintWriter writer = new PrintWriter(seqFile);
 +      writer.print(file.print(new SequenceI[] { seq }, false));
 +      writer.close();
 +    }
 +  }
 +
 +  /**
 +   * Answers the HMM profile for the profile sequence the user selected (default
 +   * is just the first HMM sequence in the alignment)
 +   * 
 +   * @return
 +   */
 +  protected HiddenMarkovModel getHmmProfile()
 +  {
 +    String alignToParamName = MessageManager.getString("label.use_hmm");
 +    for (ArgumentI arg : params)
 +    {
 +      String name = arg.getName();
 +      if (name.equals(alignToParamName))
 +      {
 +        String seqName = arg.getValue();
 +        SequenceI hmmSeq = alignment.findName(seqName);
 +        if (hmmSeq.hasHMMProfile())
 +        {
 +          return hmmSeq.getHMM();
 +        }
 +      }
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * Answers the query sequence the user selected (default is just the first
 +   * sequence in the alignment)
 +   * 
 +   * @return
 +   */
 +  protected SequenceI getSequence()
 +  {
 +    String alignToParamName = MessageManager
 +            .getString("label.use_sequence");
 +    for (ArgumentI arg : params)
 +    {
 +      String name = arg.getName();
 +      if (name.equals(alignToParamName))
 +      {
 +        String seqName = arg.getValue();
 +        SequenceI seq = alignment.findName(seqName);
 +        return seq;
 +      }
 +    }
 +    return null;
 +  }
 +
 +  /**
 +   * Answers an absolute path to the given file, in a format suitable for
 +   * processing by a hmmer command. On a Windows platform, the native Windows file
 +   * path is converted to Cygwin format, by replacing '\'with '/' and drive letter
 +   * X with /cygdrive/x.
 +   * 
 +   * @param resultFile
 +   * @param isInCygwin
 +   *                     True if file is to be read/written from within the Cygwin
 +   *                     shell. Should be false for any imports.
 +   * @return
 +   */
 +  protected String getFilePath(File resultFile, boolean isInCygwin)
 +  {
 +    String path = resultFile.getAbsolutePath();
 +    if (Platform.isWindowsAndNotJS() && isInCygwin)
 +    {
 +      // the first backslash escapes '\' for the regular expression argument
 +      path = path.replaceAll("\\" + File.separator, "/");
 +      int colon = path.indexOf(':');
 +      if (colon > 0)
 +      {
 +        String drive = path.substring(0, colon);
 +        path = path.replaceAll(drive + ":", "/cygdrive/" + drive);
 +      }
 +    }
 +
 +    return path;
 +  }
 +
 +  /**
 +   * A helper method that deletes any HMM consensus sequence from the given
 +   * collection, and from the parent alignment if <code>ac</code> is a subgroup
 +   * 
 +   * @param ac
 +   */
 +  void deleteHmmSequences(AnnotatedCollectionI ac)
 +  {
 +    List<SequenceI> hmmSeqs = ac.getHmmSequences();
 +    for (SequenceI hmmSeq : hmmSeqs)
 +    {
 +      if (ac instanceof SequenceGroup)
 +      {
 +        ((SequenceGroup) ac).deleteSequence(hmmSeq, false);
 +        AnnotatedCollectionI context = ac.getContext();
 +        if (context != null && context instanceof AlignmentI)
 +        {
 +          ((AlignmentI) context).deleteSequence(hmmSeq);
 +        }
 +      }
 +      else
 +      {
 +        ((AlignmentI) ac).deleteSequence(hmmSeq);
 +      }
 +    }
 +  }
 +
 +  /**
 +   * Sets the names of any duplicates within the given sequences to include their
 +   * respective lengths. Deletes any duplicates that have the same name after this
 +   * step
 +   * 
 +   * @param seqs
 +   */
 +  void renameDuplicates(AlignmentI al)
 +  {
 +
 +    SequenceI[] seqs = al.getSequencesArray();
 +    List<Boolean> wasRenamed = new ArrayList<>();
 +
 +    for (SequenceI seq : seqs)
 +    {
 +      wasRenamed.add(false);
 +    }
 +
 +    for (int i = 0; i < seqs.length; i++)
 +    {
 +      for (int j = 0; j < seqs.length; j++)
 +      {
 +        if (seqs[i].getName().equals(seqs[j].getName()) && i != j
 +                && !wasRenamed.get(j))
 +        {
 +
 +          wasRenamed.set(i, true);
 +          String range = "/" + seqs[j].getStart() + "-" + seqs[j].getEnd();
 +          // setting sequence name to include range - to differentiate between
 +          // sequences of the same name. Currently have to include the range twice
 +          // because the range is removed (once) when setting the name
 +          // TODO come up with a better way of doing this
 +          seqs[j].setName(seqs[j].getName() + range + range);
 +        }
 +
 +      }
 +      if (wasRenamed.get(i))
 +      {
 +        String range = "/" + seqs[i].getStart() + "-" + seqs[i].getEnd();
 +        seqs[i].setName(seqs[i].getName() + range + range);
 +      }
 +    }
 +
 +    for (int i = 0; i < seqs.length; i++)
 +    {
 +      for (int j = 0; j < seqs.length; j++)
 +      {
 +        if (seqs[i].getName().equals(seqs[j].getName()) && i != j)
 +        {
 +          al.deleteSequence(j);
 +        }
 +      }
 +    }
 +  }
 +
 +}
index 12c0492,0000000..00314f0
mode 100644,000000..100644
--- /dev/null
@@@ -1,179 -1,0 +1,180 @@@
 +package jalview.hmmer;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.SequenceI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.gui.JvOptionPane;
 +import jalview.io.DataSourceType;
 +import jalview.io.FileParse;
 +import jalview.io.StockholmFile;
 +import jalview.util.FileUtils;
 +import jalview.util.MessageManager;
 +import jalview.ws.params.ArgumentI;
 +
 +import java.io.BufferedReader;
 +import java.io.File;
 +import java.io.FileReader;
 +import java.io.IOException;
 +import java.util.ArrayList;
 +import java.util.List;
 +
 +import javax.swing.JOptionPane;
 +
 +public class JackHMMER extends Search
 +{
 +
 +  SequenceI seq = null;
 +
 +  /**
 +   * Constructor for the JackhmmerThread
 +   * 
 +   * @param af
 +   */
 +  public JackHMMER(AlignFrame af, List<ArgumentI> args)
 +  {
 +    super(af, args);
 +  }
 +
 +  /**
 +   * Runs the JackhmmerThread: the data on the alignment or group is exported,
 +   * then the command is executed in the command line and then the data is
 +   * imported and displayed in a new frame. Call this method directly to execute
 +   * synchronously, or via start() in a new Thread for asynchronously.
 +   */
 +  @Override
 +  public void run()
 +  {
 +    seq = getSequence();
 +    if (seq == null)
 +    {
 +      // shouldn't happen if we got this far
-       Cache.log.error("Error: no sequence for jackhmmer");
++      Console.error("Error: no sequence for jackhmmer");
 +      return;
 +    }
 +
 +    long msgId = System.currentTimeMillis();
 +    af.setProgressBar(MessageManager.getString("status.running_search"),
 +            msgId);
 +
 +    try
 +    {
 +      File seqFile = FileUtils.createTempFile("seq", ".sto");
 +      File hitsAlignmentFile = FileUtils.createTempFile("hitAlignment",
 +              ".sto");
 +      File searchOutputFile = FileUtils.createTempFile("searchOutput",
 +              ".txt");
 +
 +      exportStockholm(new SequenceI[] { seq }, seqFile.getAbsoluteFile(),
 +              null);
 +
 +      boolean ran = runCommand(searchOutputFile, hitsAlignmentFile,
 +              seqFile);
 +      if (!ran)
 +      {
 +        JvOptionPane.showInternalMessageDialog(af, MessageManager
 +                .formatMessage("warn.command_failed", "jackhmmer"));
 +        return;
 +      }
 +
 +      importData(hitsAlignmentFile, seqFile, searchOutputFile);
 +      // TODO make realignment of search results a step at this level
 +      // and make it conditional on this.realign
 +    } catch (IOException | InterruptedException e)
 +    {
 +      e.printStackTrace();
 +    } finally
 +    {
 +      af.setProgressBar("", msgId);
 +    }
 +  }
 +
 +  /**
 +   * Executes an jackhmmer search with the given sequence as input. The database
 +   * to be searched is a local file as specified by the 'Database' parameter, or
 +   * the current alignment (written to file) if none is specified.
 +   * 
 +   * @param searchOutputFile
 +   * @param hitsAlignmentFile
 +   * @param seqFile
 +   * 
 +   * @return
 +   * @throws IOException
 +   */
 +  private boolean runCommand(File searchOutputFile, File hitsAlignmentFile,
 +          File seqFile) throws IOException
 +  {
 +    String command = getCommandPath(JACKHMMER);
 +    if (command == null)
 +    {
 +      return false;
 +    }
 +
 +    List<String> args = new ArrayList<>();
 +    args.add(command);
 +    buildArguments(args, searchOutputFile, hitsAlignmentFile, seqFile);
 +
 +    return runCommand(args);
 +  }
 +
 +  /**
 +   * Imports the data from the temporary file to which the output of jackhmmer was
 +   * directed.
 +   */
 +  private void importData(File inputAlignmentTemp, File seqTemp,
 +          File searchOutputFile) throws IOException, InterruptedException
 +  {
 +    BufferedReader br = new BufferedReader(
 +            new FileReader(inputAlignmentTemp));
 +    try
 +    {
 +      if (br.readLine() == null)
 +      {
 +        JOptionPane.showMessageDialog(af,
 +                MessageManager.getString("label.no_sequences_found"));
 +        return;
 +      }
 +      StockholmFile file = new StockholmFile(new FileParse(
 +              inputAlignmentTemp.getAbsolutePath(), DataSourceType.FILE));
 +      seqs = file.getSeqsAsArray();
 +
 +      readDomainTable(searchOutputFile, true);
 +
 +      if (searchAlignment)
 +      {
 +        recoverSequences(sequencesHash, seqs);
 +      }
 +
 +
 +
 +      int seqCount = seqs.length;
 +
 +
 +      AlignmentI al = new Alignment(seqs);
 +
 +      AlignFrame alignFrame = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
 +              AlignFrame.DEFAULT_HEIGHT);
 +      String ttl = "jackhmmer search of " + databaseName + " using "
 +              + seqs[0].getName();
 +      Desktop.addInternalFrame(alignFrame, ttl, AlignFrame.DEFAULT_WIDTH,
 +              AlignFrame.DEFAULT_HEIGHT);
 +
 +      seqTemp.delete();
 +      inputAlignmentTemp.delete();
 +      searchOutputFile.delete();
 +    } finally
 +    {
 +      if (br != null)
 +      {
 +        br.close();
 +      }
 +    }
 +  }
 +
 +
 +
 +
 +}
Simple merge
Simple merge
@@@ -656,14 -662,16 +662,13 @@@ public class BackupFile
            MessageManager.getString("label.keep") };
  
        confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
 -              : JvOptionPane.showOptionDialog(Desktop.desktop,
 -                      messageSB.toString(),
 -                      MessageManager.getString(
 -                              "label.backupfiles_confirm_delete"),
 -                      // "Confirm delete"
 -                      JvOptionPane.YES_NO_OPTION,
 -                      JvOptionPane.WARNING_MESSAGE, null, options,
 -                      options[0]);
 +              : JvOptionPane.showOptionDialog(Desktop.getDesktopPane(),
 +              messageSB.toString(),
 +              MessageManager.getString("label.backupfiles_confirm_delete"),
 +              JvOptionPane.YES_NO_OPTION, JvOptionPane.WARNING_MESSAGE,
 +              null, options, options[0]);
      }
  
      // return should be TRUE if file is to be deleted
      return (confirmButton == JvOptionPane.YES_OPTION);
    }
          }
  
          int confirmButton = Platform.isHeadless() ? JvOptionPane.YES_OPTION
-                 : JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
 -                : JvOptionPane.showConfirmDialog(Desktop.desktop,
++          : JvOptionPane.showConfirmDialog(Desktop.getDesktopPane(),
                          messageSB.toString(),
                          MessageManager.getString(
                                  "label.backupfiles_confirm_delete"),
@@@ -45,14 -45,15 +46,14 @@@ public class BioJsHTMLOutput extends HT
  
    private static TreeMap<String, File> bioJsMSAVersions;
  
 -  public static final String DEFAULT_DIR = System.getProperty("user.home")
 -          + File.separatorChar + ".biojs_templates" + File.separatorChar;
 +  public static final String DEFAULT_DIR = Platform.getUserPath(".biojs_templates/");
  
-   public static final String BJS_TEMPLATES_LOCAL_DIRECTORY = jalview.bin.Cache
+   public static final String BJS_TEMPLATES_LOCAL_DIRECTORY = Cache
            .getDefault("biojs_template_directory", DEFAULT_DIR);
  
-   public static final String BJS_TEMPLATE_GIT_REPO = jalview.bin.Cache
-           .getDefault("biojs_template_git_repo",
-                   "https://raw.githubusercontent.com/jalview/exporter-templates/master/biojs/package.json");
+   public static final String BJS_TEMPLATE_GIT_REPO = Cache.getDefault(
+           "biojs_template_git_repo",
+           "https://raw.githubusercontent.com/jalview/exporter-templates/master/biojs/package.json");
  
    public BioJsHTMLOutput(AlignmentPanel ap)
    {
Simple merge
Simple merge
Simple merge
Simple merge
   */
  package jalview.io;
  
 -import jalview.bin.Cache;
  import jalview.datamodel.DBRefEntry;
  import jalview.datamodel.SequenceI;
 +import jalview.util.Platform;
++import jalview.bin.Console;
  
  import java.util.List;
  
@@@ -96,20 -96,11 +97,20 @@@ public class ModellerDescriptio
      }
    };
  
 +  private static Regex VALIDATION_REGEX;
 +
 +  private static Regex getRegex()
 +  {
 +    return (VALIDATION_REGEX == null
 +            ? VALIDATION_REGEX = Platform
 +                    .newRegex("\\s*((([-0-9]+).?)|FIRST|LAST|@)", null)
 +            : VALIDATION_REGEX);
 +  }
    private resCode validResidueCode(String field)
    {
      Integer val = null;
 -    Regex r = new Regex("\\s*((([-0-9]+).?)|FIRST|LAST|@)");
 +    Regex r = getRegex();
      if (!r.search(field))
      {
        return null; // invalid
      {
        value = r.stringMatched(1);
      }
-     // jalview.bin.Cache.log.debug("from '" + field + "' matched '" + value +
 -    // Cache.debug("from '" + field + "' matched '" + value +
++    // jalview.bin.Console.debug("from '" + field + "' matched '" + value +
      // "'");
      try
      {
                  }
                  else
                  {
-                   // jalview.bin.Cache.log.debug(
 -                  // Cache.debug(
++                  // jalview.bin.Console.debug(
                    // "Ignoring non-Modeller description: invalid integer-like
                    // field '" + field + "'");
                    type = -1; /* invalid field! - throw the FieldSet away */
@@@ -40,8 -39,6 +40,7 @@@ import java.util.StringTokenizer
  
  import com.stevesoft.pat.Regex;
  
 +// TODO This class does not conform to Java standards for field name capitalization.
  /**
   * Parse a new hanpshire style tree Caveats: NHX files are NOT supported and the
   * tree distances and topology are unreliable when they are parsed. TODO: on
@@@ -20,8 -20,7 +20,8 @@@
   */
  package jalview.io;
  
- import java.util.Locale;
+ import java.util.ArrayList;
 +
  import java.util.Collection;
  import java.util.Comparator;
  import java.util.LinkedHashMap;
@@@ -530,24 -520,7 +547,21 @@@ public class SequenceAnnotationRepor
          maxWidth = Math.max(maxWidth, sz);
        }
      }
 +    if (sequence.getAnnotation("Search Scores") != null)
 +    {
 +      sb.append("<br>");
 +      String eValue = " E-Value: "
 +              + sequence.getAnnotation("Search Scores")[0].getEValue();
 +      String bitScore = " Bit Score: "
 +              + sequence.getAnnotation("Search Scores")[0].getBitScore();
 +      sb.append(eValue);
 +      sb.append("<br>");
 +      sb.append(bitScore);
 +      maxWidth = Math.max(maxWidth, eValue.length());
 +      maxWidth = Math.max(maxWidth, bitScore.length());
 +      sb.append("<br>");
 +    }
      sb.append("</i>");
      return maxWidth;
    }
  
        return 0;
      }
  
+     // PATCH for JAL-3980 defensive copy
+     dbrefs = new ArrayList<DBRefEntry>();
+     dbrefs.addAll(dbrefset);
 -
      // note this sorts the refs held on the sequence!
      dbrefs.sort(comparator);
      boolean ellipsis = false;
@@@ -57,9 -40,22 +57,7 @@@ 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.*;
  /**
   * This class is supposed to parse a Stockholm format file into Jalview There
   * are TODOs in this class: we do not know what the database source and version
@@@ -80,105 -76,21 +78,105 @@@ public class StockholmFile extends Alig
  {
    private static final String ANNOTATION = "annotation";
  
 -  // private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
 -  //
 -  // private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
 -
 -  public static final Regex DETECT_BRACKETS = new Regex(
 -          "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 +  private static final char UNDERSCORE = '_';
 +  
 +  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
  
 -  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using
 -  // NOT_RNASS first.
    public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
  
 +  public static final int REGEX_STOCKHOLM = 0;
 +
 +  public static final int REGEX_BRACKETS = 1;
    // use the following regex to decide an annotations (whole) line is NOT an RNA
    // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
 -  private static final Regex NOT_RNASS = new Regex(
 -          "^[^<>[\\](){}ADFJ-RUVWYZadfj-ruvwyz]*$");
 +  public static final int REGEX_NOT_RNASS = 2;
 +
 +  private static final int REGEX_ANNOTATION = 3;
 +
 +  private static final int REGEX_PFAM = 4;
 +
 +  private static final int REGEX_RFAM = 5;
 +
 +  private static final int REGEX_ALIGN_END = 6;
 +
 +  private static final int REGEX_SPLIT_ID = 7;
 +
 +  private static final int REGEX_SUBTYPE = 8;
 +
 +  private static final int REGEX_ANNOTATION_LINE = 9;
 +
 +  private static final int REGEX_REMOVE_ID = 10;
 +
 +  private static final int REGEX_OPEN_PAREN = 11;
 +
 +  private static final int REGEX_CLOSE_PAREN = 12;
 +
 +  public static final int REGEX_MAX = 13;
 +
 +  private static Regex REGEX[] = new Regex[REGEX_MAX];
 +
 +  /**
 +   * Centralize all actual Regex instantialization in Platform.
 +   * // JBPNote: Why is this 'centralisation' better ?
 +   * @param id
 +   * @return
 +   */
 +  private static Regex getRegex(int id)
 +  {
 +    if (REGEX[id] == null)
 +    {
 +      String pat = null, pat2 = null;
 +      switch (id)
 +      {
 +      case REGEX_STOCKHOLM:
 +        pat = "# STOCKHOLM ([\\d\\.]+)";
 +        break;
 +      case REGEX_BRACKETS:
 +        // for reference; not used
 +        pat = "(<|>|\\[|\\]|\\(|\\)|\\{|\\})";
 +        break;
 +      case REGEX_NOT_RNASS:
-         pat = "^[^<>[\\](){}A-DF-Za-df-z]*$";
++        pat = "^[^<>[\\](){}ADFJ-RUVWYZadfj-ruvwyz]*$"; // update 2.11.2
 +        break;
 +      case REGEX_ANNOTATION:
 +        pat = "(\\w+)\\s*(.*)";
 +        break;
 +      case REGEX_PFAM:
 +        pat = "PF[0-9]{5}(.*)";
 +        break;
 +      case REGEX_RFAM:
 +        pat = "RF[0-9]{5}(.*)";
 +        break;
 +      case REGEX_ALIGN_END:
 +        pat = "^\\s*\\/\\/";
 +        break;
 +      case REGEX_SPLIT_ID:
 +        pat = "(\\S+)\\/(\\d+)\\-(\\d+)";
 +        break;
 +      case REGEX_SUBTYPE:
 +        pat = "(\\S+)\\s+(\\S*)\\s+(.*)";
 +        break;
 +      case REGEX_ANNOTATION_LINE:
 +        pat = "#=(G[FSRC]?)\\s+(.*)";
 +        break;
 +      case REGEX_REMOVE_ID:
 +        pat = "(\\S+)\\s+(\\S+)";
 +        break;
 +      case REGEX_OPEN_PAREN:
 +        pat = "(<|\\[)";
 +        pat2 = "(";
 +        break;
 +      case REGEX_CLOSE_PAREN:
 +        pat = "(>|\\])";
 +        pat2 = ")";
 +        break;
 +      default:
 +        return null;
 +      }
 +      REGEX[id] = Platform.newRegex(pat, pat2);
 +    }
 +    return REGEX[id];
 +  }
  
    StringBuffer out; // output buffer
  
      }
  
      // We define some Regexes here that will be used regularily later
 -    rend = new Regex("^\\s*\\/\\/"); // Find the end of an alignment
 -    p = new Regex("(\\S+)\\/(\\d+)\\-(\\d+)"); // split sequence id in
 +    rend = getRegex(REGEX_ALIGN_END);//"^\\s*\\/\\/"); // Find the end of an alignment
 +    p = getRegex(REGEX_SPLIT_ID);//"(\\S+)\\/(\\d+)\\-(\\d+)"); // split sequence id in
      // id/from/to
 -    s = new Regex("(\\S+)\\s+(\\S*)\\s+(.*)"); // Parses annotation subtype
 -    r = new Regex("#=(G[FSRC]?)\\s+(.*)"); // Finds any annotation line
 -    x = new Regex("(\\S+)\\s+(\\S+)"); // split id from sequence
 +    s = getRegex(REGEX_SUBTYPE);// "(\\S+)\\s+(\\S*)\\s+(.*)"); // Parses
 +                                // annotation subtype
 +    r = getRegex(REGEX_ANNOTATION_LINE);// "#=(G[FSRC]?)\\s+(.*)"); // Finds any
 +                                        // annotation line
 +    x = getRegex(REGEX_REMOVE_ID);// "(\\S+)\\s+(\\S+)"); // split id from
 +                                  // sequence
  
      // Convert all bracket types to parentheses (necessary for passing to VARNA)
 -    Regex openparen = new Regex("(<|\\[)", "(");
 -    Regex closeparen = new Regex("(>|\\])", ")");
 +    Regex openparen = getRegex(REGEX_OPEN_PAREN);//"(<|\\[)", "(");
 +    Regex closeparen = getRegex(REGEX_CLOSE_PAREN);//"(>|\\])", ")");
  
 -    // // Detect if file is RNA by looking for bracket types
 -    // Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
 +//    // Detect if file is RNA by looking for bracket types
-     // Regex detectbrackets = getRegex("(<|>|\\[|\\]|\\(|\\))");
++//    Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
  
      rend.optimize();
      p.optimize();
   */
  package jalview.jbgui;
  
 -import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 -import jalview.analysis.GeneticCodeI;
 -import jalview.analysis.GeneticCodes;
 -import jalview.api.SplitContainerI;
 -import jalview.bin.Cache;
 -import jalview.gui.JvSwingUtils;
 -import jalview.gui.Preferences;
 -import jalview.io.FileFormats;
 -import jalview.schemes.ResidueColourScheme;
 -import jalview.util.MessageManager;
 -import jalview.util.Platform;
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.GridLayout;
@@@ -51,21 -61,6 +52,19 @@@ import javax.swing.event.ChangeEvent
  import javax.swing.event.MenuEvent;
  import javax.swing.event.MenuListener;
  
 +import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 +import jalview.analysis.GeneticCodeI;
 +import jalview.analysis.GeneticCodes;
 +import jalview.api.SplitContainerI;
 +import jalview.bin.Cache;
 +import jalview.gui.JvSwingUtils;
 +import jalview.gui.Preferences;
 +import jalview.hmmer.HmmerCommand;
 +import jalview.io.FileFormatException;
 +import jalview.io.FileFormats;
 +import jalview.schemes.ResidueColourScheme;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
  @SuppressWarnings("serial")
  public class GAlignFrame extends JInternalFrame
  {
  
    protected JCheckBoxMenuItem normaliseSequenceLogo = new JCheckBoxMenuItem();
  
 +  protected JCheckBoxMenuItem showInformationHistogram = new JCheckBoxMenuItem();
 +
 +  protected JCheckBoxMenuItem showHMMSequenceLogo = new JCheckBoxMenuItem();
 +
 +  protected JCheckBoxMenuItem normaliseHMMSequenceLogo = new JCheckBoxMenuItem();
    protected JCheckBoxMenuItem applyAutoAnnotationSettings = new JCheckBoxMenuItem();
  
    protected JMenuItem openFeatureSettings;
      {
  
        // for Web-page embedding using id=align-frame-div
 -      setName("jalview-alignment");
 +      setName(Platform.getAppID("alignment"));
  
        jbInit();
        setJMenuBar(alignFrameMenuBar);
  
    private void jbInit() throws Exception
    {
      initColourMenu();
 +  
      JMenuItem saveAs = new JMenuItem(
              MessageManager.getString("action.save_as"));
      ActionListener al = new ActionListener()
          saveAs_actionPerformed();
        }
      };
 +  
      // FIXME getDefaultToolkit throws an exception in Headless mode
 -    KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
 -            jalview.util.ShortcutKeyMaskExWrapper.getMenuShortcutKeyMaskEx()
 -                    | jalview.util.ShortcutKeyMaskExWrapper.SHIFT_DOWN_MASK,
 +    KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S, Platform.SHORTCUT_KEY_MASK | InputEvent.SHIFT_DOWN_MASK,
              false);
      addMenuActionAndAccelerator(keyStroke, saveAs, al);
 +  
      closeMenuItem.setText(MessageManager.getString("action.close"));
 -    keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W,
 -            jalview.util.ShortcutKeyMaskExWrapper
 -                    .getMenuShortcutKeyMaskEx(),
 -            false);
 +    keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_W, Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
        }
      };
      addMenuActionAndAccelerator(keyStroke, closeMenuItem, al);
 +  
      JMenu editMenu = new JMenu(MessageManager.getString("action.edit"));
      JMenu viewMenu = new JMenu(MessageManager.getString("action.view"));
      JMenu annotationsMenu = new JMenu(
      JMenu calculateMenu = new JMenu(
              MessageManager.getString("action.calculate"));
      webService.setText(MessageManager.getString("action.web_service"));
 +    initHMMERMenu();
      JMenuItem selectAllSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.select_all"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_A,
        }
      };
      addMenuActionAndAccelerator(keyStroke, selectAllSequenceMenuItem, al);
 +  
      JMenuItem deselectAllSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.deselect_all"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, deselectAllSequenceMenuItem, al);
 +  
      JMenuItem invertSequenceMenuItem = new JMenuItem(
              MessageManager.getString("action.invert_sequence_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
        }
      };
      addMenuActionAndAccelerator(keyStroke, invertSequenceMenuItem, al);
 +  
      JMenuItem grpsFromSelection = new JMenuItem(
              MessageManager.getString("action.make_groups_selection"));
      grpsFromSelection.addActionListener(new ActionListener()
        }
      };
      addMenuActionAndAccelerator(keyStroke, remove2LeftMenuItem, al);
 +  
      JMenuItem remove2RightMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_right"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_R,
        }
      };
      addMenuActionAndAccelerator(keyStroke, remove2RightMenuItem, al);
 +  
      JMenuItem removeGappedColumnMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_empty_columns"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
        }
      };
      addMenuActionAndAccelerator(keyStroke, removeGappedColumnMenuItem, al);
 +  
      JMenuItem removeAllGapsMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_all_gaps"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_E,
        }
      };
      addMenuActionAndAccelerator(keyStroke, removeAllGapsMenuItem, al);
 +  
      JMenuItem justifyLeftMenuItem = new JMenuItem(
              MessageManager.getString("action.left_justify_alignment"));
      justifyLeftMenuItem.addActionListener(new ActionListener()
          sortGroupMenuItem_actionPerformed(e);
        }
      });
 +    JMenuItem sortEValueMenuItem = new JMenuItem(
 +            MessageManager.getString("action.by_evalue"));
 +    sortEValueMenuItem.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        sortEValueMenuItem_actionPerformed(e);
 +      }
 +    });
 +    JMenuItem sortBitScoreMenuItem = new JMenuItem(
 +            MessageManager.getString("action.by_bit_score"));
 +    sortBitScoreMenuItem.addActionListener(new ActionListener()
 +    {
 +      @Override
 +      public void actionPerformed(ActionEvent e)
 +      {
 +        sortBitScoreMenuItem_actionPerformed(e);
 +      }
 +    });
 +  
      JMenuItem removeRedundancyMenuItem = new JMenuItem(
              MessageManager.getString("action.remove_redundancy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_D,
          pairwiseAlignmentMenuItem_actionPerformed(e);
        }
      });
 +  
      this.getContentPane().setLayout(new BorderLayout());
      alignFrameMenuBar.setFont(new java.awt.Font("Verdana", 0, 11));
      statusBar.setBackground(Color.white);
          colourTextMenuItem_actionPerformed(e);
        }
      });
 +  
      JMenuItem htmlMenuItem = new JMenuItem(
              MessageManager.getString("label.html"));
      htmlMenuItem.addActionListener(new ActionListener()
          htmlMenuItem_actionPerformed(e);
        }
      });
 +  
      JMenuItem createBioJS = new JMenuItem(
              MessageManager.getString("label.biojs_html_export"));
      createBioJS.addActionListener(new java.awt.event.ActionListener()
          bioJSMenuItem_actionPerformed(e);
        }
      });
 +  
      JMenuItem overviewMenuItem = new JMenuItem(
              MessageManager.getString("label.overview_window"));
      overviewMenuItem.addActionListener(new ActionListener()
          overviewMenuItem_actionPerformed(e);
        }
      });
 +  
      undoMenuItem.setEnabled(false);
      undoMenuItem.setText(MessageManager.getString("action.undo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Z,
        }
      };
      addMenuActionAndAccelerator(keyStroke, undoMenuItem, al);
 +  
      redoMenuItem.setEnabled(false);
      redoMenuItem.setText(MessageManager.getString("action.redo"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_Y,
        }
      };
      addMenuActionAndAccelerator(keyStroke, redoMenuItem, al);
 +  
      wrapMenuItem.setText(MessageManager.getString("label.wrap"));
      wrapMenuItem.addActionListener(new ActionListener()
      {
          wrapMenuItem_actionPerformed(e);
        }
      });
 +  
      JMenuItem printMenuItem = new JMenuItem(
              MessageManager.getString("action.print"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_P,
        }
      };
      addMenuActionAndAccelerator(keyStroke, printMenuItem, al);
 +  
      renderGapsMenuItem
              .setText(MessageManager.getString("action.show_gaps"));
      renderGapsMenuItem.setState(true);
          renderGapsMenuItem_actionPerformed(e);
        }
      });
 +  
      JMenuItem findMenuItem = new JMenuItem(
              MessageManager.getString("action.find"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F,
              .setText(MessageManager.getString("label.show_database_refs"));
      showDbRefsMenuitem.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showDbRefs_actionPerformed(e);
        }
 +  
      });
      showNpFeatsMenuitem.setText(
              MessageManager.getString("label.show_non_positional_features"));
      showNpFeatsMenuitem.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showNpFeats_actionPerformed(e);
        }
 +  
      });
      showGroupConservation
              .setText(MessageManager.getString("label.group_conservation"));
      showGroupConservation.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showGroupConservation_actionPerformed(e);
        }
 +  
      });
  
      showGroupConsensus
              .setText(MessageManager.getString("label.group_consensus"));
      showGroupConsensus.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showGroupConsensus_actionPerformed(e);
        }
 +  
      });
      showConsensusHistogram.setText(
              MessageManager.getString("label.show_consensus_histogram"));
      showConsensusHistogram.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showConsensusHistogram_actionPerformed(e);
        }
 +  
      });
      showSequenceLogo
              .setText(MessageManager.getString("label.show_consensus_logo"));
      showSequenceLogo.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          showSequenceLogo_actionPerformed(e);
        }
 +  
      });
      normaliseSequenceLogo
              .setText(MessageManager.getString("label.norm_consensus_logo"));
      normaliseSequenceLogo.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          normaliseSequenceLogo_actionPerformed(e);
        }
 +  
      });
      applyAutoAnnotationSettings
              .setText(MessageManager.getString("label.apply_all_groups"));
          applyAutoAnnotationSettings_actionPerformed(e);
        }
      });
 +  
      ButtonGroup buttonGroup = new ButtonGroup();
      final JRadioButtonMenuItem showAutoFirst = new JRadioButtonMenuItem(
              MessageManager.getString("label.show_first"));
          sortAnnotations_actionPerformed();
        }
      });
 +  
      JMenuItem deleteGroups = new JMenuItem(
              MessageManager.getString("action.undefine_groups"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_U,
        }
      };
      addMenuActionAndAccelerator(keyStroke, deleteGroups, al);
 +  
      JMenuItem annotationColumn = new JMenuItem(
              MessageManager.getString("action.select_by_annotation"));
      annotationColumn.addActionListener(new ActionListener()
          annotationColumn_actionPerformed(e);
        }
      });
 +  
      JMenuItem createGroup = new JMenuItem(
              MessageManager.getString("action.create_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
        }
      };
      addMenuActionAndAccelerator(keyStroke, createGroup, al);
 +  
      JMenuItem unGroup = new JMenuItem(
              MessageManager.getString("action.remove_group"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G,
        }
      };
      addMenuActionAndAccelerator(keyStroke, unGroup, al);
 +  
      copy.setText(MessageManager.getString("action.copy"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_C,
 -            jalview.util.ShortcutKeyMaskExWrapper
 -                    .getMenuShortcutKeyMaskEx(),
 -            false);
 +            Platform.SHORTCUT_KEY_MASK, false);
  
      al = new ActionListener()
      {
        }
      };
      addMenuActionAndAccelerator(keyStroke, copy, al);
 +  
      cut.setText(MessageManager.getString("action.cut"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X,
 -            jalview.util.ShortcutKeyMaskExWrapper
 -                    .getMenuShortcutKeyMaskEx(),
 -            false);
 +            Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
        }
      };
      addMenuActionAndAccelerator(keyStroke, cut, al);
 +  
      JMenuItem delete = new JMenuItem(
              MessageManager.getString("action.delete"));
      delete.addActionListener(new ActionListener()
          delete_actionPerformed();
        }
      });
 +  
      pasteMenu.setText(MessageManager.getString("action.paste"));
      JMenuItem pasteNew = new JMenuItem(
              MessageManager.getString("label.to_new_alignment"));
        }
      };
      addMenuActionAndAccelerator(keyStroke, pasteNew, al);
 +  
      JMenuItem pasteThis = new JMenuItem(
              MessageManager.getString("label.to_this_alignment"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_V,
        }
      };
      addMenuActionAndAccelerator(keyStroke, pasteThis, al);
 +  
      JMenuItem createPNG = new JMenuItem("PNG");
      createPNG.addActionListener(new ActionListener()
      {
          createEPS(null);
        }
      });
 +  
      JMenuItem createSVG = new JMenuItem("SVG");
      createSVG.addActionListener(new ActionListener()
      {
          createSVG(null);
        }
      });
 +  
      JMenuItem loadTreeMenuItem = new JMenuItem(
              MessageManager.getString("label.load_associated_tree"));
      loadTreeMenuItem.setActionCommand(
          loadTreeMenuItem_actionPerformed(e);
        }
      });
 +  
      scaleAbove.setVisible(false);
      scaleAbove.setText(MessageManager.getString("action.scale_above"));
      scaleAbove.addActionListener(new ActionListener()
              .setText(MessageManager.getString("label.automatic_scrolling"));
      followHighlightMenuItem.addActionListener(new ActionListener()
      {
 +  
        @Override
        public void actionPerformed(ActionEvent e)
        {
          followHighlight_actionPerformed();
        }
 +  
      });
 +  
      sortByTreeMenu
              .setText(MessageManager.getString("action.by_tree_order"));
      sort.setText(MessageManager.getString("action.sort"));
      sort.add(sortByAnnotScore);
      sort.addMenuListener(new javax.swing.event.MenuListener()
      {
 +  
        @Override
        public void menuCanceled(MenuEvent e)
        {
        }
 +  
        @Override
        public void menuDeselected(MenuEvent e)
        {
        }
 +  
        @Override
        public void menuSelected(MenuEvent e)
        {
          showReverse_actionPerformed(true);
        }
      });
 +  
      JMenuItem extractScores = new JMenuItem(
              MessageManager.getString("label.extract_scores"));
      extractScores.addActionListener(new ActionListener()
      });
      extractScores.setVisible(true);
      // JBPNote: TODO: make gui for regex based score extraction
 +  
      // for show products actions see AlignFrame.canShowProducts
      showProducts.setText(MessageManager.getString("label.get_cross_refs"));
 +  
      runGroovy.setText(MessageManager.getString("label.run_groovy"));
      runGroovy.setToolTipText(
              MessageManager.getString("label.run_groovy_tip"));
          fetchSequence_actionPerformed();
        }
      });
 +  
      JMenuItem associatedData = new JMenuItem(
              MessageManager.getString("label.load_features_annotations"));
      associatedData.addActionListener(new ActionListener()
          listenToViewSelections_actionPerformed(e);
        }
      });
 +  
      JMenu addSequenceMenu = new JMenu(
              MessageManager.getString("label.add_sequences"));
      JMenuItem addFromFile = new JMenuItem(
          hiddenMarkers_actionPerformed(e);
        }
      });
 +  
      JMenuItem invertColSel = new JMenuItem(
              MessageManager.getString("action.invert_column_selection"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_I,
        }
      };
      addMenuActionAndAccelerator(keyStroke, invertColSel, al);
 +  
      showComplementMenuItem.setVisible(false);
      showComplementMenuItem.addActionListener(new ActionListener()
      {
          showComplement_actionPerformed(showComplementMenuItem.getState());
        }
      });
 +  
      tabbedPane.addChangeListener(new javax.swing.event.ChangeListener()
      {
        @Override
            tabbedPane_mousePressed(e);
          }
        }
 +  
        @Override
        public void mouseReleased(MouseEvent e)
        {
          tabbedPane_focusGained(e);
        }
      });
 +  
      JMenuItem save = new JMenuItem(MessageManager.getString("action.save"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_S,
 -            jalview.util.ShortcutKeyMaskExWrapper
 -                    .getMenuShortcutKeyMaskEx(),
 -            false);
 +            Platform.SHORTCUT_KEY_MASK, false);
      al = new ActionListener()
      {
        @Override
        }
      };
      addMenuActionAndAccelerator(keyStroke, save, al);
 +  
      reload.setEnabled(false);
      reload.setText(MessageManager.getString("action.reload"));
      reload.addActionListener(new ActionListener()
          reload_actionPerformed(e);
        }
      });
 +  
      JMenuItem newView = new JMenuItem(
              MessageManager.getString("action.new_view"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_T,
        }
      };
      addMenuActionAndAccelerator(keyStroke, newView, al);
 +  
      tabbedPane.setToolTipText("<html><i>"
              + MessageManager.getString("label.rename_tab_eXpand_reGroup")
              + "</i></html>");
 +  
      formatMenu.setText(MessageManager.getString("action.format"));
      JMenu selectMenu = new JMenu(MessageManager.getString("action.select"));
  
          idRightAlign_actionPerformed(e);
        }
      });
 +  
      gatherViews.setEnabled(false);
      gatherViews.setText(MessageManager.getString("action.gather_views"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_G, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, gatherViews, al);
 +  
      expandViews.setEnabled(false);
      expandViews.setText(MessageManager.getString("action.expand_views"));
      keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_X, 0, false);
        }
      };
      addMenuActionAndAccelerator(keyStroke, expandViews, al);
 +  
      JMenuItem pageSetup = new JMenuItem(
              MessageManager.getString("action.page_setup"));
      pageSetup.addActionListener(new ActionListener()
              MessageManager.getString("label.sequence_id_tooltip"));
      JMenu autoAnnMenu = new JMenu(
              MessageManager.getString("label.autocalculated_annotation"));
 +  
      JMenu exportImageMenu = new JMenu(
              MessageManager.getString("label.export_image"));
      JMenu fileMenu = new JMenu(MessageManager.getString("action.file"));
      alignFrameMenuBar.add(formatMenu);
      alignFrameMenuBar.add(colourMenu);
      alignFrameMenuBar.add(calculateMenu);
 +    alignFrameMenuBar.add(webService);
      if (!Platform.isJS())
      {
 -      alignFrameMenuBar.add(webService);
 +      alignFrameMenuBar.add(hmmerMenu);
      }
 +  
      fileMenu.add(fetchSequence);
      fileMenu.add(addSequenceMenu);
      fileMenu.add(reload);
      }
      fileMenu.addSeparator();
      fileMenu.add(closeMenuItem);
 +  
      pasteMenu.add(pasteNew);
      pasteMenu.add(pasteThis);
      editMenu.add(undoMenuItem);
      // editMenu.add(justifyRightMenuItem);
      // editMenu.addSeparator();
      editMenu.add(padGapsMenuitem);
 +    editMenu.addSeparator();
 +    editMenu.add(filterByEValue);
 +    editMenu.add(filterByScore);
 +  
      showMenu.add(showAllColumns);
      showMenu.add(showAllSeqs);
      showMenu.add(showAllhidden);
      viewMenu.add(alignmentProperties);
      viewMenu.addSeparator();
      viewMenu.add(overviewMenuItem);
 +  
      annotationsMenu.add(annotationPanelMenuItem);
      annotationsMenu.addSeparator();
      annotationsMenu.add(showAllAlAnnotations);
      this.getContentPane().add(statusPanel, java.awt.BorderLayout.SOUTH);
      statusPanel.add(statusBar, null);
      this.getContentPane().add(tabbedPane, java.awt.BorderLayout.CENTER);
 +  
      formatMenu.add(font);
      formatMenu.addSeparator();
      formatMenu.add(wrapMenuItem);
    {
    }
  
 +  protected void sortEValueMenuItem_actionPerformed(ActionEvent e)
 +  {
 +  }
 +
 +  protected void sortBitScoreMenuItem_actionPerformed(ActionEvent e)
 +  {
 +  }
    protected void removeRedundancyMenuItem_actionPerformed(ActionEvent e)
    {
    }
    {
    }
  
 +  protected void hmmBuild_actionPerformed(boolean withDefaults)
 +  {
 +  }
 +
 +  protected void hmmSearch_actionPerformed(boolean withDefaults)
 +  {
 +  }
 +
 +  protected void jackhmmer_actionPerformed(boolean b)
 +  {
 +  }
 +
 +  protected void addDatabase_actionPerformed()
 +          throws FileFormatException, IOException
 +  {
 +  }
 +
 +  protected void hmmAlign_actionPerformed(boolean withDefaults)
 +  {
 +  }
    public void createPNG(java.io.File f)
    {
    }
    {
    }
  
 +  protected void filterByEValue_actionPerformed()
 +  {
 +  }
 +
 +  protected void filterByScore_actionPerformed()
 +  {
 +  }
    protected void scaleRight_actionPerformed(ActionEvent e)
    {
    }
    protected void showComplement_actionPerformed(boolean complement)
    {
    }
 +  
  }
   */
  package jalview.jbgui;
  
 -import jalview.gui.JvSwingUtils;
 -import jalview.util.MessageManager;
  import java.awt.BorderLayout;
  import java.awt.Font;
 -import java.awt.Toolkit;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.MouseEvent;
@@@ -35,10 -39,6 +36,9 @@@ import javax.swing.JPanel
  import javax.swing.JScrollPane;
  import javax.swing.JTextArea;
  
 +import jalview.gui.JvSwingUtils;
 +import jalview.util.MessageManager;
 +import jalview.util.Platform;
  /**
   * DOCUMENT ME!
   * 
   */
  package jalview.jbgui;
  
- import jalview.api.AlignmentViewPanel;
- import jalview.io.FileFormatException;
- import jalview.util.MessageManager;
- import jalview.util.Platform;
 +
  import java.awt.FlowLayout;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
@@@ -35,6 -30,12 +31,11 @@@ import javax.swing.JMenu
  import javax.swing.JMenuBar;
  import javax.swing.JMenuItem;
  
+ import jalview.api.AlignmentViewPanel;
+ import jalview.bin.Cache;
+ import jalview.io.FileFormatException;
+ import jalview.util.MessageManager;
+ import jalview.util.Platform;
 -
  /**
   * DOCUMENT ME!
   * 
@@@ -139,8 -140,27 +140,28 @@@ public class GDesktop extends JFram
     */
    private void jbInit() throws Exception
    {
+     boolean apqHandlersSet = false;
+     /**
+      * APQHandlers sets handlers for About, Preferences and Quit actions
+      * peculiar to macOS's application menu. APQHandlers will check to see if a
+      * handler is supported before setting it.
+      */
+     try
+     {
++      // TODO: if (!Platform.isJS()
+       apqHandlersSet = APQHandlers.setAPQHandlers(this);
+     } catch (Exception e)
+     {
+       System.out.println("Cannot set APQHandlers");
+       // e.printStackTrace();
+     } catch (Throwable t)
+     {
+       jalview.bin.Console
+               .warn("Error setting APQHandlers: " + t.toString());
+       jalview.bin.Console.trace(Cache.getStackTraceString(t));
+     }
  
 -    setName("jalview-desktop");
 +    setName(Platform.getAppID("desktop"));
      FileMenu.setText(MessageManager.getString("action.file"));
      HelpMenu.setText(MessageManager.getString("action.help"));
      inputLocalFileMenuItem
   */
  package jalview.jbgui;
  
 +import jalview.bin.Cache;
++import jalview.bin.Console;
++import jalview.bin.MemorySetting;
 +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 jalview.util.StringUtils;
 +
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Component;
@@@ -93,6 -76,25 +97,8 @@@ import javax.swing.event.DocumentListen
  import javax.swing.table.TableCellEditor;
  import javax.swing.table.TableCellRenderer;
  
 -import jalview.bin.Cache;
 -import jalview.bin.Console;
 -import jalview.bin.MemorySetting;
 -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 jalview.util.StringUtils;
++import net.miginfocom.swing.MigLayout;
  /**
   * Base class for the Preferences panel.
   * 
@@@ -403,6 -381,31 +408,30 @@@ public class GPreferences extends JPane
    private final JTabbedPane tabbedPane = new JTabbedPane();
  
    private JLabel messageLabel = new JLabel("", JLabel.CENTER);
+   /*
+    * Startup tab components
+    */
+   protected JCheckBox customiseMemorySetting = new JCheckBox();
+   protected JLabel exampleMemoryLabel = new JLabel();
+   protected JTextArea exampleMemoryMessageTextArea = new JTextArea();
+   protected JLabel maxMemoryLabel = new JLabel();
+   protected JLabel jvmMemoryPercentLabel = new JLabel();
+   protected JSlider jvmMemoryPercentSlider = new JSlider();
+   protected JLabel jvmMemoryPercentDisplay = new JLabel();
+   protected JLabel jvmMemoryMaxLabel = new JLabel();
+   protected JTextField jvmMemoryMaxTextField = new JTextField(null, 8);
+   protected JComboBox<Object> lafCombo = new JComboBox<>();
 -
    /**
     * Creates a new GPreferences object.
     */
      tabbedPane.add(initEditingTab(),
              MessageManager.getString("label.editing"));
  
-     tabbedPane.add(initHMMERTab(), MessageManager.getString("label.hmmer"));
 -    tabbedPane.add(initStartupTab(),
 -            MessageManager.getString("label.startup"));
 -
      /*
       * See WsPreferences for the real work of configuring this tab.
       */
      if (!Platform.isJS())
      {
++      tabbedPane.add(initHMMERTab(), MessageManager.getString("label.hmmer"));
++      tabbedPane.add(initStartupTab(),
++              MessageManager.getString("label.startup"));
        wsTab.setLayout(new BorderLayout());
        tabbedPane.add(wsTab, MessageManager.getString("label.web_services"));
      }
      connectTab = new JPanel();
      connectTab.setLayout(new GridBagLayout());
  
-     // Label for browser text box
-     JLabel browserLabel = new JLabel();
-     browserLabel.setFont(LABEL_FONT);
-     browserLabel.setHorizontalAlignment(SwingConstants.TRAILING);
-     browserLabel.setText(
-             MessageManager.getString("label.default_browser_unix"));
-     defaultBrowser.setFont(LABEL_FONT);
-     defaultBrowser.setText("");
-     final String tooltip = JvSwingUtils.wrapTooltip(true,
-             MessageManager.getString("label.double_click_to_browse"));
-     defaultBrowser.setToolTipText(tooltip);
-     defaultBrowser.addMouseListener(new MouseAdapter()
-     {
-       @Override
-       public void mouseClicked(MouseEvent e)
-       {
-         if (e.getClickCount() > 1)
-         {
-           defaultBrowser_mouseClicked(e);
-         }
-       }
-     });
 +
      JPanel proxyPanel = initConnTabProxyPanel();
      initConnTabCheckboxes();
  
        {
          if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
          {
-           structureViewer_actionPerformed(
-                   (String) structViewer.getSelectedItem());
-         }
-       });
-       structureTab.add(structViewer);
-       ypos += lineSpacing;
-       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.viewer_path_tip"));
-       structureViewerPath.setToolTipText(tooltip);
-       structureViewerPath.setBounds(new Rectangle(190, ypos, 290, height));
-       structureViewerPath.addMouseListener(new MouseAdapter()
-       {
-         if (structureViewerPath.isEnabled() && e.getClickCount() == 2)
-         {
--          String chosen = openFileChooser();
++          String chosen = openFileChooser(false);
            if (chosen != null)
            {
              structureViewerPath.setText(chosen);
                true);
      }
  
 +    if (forFolder)
 +    {
 +      chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
 +    }
      // chooser.setFileView(new JalviewFileView());
      chooser.setDialogTitle(
              MessageManager.getString("label.open_local_file"));
      visualTab.add(fontNameCB);
      visualTab.add(fontSizeCB);
      visualTab.add(fontStyleCB);
 +    
      if (Platform.isJS())
      {
        startupCheckbox.setVisible(false);
        startupFileTextfield.setVisible(false);
      }
 +    
      return visualTab;
    }
  
      updateBackupFilesExampleLabel();
    }
  
+   /*
+    * Load the saved Memory settings
+    */
+   protected void loadLastSavedMemorySettings()
+   {
+     customiseMemorySetting.setSelected(
+             Cache.getDefault(MemorySetting.CUSTOMISED_SETTINGS, false));
+     jvmMemoryPercentSlider
+             .setValue(Cache.getDefault(MemorySetting.MEMORY_JVMMEMPC, 90));
+     jvmMemoryMaxTextField.setText(
+             Cache.getDefault(MemorySetting.MEMORY_JVMMEMMAX, "32g"));
+   }
 -
    private boolean warnAboutSuffixReverseChange()
    {
      BackupFilesPresetEntry bfpe = BackupFilesPresetEntry
      }
  
    }
 +
 +  protected void validateHmmerPath()
 +  {
 +  }
 +
 +  protected void validateCygwinPath()
 +  {
 +  }
 +
 +  /**
 +   * A helper method to add a panel containing a label and a component to a
 +   * panel
 +   * 
 +   * @param panel
 +   * @param tooltip
 +   * @param label
 +   * @param valBox
 +   */
 +  protected static void addtoLayout(JPanel panel, String tooltip,
 +          JComponent label, JComponent valBox)
 +  {
 +    JPanel laypanel = new JPanel(new GridLayout(1, 2));
 +    JPanel labPanel = new JPanel(new BorderLayout());
 +    JPanel valPanel = new JPanel();
 +    labPanel.setBounds(new Rectangle(7, 7, 158, 23));
 +    valPanel.setBounds(new Rectangle(172, 7, 270, 23));
 +    labPanel.add(label, BorderLayout.WEST);
 +    valPanel.add(valBox);
 +    laypanel.add(labPanel);
 +    laypanel.add(valPanel);
 +    valPanel.setToolTipText(tooltip);
 +    labPanel.setToolTipText(tooltip);
 +    valBox.setToolTipText(tooltip);
 +    panel.add(laypanel);
 +    panel.validate();
 +  }
  }
   */
  package jalview.project;
  
++
  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.Locale;
 -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;
@@@ -155,56 -201,6 +158,57 @@@ import jalview.xml.binding.jalview.Sequ
  import jalview.xml.binding.jalview.ThresholdType;
  import jalview.xml.binding.jalview.VAMSAS;
  
 +import java.awt.Color;
 +import java.awt.Dimension;
 +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.InputStream;
 +import java.io.InputStreamReader;
++import java.io.OutputStream;
 +import java.io.OutputStreamWriter;
 +import java.io.PrintWriter;
 +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.Locale;
 +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.
   * 
  public class Jalview2XML
  {
  
--  // BH 2018 we add the .jvp binary extension to J2S so that
--  // it will declare that binary when we do the file save from the browser
-   static
-   {
-     Platform.addJ2SBinaryType(".jvp?");
-   }
-   private static final String VIEWER_PREFIX = "viewer_";
-   private static final String RNA_PREFIX = "rna_";
++    // BH 2018 we add the .jvp binary extension to J2S so that
++    // it will declare that binary when we do the file save from the browser
  
-   private static final String HMMER_PREFIX = "hmmer_";
-   private static final String UTF_8 = "UTF-8";
 -  static
 -  {
 -    Platform.addJ2SBinaryType(".jvp?");
 -  }
++    static
++    {
++      Platform.addJ2SBinaryType(".jvp?");
++    }
  
-   /**
-    * prefix for recovering datasets for alignments with multiple views where
-    * non-existent dataset IDs were written for some views
-    */
-   private static final String UNIQSEQSETID = "uniqueSeqSetId.";
 -  private static final String VIEWER_PREFIX = "viewer_";
++    private static final String VIEWER_PREFIX = "viewer_";
  
-   // use this with nextCounter() to make unique names for entities
-   private int counter = 0;
 -  private static final String RNA_PREFIX = "rna_";
++    private static final String RNA_PREFIX = "rna_";
  
-   /*
-    * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
-    * of sequence objects are created.
-    */
-   IdentityHashMap<SequenceI, String> seqsToIds = null;
 -  private static final String UTF_8 = "UTF-8";
++    private static final String HMMER_PREFIX = "hmmer_";
++    private static final String UTF_8 = "UTF-8";
  
--  /**
-    * jalview XML Sequence ID to jalview sequence object reference (both dataset
-    * and alignment sequences. Populated as XML reps of sequence objects are
-    * created.)
-    */
-   Map<String, SequenceI> seqRefIds = null;
 -   * prefix for recovering datasets for alignments with multiple views where
 -   * non-existent dataset IDs were written for some views
 -   */
 -  private static final String UNIQSEQSETID = "uniqueSeqSetId.";
++    /**
++     * prefix for recovering datasets for alignments with multiple views where
++     * non-existent dataset IDs were written for some views
++     */
++    private static final String UNIQSEQSETID = "uniqueSeqSetId.";
  
-   Map<String, SequenceI> incompleteSeqs = null;
 -  // use this with nextCounter() to make unique names for entities
 -  private int counter = 0;
++    // use this with nextCounter() to make unique names for entities
++    private int counter = 0;
  
-   List<SeqFref> frefedSequence = null;
 -  /*
 -   * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
 -   * of sequence objects are created.
 -   */
 -  IdentityHashMap<SequenceI, String> seqsToIds = null;
++    /*
++     * SequenceI reference -> XML ID string in jalview XML. Populated as XML reps
++     * of sequence objects are created.
++     */
++    IdentityHashMap<SequenceI, String> seqsToIds = null;
  
-   boolean raiseGUI = true; // whether errors are raised in dialog boxes or not
 -  /**
 -   * jalview XML Sequence ID to jalview sequence object reference (both dataset
 -   * and alignment sequences. Populated as XML reps of sequence objects are
 -   * created.)
 -   */
 -  Map<String, SequenceI> seqRefIds = null;
++    /**
++     * jalview XML Sequence ID to jalview sequence object reference (both dataset
++     * and alignment sequences. Populated as XML reps of sequence objects are
++     * created.)
++     */
++    Map<String, SequenceI> seqRefIds = null;
  
-   /*
-    * Map of reconstructed AlignFrame objects that appear to have come from
-    * SplitFrame objects (have a dna/protein complement view).
-    */
-   private Map<Viewport, AlignFrame> splitFrameCandidates = new HashMap<>();
 -  Map<String, SequenceI> incompleteSeqs = null;
++    Map<String, SequenceI> incompleteSeqs = null;
  
-   /*
-    * Map from displayed rna structure models to their saved session state jar
-    * entry names
-    */
-   private Map<RnaModel, String> rnaSessions = new HashMap<>();
 -  List<SeqFref> frefedSequence = null;
++    List<SeqFref> frefedSequence = null;
  
-   /**
-    * contains last error message (if any) encountered by XML loader.
-    */
-   String errorMessage = null;
 -  boolean raiseGUI = true; // whether errors are raised in dialog boxes or not
++    boolean raiseGUI = true; // whether errors are raised in dialog boxes or not
  
-   /**
-    * flag to control whether the Jalview2XML_V1 parser should be deferred to if
-    * exceptions are raised during project XML parsing
-    */
-   public boolean attemptversion1parse = false;
 -  /*
 -   * Map of reconstructed AlignFrame objects that appear to have come from
 -   * SplitFrame objects (have a dna/protein complement view).
 -   */
 -  private Map<Viewport, AlignFrame> splitFrameCandidates = new HashMap<>();
++    /*
++     * Map of reconstructed AlignFrame objects that appear to have come from
++     * SplitFrame objects (have a dna/protein complement view).
++     */
++    private Map<Viewport, AlignFrame> splitFrameCandidates = new HashMap<>();
  
--  /*
-    * JalviewJS only -- to allow read file bytes to be saved in the
-    * created AlignFrame, allowing File | Reload of a project file to work
-    * 
-    * BH 2019 JAL-3436
-    */
-   private File jarFile;
 -   * Map from displayed rna structure models to their saved session state jar
 -   * entry names
 -   */
 -  private Map<RnaModel, String> rnaSessions = new HashMap<>();
++    /*
++     * Map from displayed rna structure models to their saved session state jar
++     * entry names
++     */
++    private Map<RnaModel, String> rnaSessions = new HashMap<>();
  
--  /**
--   * A helper method for safely using the value of an optional attribute that
--   * may be null if not present in the XML. Answers the boolean value, or false
--   * if null.
--   * 
--   * @param b
--   * @return
--   */
--  public static boolean safeBoolean(Boolean b)
--  {
--    return b == null ? false : b.booleanValue();
--  }
++    /**
++     * contains last error message (if any) encountered by XML loader.
++     */
++    String errorMessage = null;
  
--  /**
--   * A helper method for safely using the value of an optional attribute that
--   * may be null if not present in the XML. Answers the integer value, or zero
--   * if null.
--   * 
--   * @param i
--   * @return
--   */
--  public static int safeInt(Integer i)
--  {
--    return i == null ? 0 : i.intValue();
--  }
++    /**
++     * flag to control whether the Jalview2XML_V1 parser should be deferred to if
++     * exceptions are raised during project XML parsing
++     */
++    public boolean attemptversion1parse = false;
  
--  /**
--   * A helper method for safely using the value of an optional attribute that
--   * may be null if not present in the XML. Answers the float value, or zero if
--   * null.
--   * 
--   * @param f
--   * @return
--   */
--  public static float safeFloat(Float f)
--  {
--    return f == null ? 0f : f.floatValue();
--  }
++    /*
++     * JalviewJS only -- to allow read file bytes to be saved in the
++     * created AlignFrame, allowing File | Reload of a project file to work
++     * 
++     * BH 2019 JAL-3436
++     */
++    private File jarFile;
  
--  /**
--   * create/return unique hash string for sq
--   * 
--   * @param sq
--   * @return new or existing unique string for sq
--   */
--  String seqHash(SequenceI sq)
--  {
--    if (seqsToIds == null)
++    /**
++     * A helper method for safely using the value of an optional attribute that
++     * may be null if not present in the XML. Answers the boolean value, or false
++     * if null.
++     * 
++     * @param b
++     * @return
++     */
++    public static boolean safeBoolean(Boolean b)
      {
--      initSeqRefs();
++      return b == null ? false : b.booleanValue();
      }
--    if (seqsToIds.containsKey(sq))
++
++    /**
++     * A helper method for safely using the value of an optional attribute that
++     * may be null if not present in the XML. Answers the integer value, or zero
++     * if null.
++     * 
++     * @param i
++     * @return
++     */
++    public static int safeInt(Integer i)
      {
--      return seqsToIds.get(sq);
++      return i == null ? 0 : i.intValue();
      }
--    else
++
++    /**
++     * A helper method for safely using the value of an optional attribute that
++     * may be null if not present in the XML. Answers the float value, or zero if
++     * null.
++     * 
++     * @param f
++     * @return
++     */
++    public static float safeFloat(Float f)
      {
--      // create sequential key
--      String key = "sq" + (seqsToIds.size() + 1);
--      key = makeHashCode(sq, key); // check we don't have an external reference
--      // for it already.
--      seqsToIds.put(sq, key);
--      return key;
++      return f == null ? 0f : f.floatValue();
      }
--  }
  
--  void initSeqRefs()
--  {
--    if (seqsToIds == null)
++    /**
++     * create/return unique hash string for sq
++     * 
++     * @param sq
++     * @return new or existing unique string for sq
++     */
++    String seqHash(SequenceI sq)
      {
--      seqsToIds = new IdentityHashMap<>();
++      if (seqsToIds == null)
++          {
++              initSeqRefs();
++          }
++      if (seqsToIds.containsKey(sq))
++          {
++              return seqsToIds.get(sq);
++          }
++      else
++          {
++              // create sequential key
++              String key = "sq" + (seqsToIds.size() + 1);
++              key = makeHashCode(sq, key); // check we don't have an external reference
++              // for it already.
++              seqsToIds.put(sq, key);
++              return key;
++          }
      }
--    if (seqRefIds == null)
++
++    void initSeqRefs()
      {
--      seqRefIds = new HashMap<>();
++      if (seqsToIds == null)
++          {
++              seqsToIds = new IdentityHashMap<>();
++          }
++      if (seqRefIds == null)
++          {
++              seqRefIds = new HashMap<>();
++          }
++      if (incompleteSeqs == null)
++          {
++              incompleteSeqs = new HashMap<>();
++          }
++      if (frefedSequence == null)
++          {
++              frefedSequence = new ArrayList<>();
++          }
      }
--    if (incompleteSeqs == null)
++
++    public Jalview2XML()
      {
--      incompleteSeqs = new HashMap<>();
      }
--    if (frefedSequence == null)
++
++    public Jalview2XML(boolean raiseGUI)
      {
--      frefedSequence = new ArrayList<>();
++      this.raiseGUI = raiseGUI;
      }
--  }
  
--  public Jalview2XML()
--  {
--  }
++    /**
++     * base class for resolving forward references to sequences by their ID
++     * 
++     * @author jprocter
++     *
++     */
++    abstract class SeqFref
++    {
++      String sref;
++
++      String type;
++
++      public SeqFref(String _sref, String type)
++      {
++          sref = _sref;
++          this.type = type;
++      }
++
++      public String getSref()
++      {
++          return sref;
++      }
++
++      public SequenceI getSrefSeq()
++      {
++          return seqRefIds.get(sref);
++      }
++
++      public boolean isResolvable()
++      {
++          return seqRefIds.get(sref) != null;
++      }
++
++      public SequenceI getSrefDatasetSeq()
++      {
++          SequenceI sq = seqRefIds.get(sref);
++          if (sq != null)
++              {
++                  while (sq.getDatasetSequence() != null)
++                      {
++                          sq = sq.getDatasetSequence();
++                      }
++              }
++          return sq;
++      }
++
++      /**
++       * @return true if the forward reference was fully resolved
++       */
++      abstract boolean resolve();
++
++      @Override
++      public String toString()
++      {
++          return type + " reference to " + sref;
++      }
++    }
  
--  public Jalview2XML(boolean raiseGUI)
--  {
--    this.raiseGUI = raiseGUI;
--  }
++    /**
++     * create forward reference for a mapping
++     * 
++     * @param sref
++     * @param _jmap
++     * @return
++     */
++    protected SeqFref newMappingRef(final String sref,
++                                  final jalview.datamodel.Mapping _jmap)
++    {
++      SeqFref fref = new SeqFref(sref, "Mapping")
++          {
++              public jalview.datamodel.Mapping jmap = _jmap;
++
++              @Override
++              boolean resolve()
++              {
++                  SequenceI seq = getSrefDatasetSeq();
++                  if (seq == null)
++                      {
++                          return false;
++                      }
++                  jmap.setTo(seq);
++                  return true;
++              }
++          };
++      return fref;
++    }
++
++    protected SeqFref newAlcodMapRef(final String sref,
++                                   final AlignedCodonFrame _cf,
++                                   final jalview.datamodel.Mapping _jmap)
++    {
++
++      SeqFref fref = new SeqFref(sref, "Codon Frame")
++          {
++              AlignedCodonFrame cf = _cf;
++
++              public jalview.datamodel.Mapping mp = _jmap;
++
++              @Override
++              public boolean isResolvable()
++              {
++                  return super.isResolvable() && mp.getTo() != null;
++              }
++
++              @Override
++              boolean resolve()
++              {
++                  SequenceI seq = getSrefDatasetSeq();
++                  if (seq == null)
++                      {
++                          return false;
++                      }
++                  cf.addMap(seq, mp.getTo(), mp.getMap());
++                  return true;
++              }
++          };
++      return fref;
++    }
++
++    protected void resolveFrefedSequences()
++    {
++      Iterator<SeqFref> nextFref = frefedSequence.iterator();
++      int toresolve = frefedSequence.size();
++      int unresolved = 0, failedtoresolve = 0;
++      while (nextFref.hasNext())
++          {
++              SeqFref ref = nextFref.next();
++              if (ref.isResolvable())
++                  {
++                      try
++                          {
++                              if (ref.resolve())
++                                  {
++                                      nextFref.remove();
++                                  }
++                              else
++                                  {
++                                      failedtoresolve++;
++                                  }
++                          } catch (Exception x)
++                          {
++                              System.err.println(
++                                                 "IMPLEMENTATION ERROR: Failed to resolve forward reference for sequence "
++                                                 + ref.getSref());
++                              x.printStackTrace();
++                              failedtoresolve++;
++                          }
++                  }
++              else
++                  {
++                      unresolved++;
++                  }
++          }
++      if (unresolved > 0)
++          {
++              System.err.println("Jalview Project Import: There were " + unresolved
++                                 + " forward references left unresolved on the stack.");
++          }
++      if (failedtoresolve > 0)
++          {
++              System.err.println("SERIOUS! " + failedtoresolve
++                                 + " resolvable forward references failed to resolve.");
++          }
++      if (incompleteSeqs != null && incompleteSeqs.size() > 0)
++          {
++              System.err.println(
++                                 "Jalview Project Import: There are " + incompleteSeqs.size()
++                                 + " sequences which may have incomplete metadata.");
++              if (incompleteSeqs.size() < 10)
++                  {
++                      for (SequenceI s : incompleteSeqs.values())
++                          {
++                              System.err.println(s.toString());
++                          }
++                  }
++              else
++                  {
++                      System.err.println(
++                                         "Too many to report. Skipping output of incomplete sequences.");
++                  }
++          }
++    }
  
--  /**
--   * base class for resolving forward references to sequences by their ID
--   * 
--   * @author jprocter
--   *
--   */
--  abstract class SeqFref
--  {
--    String sref;
++    /**
++     * This maintains a map of viewports, the key being the seqSetId. Important to
++     * set historyItem and redoList for multiple views
++     */
++    Map<String, AlignViewport> viewportsAdded = new HashMap<>();
  
--    String type;
++    Map<String, AlignmentAnnotation> annotationIds = new HashMap<>();
  
--    public SeqFref(String _sref, String type)
--    {
--      sref = _sref;
--      this.type = type;
++    String uniqueSetSuffix = "";
++
++    /**
++     * List of pdbfiles added to Jar
++     */
++    List<String> pdbfiles = null;
++
++    // SAVES SEVERAL ALIGNMENT WINDOWS TO SAME JARFILE
++    public void saveState(File statefile)
++    {
++      FileOutputStream fos = null;
++
++      try
++          {
++
++              fos = new FileOutputStream(statefile);
++
++              JarOutputStream jout = new JarOutputStream(fos);
++              saveState(jout);
++              fos.close();
++
++          } catch (Exception e)
++          {
++              Console.error("Couln't write Jalview state to " + statefile, e);
++              // TODO: inform user of the problem - they need to know if their data was
++              // not saved !
++              if (errorMessage == null)
++                  {
++                      errorMessage = "Did't write Jalview Archive to output file '"
++                          + statefile + "' - See console error log for details";
++                  }
++              else
++                  {
++                      errorMessage += "(Didn't write Jalview Archive to output file '"
++                          + statefile + ")";
++                  }
++              e.printStackTrace();
++          } finally
++          {
++              if (fos != null)
++                  {
++                      try
++                          {
++                              fos.close();
++                          } catch (IOException e)
++                          {
++                              // ignore
++                          }
++                  }
++          }
++      reportErrors();
      }
  
--    public String getSref()
++    /**
++     * Writes a jalview project archive to the given Jar output stream.
++     * 
++     * @param jout
++     */
++    public void saveState(JarOutputStream jout)
      {
--      return sref;
++      AlignFrame[] frames = Desktop.getAlignFrames();
++
++      if (frames == null)
++          {
++              return;
++          }
++      saveAllFrames(Arrays.asList(frames), jout);
      }
  
--    public SequenceI getSrefSeq()
--    {
--      return seqRefIds.get(sref);
++    /**
++     * core method for storing state for a set of AlignFrames.
++     * 
++     * @param frames
++     *          - frames involving all data to be exported (including those
++     *          contained in splitframes, though not the split frames themselves)
++     * @param jout
++     *          - project output stream
++     */
++    private void saveAllFrames(List<AlignFrame> frames, JarOutputStream jout)
++    {
++      Hashtable<String, AlignFrame> dsses = new Hashtable<>();
++
++      /*
++       * ensure cached data is clear before starting
++       */
++      // todo tidy up seqRefIds, seqsToIds initialisation / reset
++      rnaSessions.clear();
++      splitFrameCandidates.clear();
++
++      try
++          {
++
++              // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
++              // //////////////////////////////////////////////////
++
++              List<String> shortNames = new ArrayList<>();
++              List<String> viewIds = new ArrayList<>();
++
++              // REVERSE ORDER
++              for (int i = frames.size() - 1; i > -1; i--)
++                  {
++                      AlignFrame af = frames.get(i);
++                      AlignViewport vp = af.getViewport();
++                      // skip ?
++                      if (skipList != null && skipList
++                          .containsKey(vp.getSequenceSetId()))
++                          {
++                              continue;
++                          }
++
++                      String shortName = makeFilename(af, shortNames);
++
++                      AlignmentI alignment = vp.getAlignment();
++                      List<? extends AlignmentViewPanel> panels = af.getAlignPanels();
++                      int apSize = panels.size();
++
++                      for (int ap = 0; ap < apSize; ap++)
++                          {
++                              AlignmentPanel apanel = (AlignmentPanel) panels.get(ap);
++                              String fileName = apSize == 1 ? shortName : ap + shortName;
++                              if (!fileName.endsWith(".xml"))
++                                  {
++                                      fileName = fileName + ".xml";
++                                  }
++
++                              saveState(apanel, fileName, jout, viewIds);
++
++                          }
++                      if (apSize > 0)
++                          {
++                              // BH moved next bit out of inner loop, not that it really matters.
++                              // so we are testing to make sure we actually have an alignment,
++                              // apparently.
++                              String dssid = getDatasetIdRef(alignment.getDataset());
++                              if (!dsses.containsKey(dssid))
++                                  {
++                                      // We have not already covered this data by reference from another
++                                      // frame.
++                                      dsses.put(dssid, af);
++                                  }
++                          }
++                  }
++
++              writeDatasetFor(dsses, "" + jout.hashCode() + " " + uniqueSetSuffix,
++                              jout);
++
++              try
++                  {
++                      jout.flush();
++                  } catch (Exception foo)
++                  {
++                  }
++              jout.close();
++          } catch (Exception ex)
++          {
++              // TODO: inform user of the problem - they need to know if their data was
++              // not saved !
++              if (errorMessage == null)
++                  {
++                      errorMessage = "Couldn't write Jalview Archive - see error output for details";
++                  }
++              ex.printStackTrace();
++          }
      }
  
--    public boolean isResolvable()
--    {
--      return seqRefIds.get(sref) != null;
++    /**
++     * Generates a distinct file name, based on the title of the AlignFrame, by
++     * appending _n for increasing n until an unused name is generated. The new
++     * name (without its extension) is added to the list.
++     * 
++     * @param af
++     * @param namesUsed
++     * @return the generated name, with .xml extension
++     */
++    protected String makeFilename(AlignFrame af, List<String> namesUsed)
++    {
++      String shortName = af.getTitle();
++
++      if (shortName.indexOf(File.separatorChar) > -1)
++          {
++              shortName = shortName
++                  .substring(shortName.lastIndexOf(File.separatorChar) + 1);
++          }
++
++      int count = 1;
++
++      while (namesUsed.contains(shortName))
++          {
++              if (shortName.endsWith("_" + (count - 1)))
++                  {
++                      shortName = shortName.substring(0, shortName.lastIndexOf("_"));
++                  }
++
++              shortName = shortName.concat("_" + count);
++              count++;
++          }
++
++      namesUsed.add(shortName);
++
++      if (!shortName.endsWith(".xml"))
++          {
++              shortName = shortName + ".xml";
++          }
++      return shortName;
++    }
++
++    // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
++    public boolean saveAlignment(AlignFrame af, String jarFile,
++                               String fileName)
++    {
++      try
++          {
++              // create backupfiles object and get new temp filename destination
++              boolean doBackup = BackupFiles.getEnabled();
++              BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
++              FileOutputStream fos = new FileOutputStream(doBackup ? 
++                                                          backupfiles.getTempFilePath() : jarFile);
++
++              JarOutputStream jout = new JarOutputStream(fos);
++              List<AlignFrame> frames = new ArrayList<>();
++
++              // resolve splitframes
++              if (af.getViewport().getCodingComplement() != null)
++                  {
++                      frames = ((SplitFrame) af.getSplitViewContainer()).getAlignFrames();
++                  }
++              else
++                  {
++                      frames.add(af);
++                  }
++              saveAllFrames(frames, jout);
++              try
++                  {
++                      jout.flush();
++                  } catch (Exception foo)
++                  {
++                  }
++              jout.close();
++              boolean success = true;
++
++              if (doBackup)
++                  {
++                      backupfiles.setWriteSuccess(success);
++                      success = backupfiles.rollBackupsAndRenameTempFile();
++                  }
++
++              return success;
++          } catch (Exception ex)
++          {
++              errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
++              ex.printStackTrace();
++              return false;
++          }
      }
  
--    public SequenceI getSrefDatasetSeq()
++    /**
++     * Each AlignFrame has a single data set associated with it. Note that none of
++     * these frames are split frames, because Desktop.getAlignFrames() collects
++     * top and bottom separately here.
++     * 
++     * @param dsses
++     * @param fileName
++     * @param jout
++     */
++    private void writeDatasetFor(Hashtable<String, AlignFrame> dsses,
++                               String fileName, JarOutputStream jout)
      {
--      SequenceI sq = seqRefIds.get(sref);
--      if (sq != null)
--      {
--        while (sq.getDatasetSequence() != null)
--        {
--          sq = sq.getDatasetSequence();
--        }
--      }
--      return sq;
++
++      // Note that in saveAllFrames we have associated each specific dataset to
++      // ONE of its associated frames.
++      for (String dssids : dsses.keySet())
++          {
++              AlignFrame _af = dsses.get(dssids);
++              String jfileName = fileName + " Dataset for " + _af.getTitle();
++              if (!jfileName.endsWith(".xml"))
++                  {
++                      jfileName = jfileName + ".xml";
++                  }
++              saveState(_af.alignPanel, jfileName, true, jout, null);
++          }
      }
  
      /**
--     * @return true if the forward reference was fully resolved
++     * create a JalviewModel from an alignment view and marshall it to a
++     * JarOutputStream
++     * 
++     * @param ap
++     *          panel to create jalview model for
++     * @param fileName
++     *          name of alignment panel written to output stream
++     * @param jout
++     *          jar output stream
++     * @param viewIds
++     * @param out
++     *          jar entry name
       */
--    abstract boolean resolve();
--
--    @Override
--    public String toString()
++    protected JalviewModel saveState(AlignmentPanel ap, String fileName,
++                                   JarOutputStream jout, List<String> viewIds)
      {
--      return type + " reference to " + sref;
++      return saveState(ap, fileName, false, jout, viewIds);
      }
--  }
  
--  /**
--   * create forward reference for a mapping
--   * 
--   * @param sref
--   * @param _jmap
--   * @return
--   */
-   protected SeqFref newMappingRef(final String sref,
-           final jalview.datamodel.Mapping _jmap)
-   {
-     SeqFref fref = new SeqFref(sref, "Mapping")
-     {
-       public jalview.datamodel.Mapping jmap = _jmap;
 -  public SeqFref newMappingRef(final String sref,
 -          final jalview.datamodel.Mapping _jmap)
 -  {
 -    SeqFref fref = new SeqFref(sref, "Mapping")
 -    {
 -      public jalview.datamodel.Mapping jmap = _jmap;
++    /**
++     * create a JalviewModel from an alignment view and marshall it to a
++     * JarOutputStream
++     * 
++     * @param ap
++     *          panel to create jalview model for
++     * @param fileName
++     *          name of alignment panel written to output stream
++     * @param storeDS
++     *          when true, only write the dataset for the alignment, not the data
++     *          associated with the view.
++     * @param jout
++     *          jar output stream
++     * @param out
++     *          jar entry name
++     */
++    protected JalviewModel saveState(AlignmentPanel ap, String fileName,
++                                   boolean storeDS, JarOutputStream jout, List<String> viewIds)
++    {
++      if (viewIds == null)
++          {
++              viewIds = new ArrayList<>();
++          }
++
++      initSeqRefs();
++
++      List<UserColourScheme> userColours = new ArrayList<>();
++
++      AlignViewport av = ap.av;
++      ViewportRanges vpRanges = av.getRanges();
++
++      final ObjectFactory objectFactory = new ObjectFactory();
++      JalviewModel object = objectFactory.createJalviewModel();
++      object.setVamsasModel(new VAMSAS());
++
++      // object.setCreationDate(new java.util.Date(System.currentTimeMillis()));
++      try
++          {
++              GregorianCalendar c = new GregorianCalendar();
++              DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
++              XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(c);// gregorianCalendar);
++              object.setCreationDate(now);
++          } catch (DatatypeConfigurationException e)
++          {
++              System.err.println("error writing date: " + e.toString());
++          }
++      object.setVersion(Cache.getDefault("VERSION", "Development Build"));
++
++      /**
++       * rjal is full height alignment, jal is actual alignment with full metadata
++       * but excludes hidden sequences.
++       */
++      jalview.datamodel.AlignmentI rjal = av.getAlignment(), jal = rjal;
++
++      if (av.hasHiddenRows())
++          {
++              rjal = jal.getHiddenSequences().getFullAlignment();
++          }
++
++      SequenceSet vamsasSet = new SequenceSet();
++      Sequence vamsasSeq;
++      // JalviewModelSequence jms = new JalviewModelSequence();
++
++      vamsasSet.setGapChar(jal.getGapCharacter() + "");
++
++      if (jal.getDataset() != null)
++          {
++              // dataset id is the dataset's hashcode
++              vamsasSet.setDatasetId(getDatasetIdRef(jal.getDataset()));
++              if (storeDS)
++                  {
++                      // switch jal and the dataset
++                      jal = jal.getDataset();
++                      rjal = jal;
++                  }
++          }
++      if (jal.getProperties() != null)
++          {
++              Enumeration en = jal.getProperties().keys();
++              while (en.hasMoreElements())
++                  {
++                      String key = en.nextElement().toString();
++                      SequenceSetProperties ssp = new SequenceSetProperties();
++                      ssp.setKey(key);
++                      ssp.setValue(jal.getProperties().get(key).toString());
++                      // vamsasSet.addSequenceSetProperties(ssp);
++                      vamsasSet.getSequenceSetProperties().add(ssp);
++                  }
++          }
++
++      JSeq jseq;
++      Set<String> calcIdSet = new HashSet<>();
++      // record the set of vamsas sequence XML POJO we create.
++      HashMap<String, Sequence> vamsasSetIds = new HashMap<>();
++      // SAVE SEQUENCES
++      for (final SequenceI jds : rjal.getSequences())
++          {
++              final SequenceI jdatasq = jds.getDatasetSequence() == null ? jds
++                  : jds.getDatasetSequence();
++              String id = seqHash(jds);
++              if (vamsasSetIds.get(id) == null)
++                  {
++                      if (seqRefIds.get(id) != null && !storeDS)
++                          {
++                              // This happens for two reasons: 1. multiple views are being
++                              // serialised.
++                              // 2. the hashCode has collided with another sequence's code. This
++                              // DOES
++                              // HAPPEN! (PF00072.15.stk does this)
++                              // JBPNote: Uncomment to debug writing out of files that do not read
++                              // back in due to ArrayOutOfBoundExceptions.
++                              // System.err.println("vamsasSeq backref: "+id+"");
++                              // System.err.println(jds.getName()+"
++                              // "+jds.getStart()+"-"+jds.getEnd()+" "+jds.getSequenceAsString());
++                              // System.err.println("Hashcode: "+seqHash(jds));
++                              // SequenceI rsq = (SequenceI) seqRefIds.get(id + "");
++                              // System.err.println(rsq.getName()+"
++                              // "+rsq.getStart()+"-"+rsq.getEnd()+" "+rsq.getSequenceAsString());
++                              // System.err.println("Hashcode: "+seqHash(rsq));
++                          }
++                      else
++                          {
++                              vamsasSeq = createVamsasSequence(id, jds);
++                              //          vamsasSet.addSequence(vamsasSeq);
++                              vamsasSet.getSequence().add(vamsasSeq);
++                              vamsasSetIds.put(id, vamsasSeq);
++                              seqRefIds.put(id, jds);
++                          }
++                  }
++              jseq = new JSeq();
++              jseq.setStart(jds.getStart());
++              jseq.setEnd(jds.getEnd());
++              jseq.setColour(av.getSequenceColour(jds).getRGB());
++
++              jseq.setId(id); // jseq id should be a string not a number
++              if (!storeDS)
++                  {
++                      // Store any sequences this sequence represents
++                      if (av.hasHiddenRows())
++                          {
++                              // use rjal, contains the full height alignment
++                              jseq.setHidden(
++                                             av.getAlignment().getHiddenSequences().isHidden(jds));
++
++                              if (av.isHiddenRepSequence(jds))
++                                  {
++                                      jalview.datamodel.SequenceI[] reps = av
++                                          .getRepresentedSequences(jds).getSequencesInOrder(rjal);
++
++                                      for (int h = 0; h < reps.length; h++)
++                                          {
++                                              if (reps[h] != jds)
++                                                  {
++                                                      // jseq.addHiddenSequences(rjal.findIndex(reps[h]));
++                                                      jseq.getHiddenSequences().add(rjal.findIndex(reps[h]));
++                                                  }
++                                          }
++                                  }
++                          }
++                      // mark sequence as reference - if it is the reference for this view
++                      if (jal.hasSeqrep())
++                          {
++                              jseq.setViewreference(jds == jal.getSeqrep());
++                          }
++                  }
++
++              // TODO: omit sequence features from each alignment view's XML dump if we
++              // are storing dataset
++              List<SequenceFeature> sfs = jds.getSequenceFeatures();
++              for (SequenceFeature sf : sfs)
++                  {
++                      // Features features = new Features();
++                      Feature features = new Feature();
++
++                      features.setBegin(sf.getBegin());
++                      features.setEnd(sf.getEnd());
++                      features.setDescription(sf.getDescription());
++                      features.setType(sf.getType());
++                      features.setFeatureGroup(sf.getFeatureGroup());
++                      features.setScore(sf.getScore());
++                      if (sf.links != null)
++                          {
++                              for (int l = 0; l < sf.links.size(); l++)
++                                  {
++                                      OtherData keyValue = new OtherData();
++                                      keyValue.setKey("LINK_" + l);
++                                      keyValue.setValue(sf.links.elementAt(l).toString());
++                                      // features.addOtherData(keyValue);
++                                      features.getOtherData().add(keyValue);
++                                  }
++                          }
++                      if (sf.otherDetails != null)
++                          {
++                              /*
++                               * save feature attributes, which may be simple strings or
++                               * map valued (have sub-attributes)
++                               */
++                              for (Entry<String, Object> entry : sf.otherDetails.entrySet())
++                                  {
++                                      String key = entry.getKey();
++                                      Object value = entry.getValue();
++                                      if (value instanceof Map<?, ?>)
++                                          {
++                                              for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
++                                                       .entrySet())
++                                                  {
++                                                      OtherData otherData = new OtherData();
++                                                      otherData.setKey(key);
++                                                      otherData.setKey2(subAttribute.getKey());
++                                                      otherData.setValue(subAttribute.getValue().toString());
++                                                      // features.addOtherData(otherData);
++                                                      features.getOtherData().add(otherData);
++                                                  }
++                                          }
++                                      else
++                                          {
++                                              OtherData otherData = new OtherData();
++                                              otherData.setKey(key);
++                                              otherData.setValue(value.toString());
++                                              // features.addOtherData(otherData);
++                                              features.getOtherData().add(otherData);
++                                          }
++                                  }
++                          }
++
++                      // jseq.addFeatures(features);
++                      jseq.getFeatures().add(features);
++                  }
++
++              /*
++               * save PDB entries for sequence
++               */
++              if (jdatasq.getAllPDBEntries() != null)
++                  {
++                      Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
++                      while (en.hasMoreElements())
++                          {
++                              Pdbids pdb = new Pdbids();
++                              jalview.datamodel.PDBEntry entry = en.nextElement();
++
++                              String pdbId = entry.getId();
++                              pdb.setId(pdbId);
++                              pdb.setType(entry.getType());
++
++                              /*
++                               * Store any structure views associated with this sequence. This
++                               * section copes with duplicate entries in the project, so a dataset
++                               * only view *should* be coped with sensibly.
++                               */
++                              // This must have been loaded, is it still visible?
++                              JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
++                              String matchedFile = null;
++                              for (int f = frames.length - 1; f > -1; f--)
++                                  {
++                                      if (frames[f] instanceof StructureViewerBase)
++                                          {
++                                              StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
++                                              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);
++                                                      File viewerState = viewFrame.saveSession();
++                                                      if (viewerState != null)
++                                                          {
++                                                              copyFileToJar(jout, viewerState.getPath(),
++                                                                            getViewerJarEntryName(viewId), viewerType);
++                                                          }
++                                                      else
++                                                          {
++                                                              Console.error(
++                                                                            "Failed to save viewer state for " + viewerType);
++                                                          }
++                                                  }
++                                          }
++                                  }
++
++                              if (matchedFile != null || entry.getFile() != null)
++                                  {
++                                      if (entry.getFile() != null)
++                                          {
++                                              // use entry's file
++                                              matchedFile = entry.getFile();
++                                          }
++                                      pdb.setFile(matchedFile); // entry.getFile());
++                                      if (pdbfiles == null)
++                                          {
++                                              pdbfiles = new ArrayList<>();
++                                          }
++
++                                      if (!pdbfiles.contains(pdbId))
++                                          {
++                                              pdbfiles.add(pdbId);
++                                              copyFileToJar(jout, matchedFile, pdbId, pdbId);
++                                          }
++                                  }
++
++                              Enumeration<String> props = entry.getProperties();
++                              if (props.hasMoreElements())
++                                  {
++                                      // PdbentryItem item = new PdbentryItem();
++                                      while (props.hasMoreElements())
++                                          {
++                                              Property prop = new Property();
++                                              String key = props.nextElement();
++                                              prop.setName(key);
++                                              prop.setValue(entry.getProperty(key).toString());
++                                              // item.addProperty(prop);
++                                              pdb.getProperty().add(prop);
++                                          }
++                                      // pdb.addPdbentryItem(item);
++                                  }
++
++                              // jseq.addPdbids(pdb);
++                              jseq.getPdbids().add(pdb);
++                          }
++                  }
++
++              saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
++
++              if (jds.hasHMMProfile())
++                  {
++                      saveHmmerProfile(jout, jseq, jds);
++                  }
++              // jms.addJSeq(jseq);
++              object.getJSeq().add(jseq);
++          }
++
++      if (!storeDS && av.hasHiddenRows())
++          {
++              jal = av.getAlignment();
++          }
++      // SAVE MAPPINGS
++      // FOR DATASET
++      if (storeDS && jal.getCodonFrames() != null)
++          {
++              List<AlignedCodonFrame> jac = jal.getCodonFrames();
++              for (AlignedCodonFrame acf : jac)
++                  {
++                      AlcodonFrame alc = new AlcodonFrame();
++                      if (acf.getProtMappings() != null
++                          && acf.getProtMappings().length > 0)
++                          {
++                              boolean hasMap = false;
++                              SequenceI[] dnas = acf.getdnaSeqs();
++                              jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
++                              for (int m = 0; m < pmaps.length; m++)
++                                  {
++                                      AlcodMap alcmap = new AlcodMap();
++                                      alcmap.setDnasq(seqHash(dnas[m]));
++                                      alcmap.setMapping(
++                                                        createVamsasMapping(pmaps[m], dnas[m], null, false));
++                                      // alc.addAlcodMap(alcmap);
++                                      alc.getAlcodMap().add(alcmap);
++                                      hasMap = true;
++                                  }
++                              if (hasMap)
++                                  {
++                                      // vamsasSet.addAlcodonFrame(alc);
++                                      vamsasSet.getAlcodonFrame().add(alc);
++                                  }
++                          }
++                      // TODO: delete this ? dead code from 2.8.3->2.9 ?
++                      // {
++                      // AlcodonFrame alc = new AlcodonFrame();
++                      // vamsasSet.addAlcodonFrame(alc);
++                      // for (int p = 0; p < acf.aaWidth; p++)
++                      // {
++                      // Alcodon cmap = new Alcodon();
++                      // if (acf.codons[p] != null)
++                      // {
++                      // // Null codons indicate a gapped column in the translated peptide
++                      // // alignment.
++                      // cmap.setPos1(acf.codons[p][0]);
++                      // cmap.setPos2(acf.codons[p][1]);
++                      // cmap.setPos3(acf.codons[p][2]);
++                      // }
++                      // alc.addAlcodon(cmap);
++                      // }
++                      // if (acf.getProtMappings() != null
++                      // && acf.getProtMappings().length > 0)
++                      // {
++                      // SequenceI[] dnas = acf.getdnaSeqs();
++                      // jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
++                      // for (int m = 0; m < pmaps.length; m++)
++                      // {
++                      // AlcodMap alcmap = new AlcodMap();
++                      // alcmap.setDnasq(seqHash(dnas[m]));
++                      // alcmap.setMapping(createVamsasMapping(pmaps[m], dnas[m], null,
++                      // false));
++                      // alc.addAlcodMap(alcmap);
++                      // }
++                      // }
++                  }
++          }
++
++      // SAVE TREES
++      // /////////////////////////////////
++      if (!storeDS && av.getCurrentTree() != null)
++          {
++              // FIND ANY ASSOCIATED TREES
++              // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
++              if (Desktop.getDesktopPane() != null)
++                  {
++                      JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
++
++                      for (int t = 0; t < frames.length; t++)
++                          {
++                              if (frames[t] instanceof TreePanel)
++                                  {
++                                      TreePanel tp = (TreePanel) frames[t];
++
++                                      if (tp.getTreeCanvas().getViewport().getAlignment() == jal)
++                                          {
++                                              JalviewModel.Tree tree = new JalviewModel.Tree();
++                                              tree.setTitle(tp.getTitle());
++                                              tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
++                                              tree.setNewick(tp.getTree().print());
++                                              tree.setThreshold(tp.getTreeCanvas().getThreshold());
++
++                                              tree.setFitToWindow(tp.fitToWindow.getState());
++                                              tree.setFontName(tp.getTreeFont().getName());
++                                              tree.setFontSize(tp.getTreeFont().getSize());
++                                              tree.setFontStyle(tp.getTreeFont().getStyle());
++                                              tree.setMarkUnlinked(tp.placeholdersMenu.getState());
++
++                                              tree.setShowBootstrap(tp.bootstrapMenu.getState());
++                                              tree.setShowDistances(tp.distanceMenu.getState());
++
++                                              tree.setHeight(tp.getHeight());
++                                              tree.setWidth(tp.getWidth());
++                                              tree.setXpos(tp.getX());
++                                              tree.setYpos(tp.getY());
++                                              tree.setId(makeHashCode(tp, null));
++                                              tree.setLinkToAllViews(
++                                                                     tp.getTreeCanvas().isApplyToAllViews());
++
++                                              // jms.addTree(tree);
++                                              object.getTree().add(tree);
++                                          }
++                                  }
++                          }
++                  }
++          }
++
++      /*
++       * save PCA viewers
++       */
++      if (!storeDS && Desktop.getDesktopPane() != null)
++          {
++              for (JInternalFrame frame : Desktop.getDesktopPane().getAllFrames())
++                  {
++                      if (frame instanceof PCAPanel)
++                          {
++                              PCAPanel panel = (PCAPanel) frame;
++                              if (panel.getAlignViewport().getAlignment() == jal)
++                                  {
++                                      savePCA(panel, object);
++                                  }
++                          }
++                  }
++          }
++
++      // SAVE ANNOTATIONS
++      /**
++       * store forward refs from an annotationRow to any groups
++       */
++      IdentityHashMap<SequenceGroup, String> groupRefs = new IdentityHashMap<>();
++      if (storeDS)
++          {
++              for (SequenceI sq : jal.getSequences())
++                  {
++                      // Store annotation on dataset sequences only
++                      AlignmentAnnotation[] aa = sq.getAnnotation();
++                      if (aa != null && aa.length > 0)
++                          {
++                              storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
++                                                       vamsasSet);
++                          }
++                  }
++          }
++      else
++          {
++              if (jal.getAlignmentAnnotation() != null)
++                  {
++                      // Store the annotation shown on the alignment.
++                      AlignmentAnnotation[] aa = jal.getAlignmentAnnotation();
++                      storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
++                                               vamsasSet);
++                  }
++          }
++      // SAVE GROUPS
++      if (jal.getGroups() != null)
++          {
++              JGroup[] groups = new JGroup[jal.getGroups().size()];
++              int i = -1;
++              for (jalview.datamodel.SequenceGroup sg : jal.getGroups())
++                  {
++                      JGroup jGroup = new JGroup();
++                      groups[++i] = jGroup;
++
++                      jGroup.setStart(sg.getStartRes());
++                      jGroup.setEnd(sg.getEndRes());
++                      jGroup.setName(sg.getName());
++                      if (groupRefs.containsKey(sg))
++                          {
++                              // group has references so set its ID field
++                              jGroup.setId(groupRefs.get(sg));
++                          }
++                      ColourSchemeI colourScheme = sg.getColourScheme();
++                      if (colourScheme != null)
++                          {
++                              ResidueShaderI groupColourScheme = sg.getGroupColourScheme();
++                              if (groupColourScheme.conservationApplied())
++                                  {
++                                      jGroup.setConsThreshold(groupColourScheme.getConservationInc());
++
++                                      if (colourScheme instanceof jalview.schemes.UserColourScheme)
++                                          {
++                                              jGroup.setColour(
++                                                               setUserColourScheme(colourScheme, userColours,
++                                                                                   object));
++                                          }
++                                      else
++                                          {
++                                              jGroup.setColour(colourScheme.getSchemeName());
++                                          }
++                                  }
++                              else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
++                                  {
++                                      jGroup.setColour("AnnotationColourGradient");
++                                      jGroup.setAnnotationColours(constructAnnotationColours(
++                                                                                             (jalview.schemes.AnnotationColourGradient) colourScheme,
++                                                                                             userColours, object));
++                                  }
++                              else if (colourScheme instanceof jalview.schemes.UserColourScheme)
++                                  {
++                                      jGroup.setColour(
++                                                       setUserColourScheme(colourScheme, userColours, object));
++                                  }
++                              else
++                                  {
++                                      jGroup.setColour(colourScheme.getSchemeName());
++                                  }
++
++                              jGroup.setPidThreshold(groupColourScheme.getThreshold());
++                          }
++
++                      jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
++                      jGroup.setDisplayBoxes(sg.getDisplayBoxes());
++                      jGroup.setDisplayText(sg.getDisplayText());
++                      jGroup.setColourText(sg.getColourText());
++                      jGroup.setTextCol1(sg.textColour.getRGB());
++                      jGroup.setTextCol2(sg.textColour2.getRGB());
++                      jGroup.setTextColThreshold(sg.thresholdTextColour);
++                      jGroup.setShowUnconserved(sg.getShowNonconserved());
++                      jGroup.setIgnoreGapsinConsensus(sg.getIgnoreGapsConsensus());
++                      jGroup.setShowConsensusHistogram(sg.isShowConsensusHistogram());
++                      jGroup.setShowSequenceLogo(sg.isShowSequenceLogo());
++                      jGroup.setNormaliseSequenceLogo(sg.isNormaliseSequenceLogo());
++                      for (SequenceI seq : sg.getSequences())
++                          {
++                              // jGroup.addSeq(seqHash(seq));
++                              jGroup.getSeq().add(seqHash(seq));
++                          }
++                  }
++
++              //jms.setJGroup(groups);
++              Object group;
++              for (JGroup grp : groups)
++                  {
++                      object.getJGroup().add(grp);
++                  }
++          }
++      if (!storeDS)
++          {
++              // /////////SAVE VIEWPORT
++              Viewport view = new Viewport();
++              view.setTitle(ap.alignFrame.getTitle());
++              view.setSequenceSetId(
++                                    makeHashCode(av.getSequenceSetId(), av.getSequenceSetId()));
++              view.setId(av.getViewId());
++              if (av.getCodingComplement() != null)
++                  {
++                      view.setComplementId(av.getCodingComplement().getViewId());
++                  }
++              view.setViewName(av.getViewName());
++              view.setGatheredViews(av.isGatherViewsHere());
++
++              Rectangle size = ap.av.getExplodedGeometry();
++              Rectangle position = size;
++              if (size == null)
++                  {
++                      size = ap.alignFrame.getBounds();
++                      if (av.getCodingComplement() != null)
++                          {
++                              position = ((SplitFrame) ap.alignFrame.getSplitViewContainer())
++                                  .getBounds();
++                          }
++                      else
++                          {
++                              position = size;
++                          }
++                  }
++              view.setXpos(position.x);
++              view.setYpos(position.y);
++
++              view.setWidth(size.width);
++              view.setHeight(size.height);
++
++              view.setStartRes(vpRanges.getStartRes());
++              view.setStartSeq(vpRanges.getStartSeq());
++
++              if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
++                  {
++                      view.setBgColour(setUserColourScheme(av.getGlobalColourScheme(),
++                                                           userColours, object));
++                  }
++              else if (av
++                       .getGlobalColourScheme() instanceof jalview.schemes.AnnotationColourGradient)
++                  {
++                      AnnotationColourScheme ac = constructAnnotationColours(
++                                                                             (jalview.schemes.AnnotationColourGradient) av
++                                                                             .getGlobalColourScheme(),
++                                                                             userColours, object);
++
++                      view.setAnnotationColours(ac);
++                      view.setBgColour("AnnotationColourGradient");
++                  }
++              else
++                  {
++                      view.setBgColour(ColourSchemeProperty
++                                       .getColourName(av.getGlobalColourScheme()));
++                  }
++
++              ResidueShaderI vcs = av.getResidueShading();
++              ColourSchemeI cs = av.getGlobalColourScheme();
++
++              if (cs != null)
++                  {
++                      if (vcs.conservationApplied())
++                          {
++                              view.setConsThreshold(vcs.getConservationInc());
++                              if (cs instanceof jalview.schemes.UserColourScheme)
++                                  {
++                                      view.setBgColour(setUserColourScheme(cs, userColours, object));
++                                  }
++                          }
++                      view.setPidThreshold(vcs.getThreshold());
++                  }
++
++              view.setConservationSelected(av.getConservationSelected());
++              view.setPidSelected(av.getAbovePIDThreshold());
++              final Font font = av.getFont();
++              view.setFontName(font.getName());
++              view.setFontSize(font.getSize());
++              view.setFontStyle(font.getStyle());
++              view.setScaleProteinAsCdna(av.getViewStyle().isScaleProteinAsCdna());
++              view.setRenderGaps(av.isRenderGaps());
++              view.setShowAnnotation(av.isShowAnnotation());
++              view.setShowBoxes(av.getShowBoxes());
++              view.setShowColourText(av.getColourText());
++              view.setShowFullId(av.getShowJVSuffix());
++              view.setRightAlignIds(av.isRightAlignIds());
++              view.setShowSequenceFeatures(av.isShowSequenceFeatures());
++              view.setShowText(av.getShowText());
++              view.setShowUnconserved(av.getShowUnconserved());
++              view.setWrapAlignment(av.getWrapAlignment());
++              view.setTextCol1(av.getTextColour().getRGB());
++              view.setTextCol2(av.getTextColour2().getRGB());
++              view.setTextColThreshold(av.getThresholdTextColour());
++              view.setShowConsensusHistogram(av.isShowConsensusHistogram());
++              view.setShowSequenceLogo(av.isShowSequenceLogo());
++              view.setNormaliseSequenceLogo(av.isNormaliseSequenceLogo());
++              view.setShowGroupConsensus(av.isShowGroupConsensus());
++              view.setShowGroupConservation(av.isShowGroupConservation());
++              view.setShowNPfeatureTooltip(av.isShowNPFeats());
++              view.setShowDbRefTooltip(av.isShowDBRefs());
++              view.setFollowHighlight(av.isFollowHighlight());
++              view.setFollowSelection(av.followSelection);
++              view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
++              view.setShowComplementFeatures(av.isShowComplementFeatures());
++              view.setShowComplementFeaturesOnTop(
++                                                  av.isShowComplementFeaturesOnTop());
++              if (av.getFeaturesDisplayed() != null)
++                  {
++                      FeatureSettings fs = new FeatureSettings();
++
++                      FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
++                          .getFeatureRenderer();
++                      String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
++
++                      Vector<String> settingsAdded = new Vector<>();
++                      if (renderOrder != null)
++                          {
++                              for (String featureType : renderOrder)
++                                  {
++                                      FeatureSettings.Setting setting = new FeatureSettings.Setting();
++                                      setting.setType(featureType);
++
++                                      /*
++                                       * save any filter for the feature type
++                                       */
++                                      FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
++                                      if (filter != null)  {
++                                          Iterator<FeatureMatcherI> filters = filter.getMatchers().iterator();
++                                          FeatureMatcherI firstFilter = filters.next();
++                                          setting.setMatcherSet(Jalview2XML.marshalFilter(
++                                                                                          firstFilter, filters, filter.isAnded()));
++                                      }
++
++                                      /*
++                                       * save colour scheme for the feature type
++                                       */
++                                      FeatureColourI fcol = fr.getFeatureStyle(featureType);
++                                      if (!fcol.isSimpleColour())
++                                          {
++                                              setting.setColour(fcol.getMaxColour().getRGB());
++                                              setting.setMincolour(fcol.getMinColour().getRGB());
++                                              setting.setMin(fcol.getMin());
++                                              setting.setMax(fcol.getMax());
++                                              setting.setColourByLabel(fcol.isColourByLabel());
++                                              if (fcol.isColourByAttribute())
++                                                  {
++                                                      String[] attName = fcol.getAttributeName();
++                                                      setting.getAttributeName().add(attName[0]);
++                                                      if (attName.length > 1)
++                                                          {
++                                                              setting.getAttributeName().add(attName[1]);
++                                                          }
++                                                  }
++                                              setting.setAutoScale(fcol.isAutoScaled());
++                                              setting.setThreshold(fcol.getThreshold());
++                                              Color noColour = fcol.getNoColour();
++                                              if (noColour == null)
++                                                  {
++                                                      setting.setNoValueColour(NoValueColour.NONE);
++                                                  }
++                                              else if (noColour.equals(fcol.getMaxColour()))
++                                                  {
++                                                      setting.setNoValueColour(NoValueColour.MAX);
++                                                  }
++                                              else
++                                                  {
++                                                      setting.setNoValueColour(NoValueColour.MIN);
++                                                  }
++                                              // -1 = No threshold, 0 = Below, 1 = Above
++                                              setting.setThreshstate(fcol.isAboveThreshold() ? 1
++                                                                     : (fcol.isBelowThreshold() ? 0 : -1));
++                                          }
++                                      else
++                                          {
++                                              setting.setColour(fcol.getColour().getRGB());
++                                          }
++
++                                      setting.setDisplay(
++                                                         av.getFeaturesDisplayed().isVisible(featureType));
++                                      float rorder = fr
++                                          .getOrder(featureType);
++                                      if (rorder > -1)
++                                          {
++                                              setting.setOrder(rorder);
++                                          }
++                                      /// fs.addSetting(setting);
++                                      fs.getSetting().add(setting);
++                                      settingsAdded.addElement(featureType);
++                                  }
++                          }
++
++                      // is groups actually supposed to be a map here ?
++                      Iterator<String> en = fr.getFeatureGroups().iterator();
++                      Vector<String> groupsAdded = new Vector<>();
++                      while (en.hasNext())
++                          {
++                              String grp = en.next();
++                              if (groupsAdded.contains(grp))
++                                  {
++                                      continue;
++                                  }
++                              Group g = new Group();
++                              g.setName(grp);
++                              g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
++                                           .booleanValue());
++                              // fs.addGroup(g);
++                              fs.getGroup().add(g);
++                              groupsAdded.addElement(grp);
++                          }
++                      // jms.setFeatureSettings(fs);
++                      object.setFeatureSettings(fs);
++                  }
++
++              if (av.hasHiddenColumns())
++                  {
++                      jalview.datamodel.HiddenColumns hidden = av.getAlignment()
++                          .getHiddenColumns();
++                      if (hidden == null)
++                          {
++                              Console.warn(
++                                           "REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
++                          }
++                      else
++                          {
++                              Iterator<int[]> hiddenRegions = hidden.iterator();
++                              while (hiddenRegions.hasNext())
++                                  {
++                                      int[] region = hiddenRegions.next();
++                                      HiddenColumns hc = new HiddenColumns();
++                                      hc.setStart(region[0]);
++                                      hc.setEnd(region[1]);
++                                      // view.addHiddenColumns(hc);
++                                      view.getHiddenColumns().add(hc);
++                                  }
++                          }
++                  }
++              if (calcIdSet.size() > 0)
++                  {
++                      for (String calcId : calcIdSet)
++                          {
++                              if (calcId.trim().length() > 0)
++                                  {
++                                      CalcIdParam cidp = createCalcIdParam(calcId, av);
++                                      // Some calcIds have no parameters.
++                                      if (cidp != null)
++                                          {
++                                              // view.addCalcIdParam(cidp);
++                                              view.getCalcIdParam().add(cidp);
++                                          }
++                                  }
++                          }
++                  }
++
++              // jms.addViewport(view);
++              object.getViewport().add(view);
++          }
++      // object.setJalviewModelSequence(jms);
++      // object.getVamsasModel().addSequenceSet(vamsasSet);
++      object.getVamsasModel().getSequenceSet().add(vamsasSet);
++
++      if (jout != null && fileName != null)
++          {
++              // We may not want to write the object to disk,
++              // eg we can copy the alignViewport to a new view object
++              // using save and then load
++              try
++                  {
++                      fileName = fileName.replace('\\', '/');
++                      System.out.println("Writing jar entry " + fileName);
++                      JarEntry entry = new JarEntry(fileName);
++                      jout.putNextEntry(entry);
++                      PrintWriter pout = new PrintWriter(
++                                                         new OutputStreamWriter(jout, UTF_8));
++                      JAXBContext jaxbContext = JAXBContext
++                          .newInstance(JalviewModel.class);
++                      Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
++
++                      // output pretty printed
++                      // jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
++                      jaxbMarshaller.marshal(
++                                             new ObjectFactory().createJalviewModel(object), pout);
++
++                      // jaxbMarshaller.marshal(object, pout);
++                      // marshaller.marshal(object);
++                      pout.flush();
++                      jout.closeEntry();
++                  } catch (Exception ex)
++                  {
++                      // TODO: raise error in GUI if marshalling failed.
++                      System.err.println("Error writing Jalview project");
++                      ex.printStackTrace();
++                  }
++          }
++      return object;
++    }
++    /**
++     * Saves the HMMER profile associated with the sequence as a file in the jar,
++     * in HMMER format, and saves the name of the file as a child element of the
++     * XML sequence element
++     * 
++     * @param jout
++     * @param xmlSeq
++     * @param seq
++     */
++    protected void saveHmmerProfile(JarOutputStream jout, JSeq xmlSeq,
++                                  SequenceI seq)
++    {
++      HiddenMarkovModel profile = seq.getHMM();
++      if (profile == null)
++          {
++              Console.warn("Want to save HMM profile for " + seq.getName()
++                           + " but none found");
++              return;
++          }
++      HMMFile hmmFile = new HMMFile(profile);
++      String hmmAsString = hmmFile.print();
++      String jarEntryName = HMMER_PREFIX + nextCounter();
++      try
++          {
++              writeJarEntry(jout, jarEntryName, hmmAsString.getBytes());
++              xmlSeq.setHmmerProfile(jarEntryName);
++          } catch (IOException e)
++          {
++              Console.warn("Error saving HMM profile: " + e.getMessage());
++          }
++    }
 +
-       @Override
-       boolean resolve()
-       {
-         SequenceI seq = getSrefDatasetSeq();
-         if (seq == null)
-         {
-           return false;
-         }
-         jmap.setTo(seq);
-         return true;
-       }
-     };
-     return fref;
-   }
++    
++    /**
++     * Writes PCA viewer attributes and computed values to an XML model object and
++     * adds it to the JalviewModel. Any exceptions are reported by logging.
++     */
++    protected void savePCA(PCAPanel panel, JalviewModel object)
++    {
++      try
++          {
++              PcaViewer viewer = new PcaViewer();
++              viewer.setHeight(panel.getHeight());
++              viewer.setWidth(panel.getWidth());
++              viewer.setXpos(panel.getX());
++              viewer.setYpos(panel.getY());
++              viewer.setTitle(panel.getTitle());
++              PCAModel pcaModel = panel.getPcaModel();
++              viewer.setScoreModelName(pcaModel.getScoreModelName());
++              viewer.setXDim(panel.getSelectedDimensionIndex(X));
++              viewer.setYDim(panel.getSelectedDimensionIndex(Y));
++              viewer.setZDim(panel.getSelectedDimensionIndex(Z));
++              viewer.setBgColour(
++                                 panel.getRotatableCanvas().getBackgroundColour().getRGB());
++              viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
++              float[] spMin = panel.getRotatableCanvas().getSeqMin();
++              SeqPointMin spmin = new SeqPointMin();
++              spmin.setXPos(spMin[0]);
++              spmin.setYPos(spMin[1]);
++              spmin.setZPos(spMin[2]);
++              viewer.setSeqPointMin(spmin);
++              float[] spMax = panel.getRotatableCanvas().getSeqMax();
++              SeqPointMax spmax = new SeqPointMax();
++              spmax.setXPos(spMax[0]);
++              spmax.setYPos(spMax[1]);
++              spmax.setZPos(spMax[2]);
++              viewer.setSeqPointMax(spmax);
++              viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
++              viewer.setLinkToAllViews(
++                                       panel.getRotatableCanvas().isApplyToAllViews());
++              SimilarityParamsI sp = pcaModel.getSimilarityParameters();
++              viewer.setIncludeGaps(sp.includeGaps());
++              viewer.setMatchGaps(sp.matchGaps());
++              viewer.setIncludeGappedColumns(sp.includeGappedColumns());
++              viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
++
++              /*
++               * sequence points on display
++               */
++              for (jalview.datamodel.SequencePoint spt : pcaModel
++                       .getSequencePoints())
++                  {
++                      SequencePoint point = new SequencePoint();
++                      point.setSequenceRef(seqHash(spt.getSequence()));
++                      point.setXPos(spt.coord.x);
++                      point.setYPos(spt.coord.y);
++                      point.setZPos(spt.coord.z);
++                      viewer.getSequencePoint().add(point);
++                  }
++
++              /*
++               * (end points of) axes on display
++               */
++              for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
++                  {
++
++                      Axis axis = new Axis();
++                      axis.setXPos(p.x);
++                      axis.setYPos(p.y);
++                      axis.setZPos(p.z);
++                      viewer.getAxis().add(axis);
++                  }
++
++              /*
++               * raw PCA data (note we are not restoring PCA inputs here -
++               * alignment view, score model, similarity parameters)
++               */
++              PcaDataType data = new PcaDataType();
++              viewer.setPcaData(data);
++              PCA pca = pcaModel.getPcaData();
++
++              DoubleMatrix pm = new DoubleMatrix();
++              saveDoubleMatrix(pca.getPairwiseScores(), pm);
++              data.setPairwiseMatrix(pm);
++
++              DoubleMatrix tm = new DoubleMatrix();
++              saveDoubleMatrix(pca.getTridiagonal(), tm);
++              data.setTridiagonalMatrix(tm);
++
++              DoubleMatrix eigenMatrix = new DoubleMatrix();
++              data.setEigenMatrix(eigenMatrix);
++              saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
++
++              object.getPcaViewer().add(viewer);
++          } catch (Throwable t)
++          {
++              Console.error("Error saving PCA: " + t.getMessage());
++          }
++    }
  
-   protected SeqFref newAlcodMapRef(final String sref,
-           final AlignedCodonFrame _cf,
-           final jalview.datamodel.Mapping _jmap)
-   {
 -      @Override
 -      boolean resolve()
 -      {
 -        SequenceI seq = getSrefDatasetSeq();
 -        if (seq == null)
 -        {
 -          return false;
 -        }
 -        jmap.setTo(seq);
 -        return true;
 -      }
 -    };
 -    return fref;
 -  }
++    /**
++     * Stores values from a matrix into an XML element, including (if present) the
++     * D or E vectors
++     * 
++     * @param m
++     * @param xmlMatrix
++     * @see #loadDoubleMatrix(DoubleMatrix)
++     */
++    protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
++    {
++      xmlMatrix.setRows(m.height());
++      xmlMatrix.setColumns(m.width());
++      for (int i = 0; i < m.height(); i++)
++          {
++              DoubleVector row = new DoubleVector();
++              for (int j = 0; j < m.width(); j++)
++                  {
++                      row.getV().add(m.getValue(i, j));
++                  }
++              xmlMatrix.getRow().add(row);
++          }
++      if (m.getD() != null)
++          {
++              DoubleVector dVector = new DoubleVector();
++              for (double d : m.getD())
++                  {
++                      dVector.getV().add(d);
++                  }
++              xmlMatrix.setD(dVector);
++          }
++      if (m.getE() != null)
++          {
++              DoubleVector eVector = new DoubleVector();
++              for (double e : m.getE())
++                  {
++                      eVector.getV().add(e);
++                  }
++              xmlMatrix.setE(eVector);
++          }
++    }
  
-     SeqFref fref = new SeqFref(sref, "Codon Frame")
-     {
-       AlignedCodonFrame cf = _cf;
 -  public SeqFref newAlcodMapRef(final String sref,
 -          final AlignedCodonFrame _cf,
 -          final jalview.datamodel.Mapping _jmap)
 -  {
++    /**
++     * Loads XML matrix data into a new Matrix object, including the D and/or E
++     * vectors (if present)
++     * 
++     * @param mData
++     * @return
++     * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
++     */
++    protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
++    {
++      int rows = mData.getRows();
++      double[][] vals = new double[rows][];
++
++      for (int i = 0; i < rows; i++)
++          {
++              List<Double> dVector = mData.getRow().get(i).getV();
++              vals[i] = new double[dVector.size()];
++              int dvi = 0;
++              for (Double d : dVector)
++                  {
++                      vals[i][dvi++] = d;
++                  }
++          }
++
++      MatrixI m = new Matrix(vals);
++
++      if (mData.getD() != null)
++          {
++              List<Double> dVector = mData.getD().getV();
++              double[] vec = new double[dVector.size()];
++              int dvi = 0;
++              for (Double d : dVector)
++                  {
++                      vec[dvi++] = d;
++                  }
++              m.setD(vec);
++          }
++      if (mData.getE() != null)
++          {
++              List<Double> dVector = mData.getE().getV();
++              double[] vec = new double[dVector.size()];
++              int dvi = 0;
++              for (Double d : dVector)
++                  {
++                      vec[dvi++] = d;
++                  }
++              m.setE(vec);
++          }
++
++      return m;
++    }
  
-       public jalview.datamodel.Mapping mp = _jmap;
 -    SeqFref fref = new SeqFref(sref, "Codon Frame")
 -    {
 -      AlignedCodonFrame cf = _cf;
++    /**
++     * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
++     * for each viewer, with
++     * <ul>
++     * <li>viewer geometry (position, size, split pane divider location)</li>
++     * <li>index of the selected structure in the viewer (currently shows gapped
++     * or ungapped)</li>
++     * <li>the id of the annotation holding RNA secondary structure</li>
++     * <li>(currently only one SS is shown per viewer, may be more in future)</li>
++     * </ul>
++     * Varna viewer state is also written out (in native Varna XML) to separate
++     * project jar entries. A separate entry is written for each RNA structure
++     * displayed, with the naming convention
++     * <ul>
++     * <li>rna_viewId_sequenceId_annotationId_[gapped|trimmed]</li>
++     * </ul>
++     * 
++     * @param jout
++     * @param jseq
++     * @param jds
++     * @param viewIds
++     * @param ap
++     * @param storeDataset
++     */
++    protected void saveRnaViewers(JarOutputStream jout, JSeq jseq,
++                                final SequenceI jds, List<String> viewIds, AlignmentPanel ap,
++                                boolean storeDataset)
++    {
++      if (Desktop.getDesktopPane() == null)
++          {
++              return;
++          }
++      JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
++      for (int f = frames.length - 1; f > -1; f--)
++          {
++              if (frames[f] instanceof AppVarna)
++                  {
++                      AppVarna varna = (AppVarna) frames[f];
++                      /*
++                       * link the sequence to every viewer that is showing it and is linked to
++                       * its alignment panel
++                       */
++                      if (varna.isListeningFor(jds) && ap == varna.getAlignmentPanel())
++                          {
++                              String viewId = varna.getViewId();
++                              RnaViewer rna = new RnaViewer();
++                              rna.setViewId(viewId);
++                              rna.setTitle(varna.getTitle());
++                              rna.setXpos(varna.getX());
++                              rna.setYpos(varna.getY());
++                              rna.setWidth(varna.getWidth());
++                              rna.setHeight(varna.getHeight());
++                              rna.setDividerLocation(varna.getDividerLocation());
++                              rna.setSelectedRna(varna.getSelectedIndex());
++                              // jseq.addRnaViewer(rna);
++                              jseq.getRnaViewer().add(rna);
++
++                              /*
++                               * Store each Varna panel's state once in the project per sequence.
++                               * First time through only (storeDataset==false)
++                               */
++                              // boolean storeSessions = false;
++                              // String sequenceViewId = viewId + seqsToIds.get(jds);
++                              // if (!storeDataset && !viewIds.contains(sequenceViewId))
++                              // {
++                              // viewIds.add(sequenceViewId);
++                              // storeSessions = true;
++                              // }
++                              for (RnaModel model : varna.getModels())
++                                  {
++                                      if (model.seq == jds)
++                                          {
++                                              /*
++                                               * VARNA saves each view (sequence or alignment secondary
++                                               * structure, gapped or trimmed) as a separate XML file
++                                               */
++                                              String jarEntryName = rnaSessions.get(model);
++                                              if (jarEntryName == null)
++                                                  {
++
++                                                      String varnaStateFile = varna.getStateInfo(model.rna);
++                                                      jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
++                                                      copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
++                                                      rnaSessions.put(model, jarEntryName);
++                                                  }
++                                              SecondaryStructure ss = new SecondaryStructure();
++                                              String annotationId = varna.getAnnotation(jds).annotationId;
++                                              ss.setAnnotationId(annotationId);
++                                              ss.setViewerState(jarEntryName);
++                                              ss.setGapped(model.gapped);
++                                              ss.setTitle(model.title);
++                                              // rna.addSecondaryStructure(ss);
++                                              rna.getSecondaryStructure().add(ss);
++                                          }
++                                  }
++                          }
++                  }
++          }
++    }
  
-       @Override
-       public boolean isResolvable()
-       {
-         return super.isResolvable() && mp.getTo() != null;
-       }
 -      public jalview.datamodel.Mapping mp = _jmap;
++    /**
++     * Copy the contents of a file to a new entry added to the output jar
++     * 
++     * @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 msg)
++    {
++      try (InputStream is = new FileInputStream(infilePath))
++          {
++              File file = new File(infilePath);
++              if (file.exists() && jout != null)
++                  {
++                      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();
++          }
++    }
  
--      @Override
-       boolean resolve()
-       {
-         SequenceI seq = getSrefDatasetSeq();
-         if (seq == null)
-         {
-           return false;
-         }
-         cf.addMap(seq, mp.getTo(), mp.getMap());
-         return true;
-       }
-     };
-     return fref;
-   }
 -      public boolean isResolvable()
 -      {
 -        return super.isResolvable() && mp.getTo() != null;
 -      }
 -
 -      @Override
 -      boolean resolve()
 -      {
 -        SequenceI seq = getSrefDatasetSeq();
 -        if (seq == null)
 -        {
 -          return false;
 -        }
 -        cf.addMap(seq, mp.getTo(), mp.getMap());
 -        return true;
 -      }
 -    };
 -    return fref;
 -  }
 -
 -  public void resolveFrefedSequences()
 -  {
 -    Iterator<SeqFref> nextFref = frefedSequence.iterator();
 -    int toresolve = frefedSequence.size();
 -    int unresolved = 0, failedtoresolve = 0;
 -    while (nextFref.hasNext())
 -    {
 -      SeqFref ref = nextFref.next();
 -      if (ref.isResolvable())
 -      {
 -        try
 -        {
 -          if (ref.resolve())
 -          {
 -            nextFref.remove();
 -          }
 -          else
 -          {
 -            failedtoresolve++;
 -          }
 -        } catch (Exception x)
 -        {
 -          System.err.println(
 -                  "IMPLEMENTATION ERROR: Failed to resolve forward reference for sequence "
 -                          + ref.getSref());
 -          x.printStackTrace();
 -          failedtoresolve++;
 -        }
 -      }
 -      else
 -      {
 -        unresolved++;
 -      }
 -    }
 -    if (unresolved > 0)
 -    {
 -      System.err.println("Jalview Project Import: There were " + unresolved
 -              + " forward references left unresolved on the stack.");
 -    }
 -    if (failedtoresolve > 0)
 -    {
 -      System.err.println("SERIOUS! " + failedtoresolve
 -              + " resolvable forward references failed to resolve.");
 -    }
 -    if (incompleteSeqs != null && incompleteSeqs.size() > 0)
 -    {
 -      System.err.println(
 -              "Jalview Project Import: There are " + incompleteSeqs.size()
 -                      + " sequences which may have incomplete metadata.");
 -      if (incompleteSeqs.size() < 10)
 -      {
 -        for (SequenceI s : incompleteSeqs.values())
 -        {
 -          System.err.println(s.toString());
 -        }
 -      }
 -      else
 -      {
 -        System.err.println(
 -                "Too many to report. Skipping output of incomplete sequences.");
 -      }
 -    }
 -  }
 -
 -  /**
 -   * This maintains a map of viewports, the key being the seqSetId. Important to
 -   * set historyItem and redoList for multiple views
 -   */
 -  Map<String, AlignViewport> viewportsAdded = new HashMap<>();
 -
 -  Map<String, AlignmentAnnotation> annotationIds = new HashMap<>();
 -
 -  String uniqueSetSuffix = "";
 -
 -  /**
 -   * List of pdbfiles added to Jar
 -   */
 -  List<String> pdbfiles = null;
 -
 -  // SAVES SEVERAL ALIGNMENT WINDOWS TO SAME JARFILE
 -  public void saveState(File statefile)
 -  {
 -    FileOutputStream fos = null;
 -
 -    try
 -    {
 -
 -      fos = new FileOutputStream(statefile);
 -
 -      JarOutputStream jout = new JarOutputStream(fos);
 -      saveState(jout);
 -      fos.close();
 -
 -    } catch (Exception e)
 -    {
 -      Console.error("Couln't write Jalview state to " + statefile, e);
 -      // TODO: inform user of the problem - they need to know if their data was
 -      // not saved !
 -      if (errorMessage == null)
 -      {
 -        errorMessage = "Did't write Jalview Archive to output file '"
 -                + statefile + "' - See console error log for details";
 -      }
 -      else
 -      {
 -        errorMessage += "(Didn't write Jalview Archive to output file '"
 -                + statefile + ")";
 -      }
 -      e.printStackTrace();
 -    } finally
 -    {
 -      if (fos != null)
 -      {
 -        try
 -        {
 -          fos.close();
 -        } catch (IOException e)
 -        {
 -          // ignore
 -        }
 -      }
 -    }
 -    reportErrors();
 -  }
 -
 -  /**
 -   * Writes a jalview project archive to the given Jar output stream.
 -   * 
 -   * @param jout
 -   */
 -  public void saveState(JarOutputStream jout)
 -  {
 -    AlignFrame[] frames = Desktop.getAlignFrames();
 -
 -    if (frames == null)
 -    {
 -      return;
 -    }
 -    saveAllFrames(Arrays.asList(frames), jout);
 -  }
 -
 -  /**
 -   * core method for storing state for a set of AlignFrames.
 -   * 
 -   * @param frames
 -   *          - frames involving all data to be exported (including containing
 -   *          splitframes)
 -   * @param jout
 -   *          - project output stream
 -   */
 -  private void saveAllFrames(List<AlignFrame> frames, JarOutputStream jout)
 -  {
 -    Hashtable<String, AlignFrame> dsses = new Hashtable<>();
 -
 -    /*
 -     * ensure cached data is clear before starting
 -     */
 -    // todo tidy up seqRefIds, seqsToIds initialisation / reset
 -    rnaSessions.clear();
 -    splitFrameCandidates.clear();
 -
 -    try
 -    {
 -
 -      // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
 -      // //////////////////////////////////////////////////
 -
 -      List<String> shortNames = new ArrayList<>();
 -      List<String> viewIds = new ArrayList<>();
 -
 -      // REVERSE ORDER
 -      for (int i = frames.size() - 1; i > -1; i--)
 -      {
 -        AlignFrame af = frames.get(i);
 -        // skip ?
 -        if (skipList != null && skipList
 -                .containsKey(af.getViewport().getSequenceSetId()))
 -        {
 -          continue;
 -        }
 -
 -        String shortName = makeFilename(af, shortNames);
 -
 -        int apSize = af.getAlignPanels().size();
 -
 -        for (int ap = 0; ap < apSize; ap++)
 -        {
 -          AlignmentPanel apanel = (AlignmentPanel) af.getAlignPanels()
 -                  .get(ap);
 -          String fileName = apSize == 1 ? shortName : ap + shortName;
 -          if (!fileName.endsWith(".xml"))
 -          {
 -            fileName = fileName + ".xml";
 -          }
 -
 -          saveState(apanel, fileName, jout, viewIds);
 -
 -          String dssid = getDatasetIdRef(
 -                  af.getViewport().getAlignment().getDataset());
 -          if (!dsses.containsKey(dssid))
 -          {
 -            dsses.put(dssid, af);
 -          }
 -        }
 -      }
 -
 -      writeDatasetFor(dsses, "" + jout.hashCode() + " " + uniqueSetSuffix,
 -              jout);
 -
 -      try
 -      {
 -        jout.flush();
 -      } catch (Exception foo)
 -      {
 -      }
 -      jout.close();
 -    } catch (Exception ex)
 -    {
 -      // TODO: inform user of the problem - they need to know if their data was
 -      // not saved !
 -      if (errorMessage == null)
 -      {
 -        errorMessage = "Couldn't write Jalview Archive - see error output for details";
 -      }
 -      ex.printStackTrace();
 -    }
 -  }
 -
 -  /**
 -   * Generates a distinct file name, based on the title of the AlignFrame, by
 -   * appending _n for increasing n until an unused name is generated. The new
 -   * name (without its extension) is added to the list.
 -   * 
 -   * @param af
 -   * @param namesUsed
 -   * @return the generated name, with .xml extension
 -   */
 -  protected String makeFilename(AlignFrame af, List<String> namesUsed)
 -  {
 -    String shortName = af.getTitle();
 -
 -    if (shortName.indexOf(File.separatorChar) > -1)
 -    {
 -      shortName = shortName
 -              .substring(shortName.lastIndexOf(File.separatorChar) + 1);
 -    }
 -
 -    int count = 1;
 -
 -    while (namesUsed.contains(shortName))
 -    {
 -      if (shortName.endsWith("_" + (count - 1)))
 -      {
 -        shortName = shortName.substring(0, shortName.lastIndexOf("_"));
 -      }
 -
 -      shortName = shortName.concat("_" + count);
 -      count++;
 -    }
 -
 -    namesUsed.add(shortName);
 -
 -    if (!shortName.endsWith(".xml"))
 -    {
 -      shortName = shortName + ".xml";
 -    }
 -    return shortName;
 -  }
 -
 -  // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
 -  public boolean saveAlignment(AlignFrame af, String jarFile,
 -          String fileName)
 -  {
 -    try
 -    {
 -      // create backupfiles object and get new temp filename destination
 -      boolean doBackup = BackupFiles.getEnabled();
 -      BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
 -      FileOutputStream fos = new FileOutputStream(
 -              doBackup ? backupfiles.getTempFilePath() : jarFile);
 -
 -      JarOutputStream jout = new JarOutputStream(fos);
 -      List<AlignFrame> frames = new ArrayList<>();
 -
 -      // resolve splitframes
 -      if (af.getViewport().getCodingComplement() != null)
 -      {
 -        frames = ((SplitFrame) af.getSplitViewContainer()).getAlignFrames();
 -      }
 -      else
 -      {
 -        frames.add(af);
 -      }
 -      saveAllFrames(frames, jout);
 -      try
 -      {
 -        jout.flush();
 -      } catch (Exception foo)
 -      {
 -      }
 -      jout.close();
 -      boolean success = true;
 -
 -      if (doBackup)
 -      {
 -        backupfiles.setWriteSuccess(success);
 -        success = backupfiles.rollBackupsAndRenameTempFile();
 -      }
 -
 -      return success;
 -    } catch (Exception ex)
 -    {
 -      errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
 -      ex.printStackTrace();
 -      return false;
 -    }
 -  }
 -
 -  private void writeDatasetFor(Hashtable<String, AlignFrame> dsses,
 -          String fileName, JarOutputStream jout)
 -  {
 -
 -    for (String dssids : dsses.keySet())
 -    {
 -      AlignFrame _af = dsses.get(dssids);
 -      String jfileName = fileName + " Dataset for " + _af.getTitle();
 -      if (!jfileName.endsWith(".xml"))
 -      {
 -        jfileName = jfileName + ".xml";
 -      }
 -      saveState(_af.alignPanel, jfileName, true, jout, null);
 -    }
 -  }
 -
 -  /**
 -   * create a JalviewModel from an alignment view and marshall it to a
 -   * JarOutputStream
 -   * 
 -   * @param ap
 -   *          panel to create jalview model for
 -   * @param fileName
 -   *          name of alignment panel written to output stream
 -   * @param jout
 -   *          jar output stream
 -   * @param viewIds
 -   * @param out
 -   *          jar entry name
 -   */
 -  public JalviewModel saveState(AlignmentPanel ap, String fileName,
 -          JarOutputStream jout, List<String> viewIds)
 -  {
 -    return saveState(ap, fileName, false, jout, viewIds);
 -  }
 -
 -  /**
 -   * create a JalviewModel from an alignment view and marshall it to a
 -   * JarOutputStream
 -   * 
 -   * @param ap
 -   *          panel to create jalview model for
 -   * @param fileName
 -   *          name of alignment panel written to output stream
 -   * @param storeDS
 -   *          when true, only write the dataset for the alignment, not the data
 -   *          associated with the view.
 -   * @param jout
 -   *          jar output stream
 -   * @param out
 -   *          jar entry name
 -   */
 -  public JalviewModel saveState(AlignmentPanel ap, String fileName,
 -          boolean storeDS, JarOutputStream jout, List<String> viewIds)
 -  {
 -    if (viewIds == null)
 -    {
 -      viewIds = new ArrayList<>();
 -    }
 -
 -    initSeqRefs();
 -
 -    List<UserColourScheme> userColours = new ArrayList<>();
 -
 -    AlignViewport av = ap.av;
 -    ViewportRanges vpRanges = av.getRanges();
 -
 -    final ObjectFactory objectFactory = new ObjectFactory();
 -    JalviewModel object = objectFactory.createJalviewModel();
 -    object.setVamsasModel(new VAMSAS());
 -
 -    // object.setCreationDate(new java.util.Date(System.currentTimeMillis()));
 -    try
 -    {
 -      GregorianCalendar c = new GregorianCalendar();
 -      DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
 -      XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(c);// gregorianCalendar);
 -      object.setCreationDate(now);
 -    } catch (DatatypeConfigurationException e)
 -    {
 -      System.err.println("error writing date: " + e.toString());
 -    }
 -    object.setVersion(Cache.getDefault("VERSION", "Development Build"));
 -
 -    /**
 -     * rjal is full height alignment, jal is actual alignment with full metadata
 -     * but excludes hidden sequences.
 -     */
 -    jalview.datamodel.AlignmentI rjal = av.getAlignment(), jal = rjal;
 -
 -    if (av.hasHiddenRows())
 -    {
 -      rjal = jal.getHiddenSequences().getFullAlignment();
 -    }
 -
 -    SequenceSet vamsasSet = new SequenceSet();
 -    Sequence vamsasSeq;
 -    // JalviewModelSequence jms = new JalviewModelSequence();
 -
 -    vamsasSet.setGapChar(jal.getGapCharacter() + "");
 -
 -    if (jal.getDataset() != null)
 -    {
 -      // dataset id is the dataset's hashcode
 -      vamsasSet.setDatasetId(getDatasetIdRef(jal.getDataset()));
 -      if (storeDS)
 -      {
 -        // switch jal and the dataset
 -        jal = jal.getDataset();
 -        rjal = jal;
 -      }
 -    }
 -    if (jal.getProperties() != null)
 -    {
 -      Enumeration en = jal.getProperties().keys();
 -      while (en.hasMoreElements())
 -      {
 -        String key = en.nextElement().toString();
 -        SequenceSetProperties ssp = new SequenceSetProperties();
 -        ssp.setKey(key);
 -        ssp.setValue(jal.getProperties().get(key).toString());
 -        // vamsasSet.addSequenceSetProperties(ssp);
 -        vamsasSet.getSequenceSetProperties().add(ssp);
 -      }
 -    }
 -
 -    JSeq jseq;
 -    Set<String> calcIdSet = new HashSet<>();
 -    // record the set of vamsas sequence XML POJO we create.
 -    HashMap<String, Sequence> vamsasSetIds = new HashMap<>();
 -    // SAVE SEQUENCES
 -    for (final SequenceI jds : rjal.getSequences())
 -    {
 -      final SequenceI jdatasq = jds.getDatasetSequence() == null ? jds
 -              : jds.getDatasetSequence();
 -      String id = seqHash(jds);
 -      if (vamsasSetIds.get(id) == null)
 -      {
 -        if (seqRefIds.get(id) != null && !storeDS)
 -        {
 -          // This happens for two reasons: 1. multiple views are being
 -          // serialised.
 -          // 2. the hashCode has collided with another sequence's code. This
 -          // DOES
 -          // HAPPEN! (PF00072.15.stk does this)
 -          // JBPNote: Uncomment to debug writing out of files that do not read
 -          // back in due to ArrayOutOfBoundExceptions.
 -          // System.err.println("vamsasSeq backref: "+id+"");
 -          // System.err.println(jds.getName()+"
 -          // "+jds.getStart()+"-"+jds.getEnd()+" "+jds.getSequenceAsString());
 -          // System.err.println("Hashcode: "+seqHash(jds));
 -          // SequenceI rsq = (SequenceI) seqRefIds.get(id + "");
 -          // System.err.println(rsq.getName()+"
 -          // "+rsq.getStart()+"-"+rsq.getEnd()+" "+rsq.getSequenceAsString());
 -          // System.err.println("Hashcode: "+seqHash(rsq));
 -        }
 -        else
 -        {
 -          vamsasSeq = createVamsasSequence(id, jds);
 -          // vamsasSet.addSequence(vamsasSeq);
 -          vamsasSet.getSequence().add(vamsasSeq);
 -          vamsasSetIds.put(id, vamsasSeq);
 -          seqRefIds.put(id, jds);
 -        }
 -      }
 -      jseq = new JSeq();
 -      jseq.setStart(jds.getStart());
 -      jseq.setEnd(jds.getEnd());
 -      jseq.setColour(av.getSequenceColour(jds).getRGB());
 -
 -      jseq.setId(id); // jseq id should be a string not a number
 -      if (!storeDS)
 -      {
 -        // Store any sequences this sequence represents
 -        if (av.hasHiddenRows())
 -        {
 -          // use rjal, contains the full height alignment
 -          jseq.setHidden(
 -                  av.getAlignment().getHiddenSequences().isHidden(jds));
 -
 -          if (av.isHiddenRepSequence(jds))
 -          {
 -            jalview.datamodel.SequenceI[] reps = av
 -                    .getRepresentedSequences(jds).getSequencesInOrder(rjal);
 -
 -            for (int h = 0; h < reps.length; h++)
 -            {
 -              if (reps[h] != jds)
 -              {
 -                // jseq.addHiddenSequences(rjal.findIndex(reps[h]));
 -                jseq.getHiddenSequences().add(rjal.findIndex(reps[h]));
 -              }
 -            }
 -          }
 -        }
 -        // mark sequence as reference - if it is the reference for this view
 -        if (jal.hasSeqrep())
 -        {
 -          jseq.setViewreference(jds == jal.getSeqrep());
 -        }
 -      }
 -
 -      // TODO: omit sequence features from each alignment view's XML dump if we
 -      // are storing dataset
 -      List<SequenceFeature> sfs = jds.getSequenceFeatures();
 -      for (SequenceFeature sf : sfs)
 -      {
 -        // Features features = new Features();
 -        Feature features = new Feature();
 -
 -        features.setBegin(sf.getBegin());
 -        features.setEnd(sf.getEnd());
 -        features.setDescription(sf.getDescription());
 -        features.setType(sf.getType());
 -        features.setFeatureGroup(sf.getFeatureGroup());
 -        features.setScore(sf.getScore());
 -        if (sf.links != null)
 -        {
 -          for (int l = 0; l < sf.links.size(); l++)
 -          {
 -            OtherData keyValue = new OtherData();
 -            keyValue.setKey("LINK_" + l);
 -            keyValue.setValue(sf.links.elementAt(l).toString());
 -            // features.addOtherData(keyValue);
 -            features.getOtherData().add(keyValue);
 -          }
 -        }
 -        if (sf.otherDetails != null)
 -        {
 -          /*
 -           * save feature attributes, which may be simple strings or
 -           * map valued (have sub-attributes)
 -           */
 -          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
 -          {
 -            String key = entry.getKey();
 -            Object value = entry.getValue();
 -            if (value instanceof Map<?, ?>)
 -            {
 -              for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
 -                      .entrySet())
 -              {
 -                OtherData otherData = new OtherData();
 -                otherData.setKey(key);
 -                otherData.setKey2(subAttribute.getKey());
 -                otherData.setValue(subAttribute.getValue().toString());
 -                // features.addOtherData(otherData);
 -                features.getOtherData().add(otherData);
 -              }
 -            }
 -            else
 -            {
 -              OtherData otherData = new OtherData();
 -              otherData.setKey(key);
 -              otherData.setValue(value.toString());
 -              // features.addOtherData(otherData);
 -              features.getOtherData().add(otherData);
 -            }
 -          }
 -        }
 -
 -        // jseq.addFeatures(features);
 -        jseq.getFeatures().add(features);
 -      }
 -
 -      if (jdatasq.getAllPDBEntries() != null)
 -      {
 -        Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
 -        while (en.hasMoreElements())
 -        {
 -          Pdbids pdb = new Pdbids();
 -          jalview.datamodel.PDBEntry entry = en.nextElement();
 -
 -          String pdbId = entry.getId();
 -          pdb.setId(pdbId);
 -          pdb.setType(entry.getType());
 -
 -          /*
 -           * Store any structure views associated with this sequence. This
 -           * section copes with duplicate entries in the project, so a dataset
 -           * only view *should* be coped with sensibly.
 -           */
 -          // This must have been loaded, is it still visible?
 -          JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 -          String matchedFile = null;
 -          for (int f = frames.length - 1; f > -1; f--)
 -          {
 -            if (frames[f] instanceof StructureViewerBase)
 -            {
 -              StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
 -              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);
 -                File viewerState = viewFrame.saveSession();
 -                if (viewerState != null)
 -                {
 -                  copyFileToJar(jout, viewerState.getPath(),
 -                          getViewerJarEntryName(viewId), viewerType);
 -                }
 -                else
 -                {
 -                  Console.error(
 -                          "Failed to save viewer state for " + viewerType);
 -                }
 -              }
 -            }
 -          }
 -
 -          if (matchedFile != null || entry.getFile() != null)
 -          {
 -            if (entry.getFile() != null)
 -            {
 -              // use entry's file
 -              matchedFile = entry.getFile();
 -            }
 -            pdb.setFile(matchedFile); // entry.getFile());
 -            if (pdbfiles == null)
 -            {
 -              pdbfiles = new ArrayList<>();
 -            }
 -
 -            if (!pdbfiles.contains(pdbId))
 -            {
 -              pdbfiles.add(pdbId);
 -              copyFileToJar(jout, matchedFile, pdbId, pdbId);
 -            }
 -          }
 -
 -          Enumeration<String> props = entry.getProperties();
 -          if (props.hasMoreElements())
 -          {
 -            // PdbentryItem item = new PdbentryItem();
 -            while (props.hasMoreElements())
 -            {
 -              Property prop = new Property();
 -              String key = props.nextElement();
 -              prop.setName(key);
 -              prop.setValue(entry.getProperty(key).toString());
 -              // item.addProperty(prop);
 -              pdb.getProperty().add(prop);
 -            }
 -            // pdb.addPdbentryItem(item);
 -          }
 -
 -          // jseq.addPdbids(pdb);
 -          jseq.getPdbids().add(pdb);
 -        }
 -      }
 -
 -      saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
 -
 -      // jms.addJSeq(jseq);
 -      object.getJSeq().add(jseq);
 -    }
 -
 -    if (!storeDS && av.hasHiddenRows())
 -    {
 -      jal = av.getAlignment();
 -    }
 -    // SAVE MAPPINGS
 -    // FOR DATASET
 -    if (storeDS && jal.getCodonFrames() != null)
 -    {
 -      List<AlignedCodonFrame> jac = jal.getCodonFrames();
 -      for (AlignedCodonFrame acf : jac)
 -      {
 -        AlcodonFrame alc = new AlcodonFrame();
 -        if (acf.getProtMappings() != null
 -                && acf.getProtMappings().length > 0)
 -        {
 -          boolean hasMap = false;
 -          SequenceI[] dnas = acf.getdnaSeqs();
 -          jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
 -          for (int m = 0; m < pmaps.length; m++)
 -          {
 -            AlcodMap alcmap = new AlcodMap();
 -            alcmap.setDnasq(seqHash(dnas[m]));
 -            alcmap.setMapping(
 -                    createVamsasMapping(pmaps[m], dnas[m], null, false));
 -            // alc.addAlcodMap(alcmap);
 -            alc.getAlcodMap().add(alcmap);
 -            hasMap = true;
 -          }
 -          if (hasMap)
 -          {
 -            // vamsasSet.addAlcodonFrame(alc);
 -            vamsasSet.getAlcodonFrame().add(alc);
 -          }
 -        }
 -        // TODO: delete this ? dead code from 2.8.3->2.9 ?
 -        // {
 -        // AlcodonFrame alc = new AlcodonFrame();
 -        // vamsasSet.addAlcodonFrame(alc);
 -        // for (int p = 0; p < acf.aaWidth; p++)
 -        // {
 -        // Alcodon cmap = new Alcodon();
 -        // if (acf.codons[p] != null)
 -        // {
 -        // // Null codons indicate a gapped column in the translated peptide
 -        // // alignment.
 -        // cmap.setPos1(acf.codons[p][0]);
 -        // cmap.setPos2(acf.codons[p][1]);
 -        // cmap.setPos3(acf.codons[p][2]);
 -        // }
 -        // alc.addAlcodon(cmap);
 -        // }
 -        // if (acf.getProtMappings() != null
 -        // && acf.getProtMappings().length > 0)
 -        // {
 -        // SequenceI[] dnas = acf.getdnaSeqs();
 -        // jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
 -        // for (int m = 0; m < pmaps.length; m++)
 -        // {
 -        // AlcodMap alcmap = new AlcodMap();
 -        // alcmap.setDnasq(seqHash(dnas[m]));
 -        // alcmap.setMapping(createVamsasMapping(pmaps[m], dnas[m], null,
 -        // false));
 -        // alc.addAlcodMap(alcmap);
 -        // }
 -        // }
 -      }
 -    }
 -
 -    // SAVE TREES
 -    // /////////////////////////////////
 -    if (!storeDS && av.getCurrentTree() != null)
 -    {
 -      // FIND ANY ASSOCIATED TREES
 -      // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
 -      if (Desktop.desktop != null)
 -      {
 -        JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 -
 -        for (int t = 0; t < frames.length; t++)
 -        {
 -          if (frames[t] instanceof TreePanel)
 -          {
 -            TreePanel tp = (TreePanel) frames[t];
 -
 -            if (tp.getTreeCanvas().getViewport().getAlignment() == jal)
 -            {
 -              JalviewModel.Tree tree = new JalviewModel.Tree();
 -              tree.setTitle(tp.getTitle());
 -              tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
 -              tree.setNewick(tp.getTree().print());
 -              tree.setThreshold(tp.getTreeCanvas().getThreshold());
 -
 -              tree.setFitToWindow(tp.fitToWindow.getState());
 -              tree.setFontName(tp.getTreeFont().getName());
 -              tree.setFontSize(tp.getTreeFont().getSize());
 -              tree.setFontStyle(tp.getTreeFont().getStyle());
 -              tree.setMarkUnlinked(tp.placeholdersMenu.getState());
 -
 -              tree.setShowBootstrap(tp.bootstrapMenu.getState());
 -              tree.setShowDistances(tp.distanceMenu.getState());
 -
 -              tree.setHeight(tp.getHeight());
 -              tree.setWidth(tp.getWidth());
 -              tree.setXpos(tp.getX());
 -              tree.setYpos(tp.getY());
 -              tree.setId(makeHashCode(tp, null));
 -              tree.setLinkToAllViews(
 -                      tp.getTreeCanvas().isApplyToAllViews());
 -
 -              // jms.addTree(tree);
 -              object.getTree().add(tree);
 -            }
 -          }
 -        }
 -      }
 -    }
 -
 -    /*
 -     * save PCA viewers
 -     */
 -    if (!storeDS && Desktop.desktop != null)
 -    {
 -      for (JInternalFrame frame : Desktop.desktop.getAllFrames())
 -      {
 -        if (frame instanceof PCAPanel)
 -        {
 -          PCAPanel panel = (PCAPanel) frame;
 -          if (panel.getAlignViewport().getAlignment() == jal)
 -          {
 -            savePCA(panel, object);
 -          }
 -        }
 -      }
 -    }
 -
 -    // SAVE ANNOTATIONS
 -    /**
 -     * store forward refs from an annotationRow to any groups
 -     */
 -    IdentityHashMap<SequenceGroup, String> groupRefs = new IdentityHashMap<>();
 -    if (storeDS)
 -    {
 -      for (SequenceI sq : jal.getSequences())
 -      {
 -        // Store annotation on dataset sequences only
 -        AlignmentAnnotation[] aa = sq.getAnnotation();
 -        if (aa != null && aa.length > 0)
 -        {
 -          storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
 -                  vamsasSet);
 -        }
 -      }
 -    }
 -    else
 -    {
 -      if (jal.getAlignmentAnnotation() != null)
 -      {
 -        // Store the annotation shown on the alignment.
 -        AlignmentAnnotation[] aa = jal.getAlignmentAnnotation();
 -        storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
 -                vamsasSet);
 -      }
 -    }
 -    // SAVE GROUPS
 -    if (jal.getGroups() != null)
 -    {
 -      JGroup[] groups = new JGroup[jal.getGroups().size()];
 -      int i = -1;
 -      for (jalview.datamodel.SequenceGroup sg : jal.getGroups())
 -      {
 -        JGroup jGroup = new JGroup();
 -        groups[++i] = jGroup;
 -
 -        jGroup.setStart(sg.getStartRes());
 -        jGroup.setEnd(sg.getEndRes());
 -        jGroup.setName(sg.getName());
 -        if (groupRefs.containsKey(sg))
 -        {
 -          // group has references so set its ID field
 -          jGroup.setId(groupRefs.get(sg));
 -        }
 -        ColourSchemeI colourScheme = sg.getColourScheme();
 -        if (colourScheme != null)
 -        {
 -          ResidueShaderI groupColourScheme = sg.getGroupColourScheme();
 -          if (groupColourScheme.conservationApplied())
 -          {
 -            jGroup.setConsThreshold(groupColourScheme.getConservationInc());
 -
 -            if (colourScheme instanceof jalview.schemes.UserColourScheme)
 -            {
 -              jGroup.setColour(setUserColourScheme(colourScheme,
 -                      userColours, object));
 -            }
 -            else
 -            {
 -              jGroup.setColour(colourScheme.getSchemeName());
 -            }
 -          }
 -          else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
 -          {
 -            jGroup.setColour("AnnotationColourGradient");
 -            jGroup.setAnnotationColours(constructAnnotationColours(
 -                    (jalview.schemes.AnnotationColourGradient) colourScheme,
 -                    userColours, object));
 -          }
 -          else if (colourScheme instanceof jalview.schemes.UserColourScheme)
 -          {
 -            jGroup.setColour(
 -                    setUserColourScheme(colourScheme, userColours, object));
 -          }
 -          else
 -          {
 -            jGroup.setColour(colourScheme.getSchemeName());
 -          }
 -
 -          jGroup.setPidThreshold(groupColourScheme.getThreshold());
 -        }
 -
 -        jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
 -        jGroup.setDisplayBoxes(sg.getDisplayBoxes());
 -        jGroup.setDisplayText(sg.getDisplayText());
 -        jGroup.setColourText(sg.getColourText());
 -        jGroup.setTextCol1(sg.textColour.getRGB());
 -        jGroup.setTextCol2(sg.textColour2.getRGB());
 -        jGroup.setTextColThreshold(sg.thresholdTextColour);
 -        jGroup.setShowUnconserved(sg.getShowNonconserved());
 -        jGroup.setIgnoreGapsinConsensus(sg.getIgnoreGapsConsensus());
 -        jGroup.setShowConsensusHistogram(sg.isShowConsensusHistogram());
 -        jGroup.setShowSequenceLogo(sg.isShowSequenceLogo());
 -        jGroup.setNormaliseSequenceLogo(sg.isNormaliseSequenceLogo());
 -        for (SequenceI seq : sg.getSequences())
 -        {
 -          // jGroup.addSeq(seqHash(seq));
 -          jGroup.getSeq().add(seqHash(seq));
 -        }
 -      }
 -
 -      // jms.setJGroup(groups);
 -      Object group;
 -      for (JGroup grp : groups)
 -      {
 -        object.getJGroup().add(grp);
 -      }
 -    }
 -    if (!storeDS)
 -    {
 -      // /////////SAVE VIEWPORT
 -      Viewport view = new Viewport();
 -      view.setTitle(ap.alignFrame.getTitle());
 -      view.setSequenceSetId(
 -              makeHashCode(av.getSequenceSetId(), av.getSequenceSetId()));
 -      view.setId(av.getViewId());
 -      if (av.getCodingComplement() != null)
 -      {
 -        view.setComplementId(av.getCodingComplement().getViewId());
 -      }
 -      view.setViewName(av.getViewName());
 -      view.setGatheredViews(av.isGatherViewsHere());
 -
 -      Rectangle size = ap.av.getExplodedGeometry();
 -      Rectangle position = size;
 -      if (size == null)
 -      {
 -        size = ap.alignFrame.getBounds();
 -        if (av.getCodingComplement() != null)
 -        {
 -          position = ((SplitFrame) ap.alignFrame.getSplitViewContainer())
 -                  .getBounds();
 -        }
 -        else
 -        {
 -          position = size;
 -        }
 -      }
 -      view.setXpos(position.x);
 -      view.setYpos(position.y);
 -
 -      view.setWidth(size.width);
 -      view.setHeight(size.height);
 -
 -      view.setStartRes(vpRanges.getStartRes());
 -      view.setStartSeq(vpRanges.getStartSeq());
 -
 -      if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
 -      {
 -        view.setBgColour(setUserColourScheme(av.getGlobalColourScheme(),
 -                userColours, object));
 -      }
 -      else if (av
 -              .getGlobalColourScheme() instanceof jalview.schemes.AnnotationColourGradient)
 -      {
 -        AnnotationColourScheme ac = constructAnnotationColours(
 -                (jalview.schemes.AnnotationColourGradient) av
 -                        .getGlobalColourScheme(),
 -                userColours, object);
 -
 -        view.setAnnotationColours(ac);
 -        view.setBgColour("AnnotationColourGradient");
 -      }
 -      else
 -      {
 -        view.setBgColour(ColourSchemeProperty
 -                .getColourName(av.getGlobalColourScheme()));
 -      }
 -
 -      ResidueShaderI vcs = av.getResidueShading();
 -      ColourSchemeI cs = av.getGlobalColourScheme();
 -
 -      if (cs != null)
 -      {
 -        if (vcs.conservationApplied())
 -        {
 -          view.setConsThreshold(vcs.getConservationInc());
 -          if (cs instanceof jalview.schemes.UserColourScheme)
 -          {
 -            view.setBgColour(setUserColourScheme(cs, userColours, object));
 -          }
 -        }
 -        view.setPidThreshold(vcs.getThreshold());
 -      }
 -
 -      view.setConservationSelected(av.getConservationSelected());
 -      view.setPidSelected(av.getAbovePIDThreshold());
 -      final Font font = av.getFont();
 -      view.setFontName(font.getName());
 -      view.setFontSize(font.getSize());
 -      view.setFontStyle(font.getStyle());
 -      view.setScaleProteinAsCdna(av.getViewStyle().isScaleProteinAsCdna());
 -      view.setRenderGaps(av.isRenderGaps());
 -      view.setShowAnnotation(av.isShowAnnotation());
 -      view.setShowBoxes(av.getShowBoxes());
 -      view.setShowColourText(av.getColourText());
 -      view.setShowFullId(av.getShowJVSuffix());
 -      view.setRightAlignIds(av.isRightAlignIds());
 -      view.setShowSequenceFeatures(av.isShowSequenceFeatures());
 -      view.setShowText(av.getShowText());
 -      view.setShowUnconserved(av.getShowUnconserved());
 -      view.setWrapAlignment(av.getWrapAlignment());
 -      view.setTextCol1(av.getTextColour().getRGB());
 -      view.setTextCol2(av.getTextColour2().getRGB());
 -      view.setTextColThreshold(av.getThresholdTextColour());
 -      view.setShowConsensusHistogram(av.isShowConsensusHistogram());
 -      view.setShowSequenceLogo(av.isShowSequenceLogo());
 -      view.setNormaliseSequenceLogo(av.isNormaliseSequenceLogo());
 -      view.setShowGroupConsensus(av.isShowGroupConsensus());
 -      view.setShowGroupConservation(av.isShowGroupConservation());
 -      view.setShowNPfeatureTooltip(av.isShowNPFeats());
 -      view.setShowDbRefTooltip(av.isShowDBRefs());
 -      view.setFollowHighlight(av.isFollowHighlight());
 -      view.setFollowSelection(av.followSelection);
 -      view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
 -      view.setShowComplementFeatures(av.isShowComplementFeatures());
 -      view.setShowComplementFeaturesOnTop(
 -              av.isShowComplementFeaturesOnTop());
 -      if (av.getFeaturesDisplayed() != null)
 -      {
 -        FeatureSettings fs = new FeatureSettings();
 -
 -        FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
 -                .getFeatureRenderer();
 -        String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
 -
 -        Vector<String> settingsAdded = new Vector<>();
 -        if (renderOrder != null)
 -        {
 -          for (String featureType : renderOrder)
 -          {
 -            FeatureSettings.Setting setting = new FeatureSettings.Setting();
 -            setting.setType(featureType);
 -
 -            /*
 -             * save any filter for the feature type
 -             */
 -            FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
 -            if (filter != null)
 -            {
 -              Iterator<FeatureMatcherI> filters = filter.getMatchers()
 -                      .iterator();
 -              FeatureMatcherI firstFilter = filters.next();
 -              setting.setMatcherSet(Jalview2XML.marshalFilter(firstFilter,
 -                      filters, filter.isAnded()));
 -            }
 -
 -            /*
 -             * save colour scheme for the feature type
 -             */
 -            FeatureColourI fcol = fr.getFeatureStyle(featureType);
 -            if (!fcol.isSimpleColour())
 -            {
 -              setting.setColour(fcol.getMaxColour().getRGB());
 -              setting.setMincolour(fcol.getMinColour().getRGB());
 -              setting.setMin(fcol.getMin());
 -              setting.setMax(fcol.getMax());
 -              setting.setColourByLabel(fcol.isColourByLabel());
 -              if (fcol.isColourByAttribute())
 -              {
 -                String[] attName = fcol.getAttributeName();
 -                setting.getAttributeName().add(attName[0]);
 -                if (attName.length > 1)
 -                {
 -                  setting.getAttributeName().add(attName[1]);
 -                }
 -              }
 -              setting.setAutoScale(fcol.isAutoScaled());
 -              setting.setThreshold(fcol.getThreshold());
 -              Color noColour = fcol.getNoColour();
 -              if (noColour == null)
 -              {
 -                setting.setNoValueColour(NoValueColour.NONE);
 -              }
 -              else if (noColour.equals(fcol.getMaxColour()))
 -              {
 -                setting.setNoValueColour(NoValueColour.MAX);
 -              }
 -              else
 -              {
 -                setting.setNoValueColour(NoValueColour.MIN);
 -              }
 -              // -1 = No threshold, 0 = Below, 1 = Above
 -              setting.setThreshstate(fcol.isAboveThreshold() ? 1
 -                      : (fcol.isBelowThreshold() ? 0 : -1));
 -            }
 -            else
 -            {
 -              setting.setColour(fcol.getColour().getRGB());
 -            }
 -
 -            setting.setDisplay(
 -                    av.getFeaturesDisplayed().isVisible(featureType));
 -            float rorder = fr.getOrder(featureType);
 -            if (rorder > -1)
 -            {
 -              setting.setOrder(rorder);
 -            }
 -            /// fs.addSetting(setting);
 -            fs.getSetting().add(setting);
 -            settingsAdded.addElement(featureType);
 -          }
 -        }
 -
 -        // is groups actually supposed to be a map here ?
 -        Iterator<String> en = fr.getFeatureGroups().iterator();
 -        Vector<String> groupsAdded = new Vector<>();
 -        while (en.hasNext())
 -        {
 -          String grp = en.next();
 -          if (groupsAdded.contains(grp))
 -          {
 -            continue;
 -          }
 -          Group g = new Group();
 -          g.setName(grp);
 -          g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
 -                  .booleanValue());
 -          // fs.addGroup(g);
 -          fs.getGroup().add(g);
 -          groupsAdded.addElement(grp);
 -        }
 -        // jms.setFeatureSettings(fs);
 -        object.setFeatureSettings(fs);
 -      }
 -
 -      if (av.hasHiddenColumns())
 -      {
 -        jalview.datamodel.HiddenColumns hidden = av.getAlignment()
 -                .getHiddenColumns();
 -        if (hidden == null)
 -        {
 -          Console.warn(
 -                  "REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
 -        }
 -        else
 -        {
 -          Iterator<int[]> hiddenRegions = hidden.iterator();
 -          while (hiddenRegions.hasNext())
 -          {
 -            int[] region = hiddenRegions.next();
 -            HiddenColumns hc = new HiddenColumns();
 -            hc.setStart(region[0]);
 -            hc.setEnd(region[1]);
 -            // view.addHiddenColumns(hc);
 -            view.getHiddenColumns().add(hc);
 -          }
 -        }
 -      }
 -      if (calcIdSet.size() > 0)
 -      {
 -        for (String calcId : calcIdSet)
 -        {
 -          if (calcId.trim().length() > 0)
 -          {
 -            CalcIdParam cidp = createCalcIdParam(calcId, av);
 -            // Some calcIds have no parameters.
 -            if (cidp != null)
 -            {
 -              // view.addCalcIdParam(cidp);
 -              view.getCalcIdParam().add(cidp);
 -            }
 -          }
 -        }
 -      }
 -
 -      // jms.addViewport(view);
 -      object.getViewport().add(view);
 -    }
 -    // object.setJalviewModelSequence(jms);
 -    // object.getVamsasModel().addSequenceSet(vamsasSet);
 -    object.getVamsasModel().getSequenceSet().add(vamsasSet);
 -
 -    if (jout != null && fileName != null)
 -    {
 -      // We may not want to write the object to disk,
 -      // eg we can copy the alignViewport to a new view object
 -      // using save and then load
 -      try
 -      {
 -        fileName = fileName.replace('\\', '/');
 -        System.out.println("Writing jar entry " + fileName);
 -        JarEntry entry = new JarEntry(fileName);
 -        jout.putNextEntry(entry);
 -        PrintWriter pout = new PrintWriter(
 -                new OutputStreamWriter(jout, UTF_8));
 -        JAXBContext jaxbContext = JAXBContext
 -                .newInstance(JalviewModel.class);
 -        Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
 -
 -        // output pretty printed
 -        // jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
 -        jaxbMarshaller.marshal(
 -                new ObjectFactory().createJalviewModel(object), pout);
 -
 -        // jaxbMarshaller.marshal(object, pout);
 -        // marshaller.marshal(object);
 -        pout.flush();
 -        jout.closeEntry();
 -      } catch (Exception ex)
 -      {
 -        // TODO: raise error in GUI if marshalling failed.
 -        System.err.println("Error writing Jalview project");
 -        ex.printStackTrace();
 -      }
 -    }
 -    return object;
 -  }
 -
 -  /**
 -   * Writes PCA viewer attributes and computed values to an XML model object and
 -   * adds it to the JalviewModel. Any exceptions are reported by logging.
 -   */
 -  protected void savePCA(PCAPanel panel, JalviewModel object)
 -  {
 -    try
 -    {
 -      PcaViewer viewer = new PcaViewer();
 -      viewer.setHeight(panel.getHeight());
 -      viewer.setWidth(panel.getWidth());
 -      viewer.setXpos(panel.getX());
 -      viewer.setYpos(panel.getY());
 -      viewer.setTitle(panel.getTitle());
 -      PCAModel pcaModel = panel.getPcaModel();
 -      viewer.setScoreModelName(pcaModel.getScoreModelName());
 -      viewer.setXDim(panel.getSelectedDimensionIndex(X));
 -      viewer.setYDim(panel.getSelectedDimensionIndex(Y));
 -      viewer.setZDim(panel.getSelectedDimensionIndex(Z));
 -      viewer.setBgColour(
 -              panel.getRotatableCanvas().getBackgroundColour().getRGB());
 -      viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
 -      float[] spMin = panel.getRotatableCanvas().getSeqMin();
 -      SeqPointMin spmin = new SeqPointMin();
 -      spmin.setXPos(spMin[0]);
 -      spmin.setYPos(spMin[1]);
 -      spmin.setZPos(spMin[2]);
 -      viewer.setSeqPointMin(spmin);
 -      float[] spMax = panel.getRotatableCanvas().getSeqMax();
 -      SeqPointMax spmax = new SeqPointMax();
 -      spmax.setXPos(spMax[0]);
 -      spmax.setYPos(spMax[1]);
 -      spmax.setZPos(spMax[2]);
 -      viewer.setSeqPointMax(spmax);
 -      viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
 -      viewer.setLinkToAllViews(
 -              panel.getRotatableCanvas().isApplyToAllViews());
 -      SimilarityParamsI sp = pcaModel.getSimilarityParameters();
 -      viewer.setIncludeGaps(sp.includeGaps());
 -      viewer.setMatchGaps(sp.matchGaps());
 -      viewer.setIncludeGappedColumns(sp.includeGappedColumns());
 -      viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
 -
 -      /*
 -       * sequence points on display
 -       */
 -      for (jalview.datamodel.SequencePoint spt : pcaModel
 -              .getSequencePoints())
 -      {
 -        SequencePoint point = new SequencePoint();
 -        point.setSequenceRef(seqHash(spt.getSequence()));
 -        point.setXPos(spt.coord.x);
 -        point.setYPos(spt.coord.y);
 -        point.setZPos(spt.coord.z);
 -        viewer.getSequencePoint().add(point);
 -      }
 -
 -      /*
 -       * (end points of) axes on display
 -       */
 -      for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
 -      {
 -
 -        Axis axis = new Axis();
 -        axis.setXPos(p.x);
 -        axis.setYPos(p.y);
 -        axis.setZPos(p.z);
 -        viewer.getAxis().add(axis);
 -      }
 -
 -      /*
 -       * raw PCA data (note we are not restoring PCA inputs here -
 -       * alignment view, score model, similarity parameters)
 -       */
 -      PcaDataType data = new PcaDataType();
 -      viewer.setPcaData(data);
 -      PCA pca = pcaModel.getPcaData();
 -
 -      DoubleMatrix pm = new DoubleMatrix();
 -      saveDoubleMatrix(pca.getPairwiseScores(), pm);
 -      data.setPairwiseMatrix(pm);
 -
 -      DoubleMatrix tm = new DoubleMatrix();
 -      saveDoubleMatrix(pca.getTridiagonal(), tm);
 -      data.setTridiagonalMatrix(tm);
 -
 -      DoubleMatrix eigenMatrix = new DoubleMatrix();
 -      data.setEigenMatrix(eigenMatrix);
 -      saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
 -
 -      object.getPcaViewer().add(viewer);
 -    } catch (Throwable t)
 -    {
 -      Console.error("Error saving PCA: " + t.getMessage());
 -    }
 -  }
 -
 -  /**
 -   * Stores values from a matrix into an XML element, including (if present) the
 -   * D or E vectors
 -   * 
 -   * @param m
 -   * @param xmlMatrix
 -   * @see #loadDoubleMatrix(DoubleMatrix)
 -   */
 -  protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
 -  {
 -    xmlMatrix.setRows(m.height());
 -    xmlMatrix.setColumns(m.width());
 -    for (int i = 0; i < m.height(); i++)
 -    {
 -      DoubleVector row = new DoubleVector();
 -      for (int j = 0; j < m.width(); j++)
 -      {
 -        row.getV().add(m.getValue(i, j));
 -      }
 -      xmlMatrix.getRow().add(row);
 -    }
 -    if (m.getD() != null)
 -    {
 -      DoubleVector dVector = new DoubleVector();
 -      for (double d : m.getD())
 -      {
 -        dVector.getV().add(d);
 -      }
 -      xmlMatrix.setD(dVector);
 -    }
 -    if (m.getE() != null)
 -    {
 -      DoubleVector eVector = new DoubleVector();
 -      for (double e : m.getE())
 -      {
 -        eVector.getV().add(e);
 -      }
 -      xmlMatrix.setE(eVector);
 -    }
 -  }
 -
 -  /**
 -   * Loads XML matrix data into a new Matrix object, including the D and/or E
 -   * vectors (if present)
 -   * 
 -   * @param mData
 -   * @return
 -   * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
 -   */
 -  protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
 -  {
 -    int rows = mData.getRows();
 -    double[][] vals = new double[rows][];
 -
 -    for (int i = 0; i < rows; i++)
 -    {
 -      List<Double> dVector = mData.getRow().get(i).getV();
 -      vals[i] = new double[dVector.size()];
 -      int dvi = 0;
 -      for (Double d : dVector)
 -      {
 -        vals[i][dvi++] = d;
 -      }
 -    }
 -
 -    MatrixI m = new Matrix(vals);
 -
 -    if (mData.getD() != null)
 -    {
 -      List<Double> dVector = mData.getD().getV();
 -      double[] vec = new double[dVector.size()];
 -      int dvi = 0;
 -      for (Double d : dVector)
 -      {
 -        vec[dvi++] = d;
 -      }
 -      m.setD(vec);
 -    }
 -    if (mData.getE() != null)
 -    {
 -      List<Double> dVector = mData.getE().getV();
 -      double[] vec = new double[dVector.size()];
 -      int dvi = 0;
 -      for (Double d : dVector)
 -      {
 -        vec[dvi++] = d;
 -      }
 -      m.setE(vec);
 -    }
 -
 -    return m;
 -  }
 -
 -  /**
 -   * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
 -   * for each viewer, with
 -   * <ul>
 -   * <li>viewer geometry (position, size, split pane divider location)</li>
 -   * <li>index of the selected structure in the viewer (currently shows gapped
 -   * or ungapped)</li>
 -   * <li>the id of the annotation holding RNA secondary structure</li>
 -   * <li>(currently only one SS is shown per viewer, may be more in future)</li>
 -   * </ul>
 -   * Varna viewer state is also written out (in native Varna XML) to separate
 -   * project jar entries. A separate entry is written for each RNA structure
 -   * displayed, with the naming convention
 -   * <ul>
 -   * <li>rna_viewId_sequenceId_annotationId_[gapped|trimmed]</li>
 -   * </ul>
 -   * 
 -   * @param jout
 -   * @param jseq
 -   * @param jds
 -   * @param viewIds
 -   * @param ap
 -   * @param storeDataset
 -   */
 -  protected void saveRnaViewers(JarOutputStream jout, JSeq jseq,
 -          final SequenceI jds, List<String> viewIds, AlignmentPanel ap,
 -          boolean storeDataset)
 -  {
 -    if (Desktop.desktop == null)
 -    {
 -      return;
 -    }
 -    JInternalFrame[] frames = Desktop.desktop.getAllFrames();
 -    for (int f = frames.length - 1; f > -1; f--)
 -    {
 -      if (frames[f] instanceof AppVarna)
 -      {
 -        AppVarna varna = (AppVarna) frames[f];
 -        /*
 -         * link the sequence to every viewer that is showing it and is linked to
 -         * its alignment panel
 -         */
 -        if (varna.isListeningFor(jds) && ap == varna.getAlignmentPanel())
 -        {
 -          String viewId = varna.getViewId();
 -          RnaViewer rna = new RnaViewer();
 -          rna.setViewId(viewId);
 -          rna.setTitle(varna.getTitle());
 -          rna.setXpos(varna.getX());
 -          rna.setYpos(varna.getY());
 -          rna.setWidth(varna.getWidth());
 -          rna.setHeight(varna.getHeight());
 -          rna.setDividerLocation(varna.getDividerLocation());
 -          rna.setSelectedRna(varna.getSelectedIndex());
 -          // jseq.addRnaViewer(rna);
 -          jseq.getRnaViewer().add(rna);
 -
 -          /*
 -           * Store each Varna panel's state once in the project per sequence.
 -           * First time through only (storeDataset==false)
 -           */
 -          // boolean storeSessions = false;
 -          // String sequenceViewId = viewId + seqsToIds.get(jds);
 -          // if (!storeDataset && !viewIds.contains(sequenceViewId))
 -          // {
 -          // viewIds.add(sequenceViewId);
 -          // storeSessions = true;
 -          // }
 -          for (RnaModel model : varna.getModels())
 -          {
 -            if (model.seq == jds)
 -            {
 -              /*
 -               * VARNA saves each view (sequence or alignment secondary
 -               * structure, gapped or trimmed) as a separate XML file
 -               */
 -              String jarEntryName = rnaSessions.get(model);
 -              if (jarEntryName == null)
 -              {
 -
 -                String varnaStateFile = varna.getStateInfo(model.rna);
 -                jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
 -                copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
 -                rnaSessions.put(model, jarEntryName);
 -              }
 -              SecondaryStructure ss = new SecondaryStructure();
 -              String annotationId = varna.getAnnotation(jds).annotationId;
 -              ss.setAnnotationId(annotationId);
 -              ss.setViewerState(jarEntryName);
 -              ss.setGapped(model.gapped);
 -              ss.setTitle(model.title);
 -              // rna.addSecondaryStructure(ss);
 -              rna.getSecondaryStructure().add(ss);
 -            }
 -          }
 -        }
 -      }
 -    }
 -  }
 -
 -  /**
 -   * Copy the contents of a file to a new entry added to the output jar
 -   * 
 -   * @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 msg)
 -  {
 -    try (InputStream is = new FileInputStream(infilePath))
 -    {
 -      File file = new File(infilePath);
 -      if (file.exists() && jout != null)
 -      {
 -        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();
 -    }
 -  }
 -
 -  /**
 -   * Copies input to output, in 4K buffers; handles any data (text or binary)
 -   * 
 -   * @param in
 -   * @param out
 -   * @throws IOException
 -   */
 -  protected void copyAll(InputStream in, OutputStream out)
 -          throws IOException
 -  {
 -    byte[] buffer = new byte[4096];
 -    int bytesRead = 0;
 -    while ((bytesRead = in.read(buffer)) != -1)
 -    {
 -      out.write(buffer, 0, bytesRead);
 -    }
 -  }
 -
 -  /**
 -   * Save the state of a structure viewer
 -   * 
 -   * @param ap
 -   * @param jds
 -   * @param pdb
 -   *          the archive XML element under which to save the state
 -   * @param entry
 -   * @param viewIds
 -   * @param matchedFile
 -   * @param viewFrame
 -   * @return
 -   */
 -  protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
 -          Pdbids pdb, PDBEntry entry, List<String> viewIds,
 -          String matchedFile, StructureViewerBase viewFrame)
 -  {
 -    final AAStructureBindingModel bindingModel = viewFrame.getBinding();
 -
 -    /*
 -     * Look for any bindings for this viewer to the PDB file of interest
 -     * (including part matches excluding chain id)
 -     */
 -    for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
 -    {
 -      final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
 -      final String pdbId = pdbentry.getId();
 -      if (!pdbId.equals(entry.getId()) && !(entry.getId().length() > 4
 -              && entry.getId().toLowerCase(Locale.ROOT)
 -                      .startsWith(pdbId.toLowerCase(Locale.ROOT))))
 -      {
 -        /*
 -         * not interested in a binding to a different PDB entry here
 -         */
 -        continue;
 -      }
 -      if (matchedFile == null)
 -      {
 -        matchedFile = pdbentry.getFile();
 -      }
 -      else if (!matchedFile.equals(pdbentry.getFile()))
 -      {
 -        Console.warn(
 -                "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
 -                        + pdbentry.getFile());
 -      }
 -      // record the
 -      // file so we
 -      // can get at it if the ID
 -      // match is ambiguous (e.g.
 -      // 1QIP==1qipA)
 -
 -      for (int smap = 0; smap < viewFrame.getBinding()
 -              .getSequence()[peid].length; smap++)
 -      {
 -        // if (jal.findIndex(jmol.jmb.sequence[peid][smap]) > -1)
 -        if (jds == viewFrame.getBinding().getSequence()[peid][smap])
 -        {
 -          StructureState state = new StructureState();
 -          state.setVisible(true);
 -          state.setXpos(viewFrame.getX());
 -          state.setYpos(viewFrame.getY());
 -          state.setWidth(viewFrame.getWidth());
 -          state.setHeight(viewFrame.getHeight());
 -          final String viewId = viewFrame.getViewId();
 -          state.setViewId(viewId);
 -          state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
 -          state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
 -          state.setColourByJmol(viewFrame.isColouredByViewer());
 -          state.setType(viewFrame.getViewerType().toString());
 -          // pdb.addStructureState(state);
 -          pdb.getStructureState().add(state);
 -        }
 -      }
 -    }
 -    return matchedFile;
 -  }
 -
 -  /**
 -   * Populates the AnnotationColourScheme xml for save. This captures the
 -   * settings of the options in the 'Colour by Annotation' dialog.
 -   * 
 -   * @param acg
 -   * @param userColours
 -   * @param jm
 -   * @return
 -   */
 -  private AnnotationColourScheme constructAnnotationColours(
 -          AnnotationColourGradient acg, List<UserColourScheme> userColours,
 -          JalviewModel jm)
 -  {
 -    AnnotationColourScheme ac = new AnnotationColourScheme();
 -    ac.setAboveThreshold(acg.getAboveThreshold());
 -    ac.setThreshold(acg.getAnnotationThreshold());
 -    // 2.10.2 save annotationId (unique) not annotation label
 -    ac.setAnnotation(acg.getAnnotation().annotationId);
 -    if (acg.getBaseColour() instanceof UserColourScheme)
 -    {
 -      ac.setColourScheme(
 -              setUserColourScheme(acg.getBaseColour(), userColours, jm));
 -    }
 -    else
 -    {
 -      ac.setColourScheme(
 -              ColourSchemeProperty.getColourName(acg.getBaseColour()));
 -    }
 -
 -    ac.setMaxColour(acg.getMaxColour().getRGB());
 -    ac.setMinColour(acg.getMinColour().getRGB());
 -    ac.setPerSequence(acg.isSeqAssociated());
 -    ac.setPredefinedColours(acg.isPredefinedColours());
 -    return ac;
 -  }
 -
 -  private void storeAlignmentAnnotation(AlignmentAnnotation[] aa,
 -          IdentityHashMap<SequenceGroup, String> groupRefs,
 -          AlignmentViewport av, Set<String> calcIdSet, boolean storeDS,
 -          SequenceSet vamsasSet)
 -  {
 -
 -    for (int i = 0; i < aa.length; i++)
 -    {
 -      Annotation an = new Annotation();
 -
 -      AlignmentAnnotation annotation = aa[i];
 -      if (annotation.annotationId != null)
 -      {
 -        annotationIds.put(annotation.annotationId, annotation);
 -      }
 -
 -      an.setId(annotation.annotationId);
 -
 -      an.setVisible(annotation.visible);
 -
 -      an.setDescription(annotation.description);
 -
 -      if (annotation.sequenceRef != null)
 -      {
 -        // 2.9 JAL-1781 xref on sequence id rather than name
 -        an.setSequenceRef(seqsToIds.get(annotation.sequenceRef));
 -      }
 -      if (annotation.groupRef != null)
 -      {
 -        String groupIdr = groupRefs.get(annotation.groupRef);
 -        if (groupIdr == null)
 -        {
 -          // make a locally unique String
 -          groupRefs.put(annotation.groupRef,
 -                  groupIdr = ("" + System.currentTimeMillis()
 -                          + annotation.groupRef.getName()
 -                          + groupRefs.size()));
 -        }
 -        an.setGroupRef(groupIdr.toString());
 -      }
 -
 -      // store all visualization attributes for annotation
 -      an.setGraphHeight(annotation.graphHeight);
 -      an.setCentreColLabels(annotation.centreColLabels);
 -      an.setScaleColLabels(annotation.scaleColLabel);
 -      an.setShowAllColLabels(annotation.showAllColLabels);
 -      an.setBelowAlignment(annotation.belowAlignment);
 -
 -      if (annotation.graph > 0)
 -      {
 -        an.setGraph(true);
 -        an.setGraphType(annotation.graph);
 -        an.setGraphGroup(annotation.graphGroup);
 -        if (annotation.getThreshold() != null)
 -        {
 -          ThresholdLine line = new ThresholdLine();
 -          line.setLabel(annotation.getThreshold().label);
 -          line.setValue(annotation.getThreshold().value);
 -          line.setColour(annotation.getThreshold().colour.getRGB());
 -          an.setThresholdLine(line);
 -        }
 -      }
 -      else
 -      {
 -        an.setGraph(false);
 -      }
 -
 -      an.setLabel(annotation.label);
 -
 -      if (annotation == av.getAlignmentQualityAnnot()
 -              || annotation == av.getAlignmentConservationAnnotation()
 -              || annotation == av.getAlignmentConsensusAnnotation()
 -              || annotation.autoCalculated)
 -      {
 -        // new way of indicating autocalculated annotation -
 -        an.setAutoCalculated(annotation.autoCalculated);
 -      }
 -      if (annotation.hasScore())
 -      {
 -        an.setScore(annotation.getScore());
 -      }
 -
 -      if (annotation.getCalcId() != null)
 -      {
 -        calcIdSet.add(annotation.getCalcId());
 -        an.setCalcId(annotation.getCalcId());
 -      }
 -      if (annotation.hasProperties())
 -      {
 -        for (String pr : annotation.getProperties())
 -        {
 -          jalview.xml.binding.jalview.Annotation.Property prop = new jalview.xml.binding.jalview.Annotation.Property();
 -          prop.setName(pr);
 -          prop.setValue(annotation.getProperty(pr));
 -          // an.addProperty(prop);
 -          an.getProperty().add(prop);
 -        }
 -      }
 -
 -      AnnotationElement ae;
 -      if (annotation.annotations != null)
 -      {
 -        an.setScoreOnly(false);
 -        for (int a = 0; a < annotation.annotations.length; a++)
 -        {
 -          if ((annotation == null) || (annotation.annotations[a] == null))
 -          {
 -            continue;
 -          }
 -
 -          ae = new AnnotationElement();
 -          if (annotation.annotations[a].description != null)
 -          {
 -            ae.setDescription(annotation.annotations[a].description);
 -          }
 -          if (annotation.annotations[a].displayCharacter != null)
 -          {
 -            ae.setDisplayCharacter(
 -                    annotation.annotations[a].displayCharacter);
 -          }
 -
 -          if (!Float.isNaN(annotation.annotations[a].value))
 -          {
 -            ae.setValue(annotation.annotations[a].value);
 -          }
 -
 -          ae.setPosition(a);
 -          if (annotation.annotations[a].secondaryStructure > ' ')
 -          {
 -            ae.setSecondaryStructure(
 -                    annotation.annotations[a].secondaryStructure + "");
 -          }
 -
 -          if (annotation.annotations[a].colour != null
 -                  && annotation.annotations[a].colour != java.awt.Color.black)
 -          {
 -            ae.setColour(annotation.annotations[a].colour.getRGB());
 -          }
 -
 -          // an.addAnnotationElement(ae);
 -          an.getAnnotationElement().add(ae);
 -          if (annotation.autoCalculated)
 -          {
 -            // only write one non-null entry into the annotation row -
 -            // sufficient to get the visualization attributes necessary to
 -            // display data
 -            continue;
 -          }
 -        }
 -      }
 -      else
 -      {
 -        an.setScoreOnly(true);
 -      }
 -      if (!storeDS || (storeDS && !annotation.autoCalculated))
 -      {
 -        // skip autocalculated annotation - these are only provided for
 -        // alignments
 -        // vamsasSet.addAnnotation(an);
 -        vamsasSet.getAnnotation().add(an);
 -      }
 -    }
 -
 -  }
 -
 -  private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
 -  {
 -    AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
 -    if (settings != null)
 -    {
 -      CalcIdParam vCalcIdParam = new CalcIdParam();
 -      vCalcIdParam.setCalcId(calcId);
 -      // vCalcIdParam.addServiceURL(settings.getServiceURI());
 -      vCalcIdParam.getServiceURL().add(settings.getServiceURI());
 -      // generic URI allowing a third party to resolve another instance of the
 -      // service used for this calculation
 -      for (String url : settings.getServiceURLs())
 -      {
 -        // vCalcIdParam.addServiceURL(urls);
 -        vCalcIdParam.getServiceURL().add(url);
 -      }
 -      vCalcIdParam.setVersion("1.0");
 -      if (settings.getPreset() != null)
 -      {
 -        WsParamSetI setting = settings.getPreset();
 -        vCalcIdParam.setName(setting.getName());
 -        vCalcIdParam.setDescription(setting.getDescription());
 -      }
 -      else
 -      {
 -        vCalcIdParam.setName("");
 -        vCalcIdParam.setDescription("Last used parameters");
 -      }
 -      // need to be able to recover 1) settings 2) user-defined presets or
 -      // recreate settings from preset 3) predefined settings provided by
 -      // service - or settings that can be transferred (or discarded)
 -      vCalcIdParam.setParameters(
 -              settings.getWsParamFile().replace("\n", "|\\n|"));
 -      vCalcIdParam.setAutoUpdate(settings.isAutoUpdate());
 -      // todo - decide if updateImmediately is needed for any projects.
 -
 -      return vCalcIdParam;
 -    }
 -    return null;
 -  }
 -
 -  private boolean recoverCalcIdParam(CalcIdParam calcIdParam,
 -          AlignViewport av)
 -  {
 -    if (calcIdParam.getVersion().equals("1.0"))
 -    {
 -      final String[] calcIds = calcIdParam.getServiceURL()
 -              .toArray(new String[0]);
 -      Jws2Instance service = Jws2Discoverer.getDiscoverer()
 -              .getPreferredServiceFor(calcIds);
 -      if (service != null)
 -      {
 -        WsParamSetI parmSet = null;
 -        try
 -        {
 -          parmSet = service.getParamStore().parseServiceParameterFile(
 -                  calcIdParam.getName(), calcIdParam.getDescription(),
 -                  calcIds,
 -                  calcIdParam.getParameters().replace("|\\n|", "\n"));
 -        } catch (IOException x)
 -        {
 -          Console.warn("Couldn't parse parameter data for "
 -                  + calcIdParam.getCalcId(), x);
 -          return false;
 -        }
 -        List<ArgumentI> argList = null;
 -        if (calcIdParam.getName().length() > 0)
 -        {
 -          parmSet = service.getParamStore()
 -                  .getPreset(calcIdParam.getName());
 -          if (parmSet != null)
 -          {
 -            // TODO : check we have a good match with settings in AACon -
 -            // otherwise we'll need to create a new preset
 -          }
 -        }
 -        else
 -        {
 -          argList = parmSet.getArguments();
 -          parmSet = null;
 -        }
 -        AAConSettings settings = new AAConSettings(
 -                calcIdParam.isAutoUpdate(), service, parmSet, argList);
 -        av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
 -                calcIdParam.isNeedsUpdate());
 -        return true;
 -      }
 -      else
 -      {
 -        Console.warn(
 -                "Cannot resolve a service for the parameters used in this project. Try configuring a JABAWS server.");
 -        return false;
 -      }
 -    }
 -    throw new Error(MessageManager.formatMessage(
 -            "error.unsupported_version_calcIdparam", new Object[]
 -            { calcIdParam.toString() }));
 -  }
 -
 -  /**
 -   * External mapping between jalview objects and objects yielding a valid and
 -   * unique object ID string. This is null for normal Jalview project IO, but
 -   * non-null when a jalview project is being read or written as part of a
 -   * vamsas session.
 -   */
 -  IdentityHashMap jv2vobj = null;
 -
 -  /**
 -   * Construct a unique ID for jvobj using either existing bindings or if none
 -   * exist, the result of the hashcode call for the object.
 -   * 
 -   * @param jvobj
 -   *          jalview data object
 -   * @return unique ID for referring to jvobj
 -   */
 -  private String makeHashCode(Object jvobj, String altCode)
 -  {
 -    if (jv2vobj != null)
 -    {
 -      Object id = jv2vobj.get(jvobj);
 -      if (id != null)
 -      {
 -        return id.toString();
 -      }
 -      // check string ID mappings
 -      if (jvids2vobj != null && jvobj instanceof String)
 -      {
 -        id = jvids2vobj.get(jvobj);
 -      }
 -      if (id != null)
 -      {
 -        return id.toString();
 -      }
 -      // give up and warn that something has gone wrong
 -      Console.warn(
 -              "Cannot find ID for object in external mapping : " + jvobj);
 -    }
 -    return altCode;
 -  }
 -
 -  /**
 -   * return local jalview object mapped to ID, if it exists
 -   * 
 -   * @param idcode
 -   *          (may be null)
 -   * @return null or object bound to idcode
 -   */
 -  private Object retrieveExistingObj(String idcode)
 -  {
 -    if (idcode != null && vobj2jv != null)
 -    {
 -      return vobj2jv.get(idcode);
 -    }
 -    return null;
 -  }
 -
 -  /**
 -   * binding from ID strings from external mapping table to jalview data model
 -   * objects.
 -   */
 -  private Hashtable vobj2jv;
 -
 -  private Sequence createVamsasSequence(String id, SequenceI jds)
 -  {
 -    return createVamsasSequence(true, id, jds, null);
 -  }
 -
 -  private Sequence createVamsasSequence(boolean recurse, String id,
 -          SequenceI jds, SequenceI parentseq)
 -  {
 -    Sequence vamsasSeq = new Sequence();
 -    vamsasSeq.setId(id);
 -    vamsasSeq.setName(jds.getName());
 -    vamsasSeq.setSequence(jds.getSequenceAsString());
 -    vamsasSeq.setDescription(jds.getDescription());
 -    List<DBRefEntry> dbrefs = null;
 -    if (jds.getDatasetSequence() != null)
 -    {
 -      vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
 -    }
 -    else
 -    {
 -      // seqId==dsseqid so we can tell which sequences really are
 -      // dataset sequences only
 -      vamsasSeq.setDsseqid(id);
 -      dbrefs = jds.getDBRefs();
 -      if (parentseq == null)
 -      {
 -        parentseq = jds;
 -      }
 -    }
 -
 -    /*
 -     * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
 -     */
 -    if (dbrefs != null)
 -    {
 -      for (int d = 0, nd = dbrefs.size(); d < nd; d++)
 -      {
 -        DBRef dbref = new DBRef();
 -        DBRefEntry ref = dbrefs.get(d);
 -        dbref.setSource(ref.getSource());
 -        dbref.setVersion(ref.getVersion());
 -        dbref.setAccessionId(ref.getAccessionId());
 -        dbref.setCanonical(ref.isCanonical());
 -        if (ref instanceof GeneLocus)
 -        {
 -          dbref.setLocus(true);
 -        }
 -        if (ref.hasMap())
 -        {
 -          Mapping mp = createVamsasMapping(ref.getMap(), parentseq, jds,
 -                  recurse);
 -          dbref.setMapping(mp);
 -        }
 -        vamsasSeq.getDBRef().add(dbref);
 -      }
 -    }
 -    return vamsasSeq;
 -  }
 -
 -  private Mapping createVamsasMapping(jalview.datamodel.Mapping jmp,
 -          SequenceI parentseq, SequenceI jds, boolean recurse)
 -  {
 -    Mapping mp = null;
 -    if (jmp.getMap() != null)
 -    {
 -      mp = new Mapping();
 -
 -      jalview.util.MapList mlst = jmp.getMap();
 -      List<int[]> r = mlst.getFromRanges();
 -      for (int[] range : r)
 -      {
 -        MapListFrom mfrom = new MapListFrom();
 -        mfrom.setStart(range[0]);
 -        mfrom.setEnd(range[1]);
 -        // mp.addMapListFrom(mfrom);
 -        mp.getMapListFrom().add(mfrom);
 -      }
 -      r = mlst.getToRanges();
 -      for (int[] range : r)
 -      {
 -        MapListTo mto = new MapListTo();
 -        mto.setStart(range[0]);
 -        mto.setEnd(range[1]);
 -        // mp.addMapListTo(mto);
 -        mp.getMapListTo().add(mto);
 -      }
 -      mp.setMapFromUnit(BigInteger.valueOf(mlst.getFromRatio()));
 -      mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
 -      if (jmp.getTo() != null)
 -      {
 -        // MappingChoice mpc = new MappingChoice();
 -
 -        // check/create ID for the sequence referenced by getTo()
 -
 -        String jmpid = "";
 -        SequenceI ps = null;
 -        if (parentseq != jmp.getTo()
 -                && parentseq.getDatasetSequence() != jmp.getTo())
 -        {
 -          // chaining dbref rather than a handshaking one
 -          jmpid = seqHash(ps = jmp.getTo());
 -        }
 -        else
 -        {
 -          jmpid = seqHash(ps = parentseq);
 -        }
 -        // mpc.setDseqFor(jmpid);
 -        mp.setDseqFor(jmpid);
 -        if (!seqRefIds.containsKey(jmpid))
 -        {
 -          Console.debug("creatign new DseqFor ID");
 -          seqRefIds.put(jmpid, ps);
 -        }
 -        else
 -        {
 -          Console.debug("reusing DseqFor ID");
 -        }
 -
 -        // mp.setMappingChoice(mpc);
 -      }
 -    }
 -    return mp;
 -  }
 -
 -  String setUserColourScheme(jalview.schemes.ColourSchemeI cs,
 -          List<UserColourScheme> userColours, JalviewModel jm)
 -  {
 -    String id = null;
 -    jalview.schemes.UserColourScheme ucs = (jalview.schemes.UserColourScheme) cs;
 -    boolean newucs = false;
 -    if (!userColours.contains(ucs))
 -    {
 -      userColours.add(ucs);
 -      newucs = true;
 -    }
 -    id = "ucs" + userColours.indexOf(ucs);
 -    if (newucs)
 -    {
 -      // actually create the scheme's entry in the XML model
 -      java.awt.Color[] colours = ucs.getColours();
 -      UserColours uc = new UserColours();
 -      // UserColourScheme jbucs = new UserColourScheme();
 -      JalviewUserColours jbucs = new JalviewUserColours();
 -
 -      for (int i = 0; i < colours.length; i++)
 -      {
 -        Colour col = new Colour();
 -        col.setName(ResidueProperties.aa[i]);
 -        col.setRGB(jalview.util.Format.getHexString(colours[i]));
 -        // jbucs.addColour(col);
 -        jbucs.getColour().add(col);
 -      }
 -      if (ucs.getLowerCaseColours() != null)
 -      {
 -        colours = ucs.getLowerCaseColours();
 -        for (int i = 0; i < colours.length; i++)
 -        {
 -          Colour col = new Colour();
 -          col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
 -          col.setRGB(jalview.util.Format.getHexString(colours[i]));
 -          // jbucs.addColour(col);
 -          jbucs.getColour().add(col);
 -        }
 -      }
 -
 -      uc.setId(id);
 -      uc.setUserColourScheme(jbucs);
 -      // jm.addUserColours(uc);
 -      jm.getUserColours().add(uc);
 -    }
 -
 -    return id;
 -  }
 -
 -  jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
 -          String id)
 -  {
 -    List<UserColours> uc = jm.getUserColours();
 -    UserColours colours = null;
 -    /*
 -    for (int i = 0; i < uc.length; i++)
 -    {
 -      if (uc[i].getId().equals(id))
 -      {
 -        colours = uc[i];
 -        break;
 -      }
 -    }
 -    */
 -    for (UserColours c : uc)
 -    {
 -      if (c.getId().equals(id))
 -      {
 -        colours = c;
 -        break;
 -      }
 -    }
 -
 -    java.awt.Color[] newColours = new java.awt.Color[24];
 -
 -    for (int i = 0; i < 24; i++)
 -    {
 -      newColours[i] = new java.awt.Color(Integer.parseInt(
 -              // colours.getUserColourScheme().getColour(i).getRGB(), 16));
 -              colours.getUserColourScheme().getColour().get(i).getRGB(),
 -              16));
 -    }
 -
 -    jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
 -            newColours);
 -
 -    if (colours.getUserColourScheme().getColour().size()/*Count()*/ > 24)
 -    {
 -      newColours = new java.awt.Color[23];
 -      for (int i = 0; i < 23; i++)
 -      {
 -        newColours[i] = new java.awt.Color(
 -                Integer.parseInt(colours.getUserColourScheme().getColour()
 -                        .get(i + 24).getRGB(), 16));
 -      }
 -      ucs.setLowerCaseColours(newColours);
 -    }
 -
 -    return ucs;
 -  }
 -
 -  /**
 -   * contains last error message (if any) encountered by XML loader.
 -   */
 -  String errorMessage = null;
 -
 -  /**
 -   * flag to control whether the Jalview2XML_V1 parser should be deferred to if
 -   * exceptions are raised during project XML parsing
 -   */
 -  public boolean attemptversion1parse = false;
 -
 -  /**
 -   * Load a jalview project archive from a jar file
 -   * 
 -   * @param file
 -   *          - HTTP URL or filename
 -   */
 -  public AlignFrame loadJalviewAlign(final Object file)
 -  {
 -
 -    jalview.gui.AlignFrame af = null;
 -
 -    try
 -    {
 -      // create list to store references for any new Jmol viewers created
 -      newStructureViewers = new Vector<>();
 -      // UNMARSHALLER SEEMS TO CLOSE JARINPUTSTREAM, MOST ANNOYING
 -      // Workaround is to make sure caller implements the JarInputStreamProvider
 -      // interface
 -      // so we can re-open the jar input stream for each entry.
 -
 -      jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
 -      af = loadJalviewAlign(jprovider);
 -      if (af != null)
 -      {
 -        af.setMenusForViewport();
 -      }
 -    } catch (MalformedURLException e)
 -    {
 -      errorMessage = "Invalid URL format for '" + file + "'";
 -      reportErrors();
 -    } finally
 -    {
 -      try
 -      {
 -        SwingUtilities.invokeAndWait(new Runnable()
 -        {
 -          @Override
 -          public void run()
 -          {
 -            setLoadingFinishedForNewStructureViewers();
 -          }
 -        });
 -      } catch (Exception x)
 -      {
 -        System.err.println("Error loading alignment: " + x.getMessage());
 -      }
 -    }
 -    return af;
 -  }
 -
 -  @SuppressWarnings("unused")
 -  private jarInputStreamProvider createjarInputStreamProvider(
 -          final Object ofile) throws MalformedURLException
 -  {
 -
 -    // BH 2018 allow for bytes already attached to File object
 -    try
 -    {
 -      String file = (ofile instanceof File
 -              ? ((File) ofile).getCanonicalPath()
 -              : ofile.toString());
 -      byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
 -              : null;
 -      URL url = null;
 -      errorMessage = null;
 -      uniqueSetSuffix = null;
 -      seqRefIds = null;
 -      viewportsAdded.clear();
 -      frefedSequence = null;
 -
 -      if (HttpUtils.startsWithHttpOrHttps(file))
 -      {
 -        url = new URL(file);
 -      }
 -      final URL _url = url;
 -      return new jarInputStreamProvider()
 -      {
 -
 -        @Override
 -        public JarInputStream getJarInputStream() throws IOException
 -        {
 -          if (bytes != null)
 -          {
 -            // System.out.println("Jalview2XML: opening byte jarInputStream for
 -            // bytes.length=" + bytes.length);
 -            return new JarInputStream(new ByteArrayInputStream(bytes));
 -          }
 -          if (_url != null)
 -          {
 -            // System.out.println("Jalview2XML: opening url jarInputStream for "
 -            // + _url);
 -            return new JarInputStream(_url.openStream());
 -          }
 -          else
 -          {
 -            // System.out.println("Jalview2XML: opening file jarInputStream for
 -            // " + file);
 -            return new JarInputStream(new FileInputStream(file));
 -          }
 -        }
 -
 -        @Override
 -        public String getFilename()
 -        {
 -          return file;
 -        }
 -      };
 -    } catch (IOException e)
 -    {
 -      e.printStackTrace();
 -      return null;
 -    }
 -  }
 -
 -  /**
 -   * Recover jalview session from a jalview project archive. Caller may
 -   * initialise uniqueSetSuffix, seqRefIds, viewportsAdded and frefedSequence
 -   * themselves. Any null fields will be initialised with default values,
 -   * non-null fields are left alone.
 -   * 
 -   * @param jprovider
 -   * @return
 -   */
 -  public AlignFrame loadJalviewAlign(final jarInputStreamProvider jprovider)
 -  {
 -    errorMessage = null;
 -    if (uniqueSetSuffix == null)
 -    {
 -      uniqueSetSuffix = System.currentTimeMillis() % 100000 + "";
 -    }
 -    if (seqRefIds == null)
 -    {
 -      initSeqRefs();
 -    }
 -    AlignFrame af = null, _af = null;
 -    IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
 -    Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
 -    final String file = jprovider.getFilename();
 -    try
 -    {
 -      JarInputStream jin = null;
 -      JarEntry jarentry = null;
 -      int entryCount = 1;
 -
 -      do
 -      {
 -        jin = jprovider.getJarInputStream();
 -        for (int i = 0; i < entryCount; i++)
 -        {
 -          jarentry = jin.getNextJarEntry();
 -        }
 -
 -        if (jarentry != null && jarentry.getName().endsWith(".xml"))
 -        {
 -          JAXBContext jc = JAXBContext
 -                  .newInstance("jalview.xml.binding.jalview");
 -          XMLStreamReader streamReader = XMLInputFactory.newInstance()
 -                  .createXMLStreamReader(jin);
 -          javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
 -          JAXBElement<JalviewModel> jbe = um.unmarshal(streamReader,
 -                  JalviewModel.class);
 -          JalviewModel object = jbe.getValue();
 -
 -          if (true) // !skipViewport(object))
 -          {
 -            _af = loadFromObject(object, file, true, jprovider);
 -            if (_af != null && object.getViewport().size() > 0)
 -            // getJalviewModelSequence().getViewportCount() > 0)
 -            {
 -              if (af == null)
 -              {
 -                // store a reference to the first view
 -                af = _af;
 -              }
 -              if (_af.getViewport().isGatherViewsHere())
 -              {
 -                // if this is a gathered view, keep its reference since
 -                // after gathering views, only this frame will remain
 -                af = _af;
 -                gatherToThisFrame.put(_af.getViewport().getSequenceSetId(),
 -                        _af);
 -              }
 -              // Save dataset to register mappings once all resolved
 -              importedDatasets.put(
 -                      af.getViewport().getAlignment().getDataset(),
 -                      af.getViewport().getAlignment().getDataset());
 -            }
 -          }
 -          entryCount++;
 -        }
 -        else if (jarentry != null)
 -        {
 -          // Some other file here.
 -          entryCount++;
 -        }
 -      } while (jarentry != null);
 -      jin.close();
 -      resolveFrefedSequences();
 -    } catch (IOException ex)
 -    {
 -      ex.printStackTrace();
 -      errorMessage = "Couldn't locate Jalview XML file : " + file;
 -      System.err.println(
 -              "Exception whilst loading jalview XML file : " + ex + "\n");
 -    } catch (Exception ex)
 -    {
 -      System.err.println("Parsing as Jalview Version 2 file failed.");
 -      ex.printStackTrace(System.err);
 -      if (attemptversion1parse)
 -      {
 -        // used to attempt to parse as V1 castor-generated xml
 -      }
 -      if (Desktop.instance != null)
 -      {
 -        Desktop.instance.stopLoading();
 -      }
 -      if (af != null)
 -      {
 -        System.out.println("Successfully loaded archive file");
 -        return af;
 -      }
 -      ex.printStackTrace();
 -
 -      System.err.println(
 -              "Exception whilst loading jalview XML file : " + ex + "\n");
 -    } catch (OutOfMemoryError e)
 -    {
 -      // Don't use the OOM Window here
 -      errorMessage = "Out of memory loading jalview XML file";
 -      System.err.println("Out of memory whilst loading jalview XML file");
 -      e.printStackTrace();
 -    }
 -
 -    /*
 -     * Regather multiple views (with the same sequence set id) to the frame (if
 -     * any) that is flagged as the one to gather to, i.e. convert them to tabbed
 -     * views instead of separate frames. Note this doesn't restore a state where
 -     * some expanded views in turn have tabbed views - the last "first tab" read
 -     * in will play the role of gatherer for all.
 -     */
 -    for (AlignFrame fr : gatherToThisFrame.values())
 -    {
 -      Desktop.instance.gatherViews(fr);
 -    }
 -
 -    restoreSplitFrames();
 -    for (AlignmentI ds : importedDatasets.keySet())
 -    {
 -      if (ds.getCodonFrames() != null)
 -      {
 -        StructureSelectionManager
 -                .getStructureSelectionManager(Desktop.instance)
 -                .registerMappings(ds.getCodonFrames());
 -      }
 -    }
 -    if (errorMessage != null)
 -    {
 -      reportErrors();
 -    }
 -
 -    if (Desktop.instance != null)
 -    {
 -      Desktop.instance.stopLoading();
 -    }
 -
 -    return af;
 -  }
 -
 -  /**
 -   * Try to reconstruct and display SplitFrame windows, where each contains
 -   * complementary dna and protein alignments. Done by pairing up AlignFrame
 -   * objects (created earlier) which have complementary viewport ids associated.
 -   */
 -  protected void restoreSplitFrames()
 -  {
 -    List<SplitFrame> gatherTo = new ArrayList<>();
 -    List<AlignFrame> addedToSplitFrames = new ArrayList<>();
 -    Map<String, AlignFrame> dna = new HashMap<>();
 -
 -    /*
 -     * Identify the DNA alignments
 -     */
 -    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 -            .entrySet())
 -    {
 -      AlignFrame af = candidate.getValue();
 -      if (af.getViewport().getAlignment().isNucleotide())
 -      {
 -        dna.put(candidate.getKey().getId(), af);
 -      }
 -    }
 -
 -    /*
 -     * Try to match up the protein complements
 -     */
 -    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 -            .entrySet())
 -    {
 -      AlignFrame af = candidate.getValue();
 -      if (!af.getViewport().getAlignment().isNucleotide())
 -      {
 -        String complementId = candidate.getKey().getComplementId();
 -        // only non-null complements should be in the Map
 -        if (complementId != null && dna.containsKey(complementId))
 -        {
 -          final AlignFrame dnaFrame = dna.get(complementId);
 -          SplitFrame sf = createSplitFrame(dnaFrame, af);
 -          addedToSplitFrames.add(dnaFrame);
 -          addedToSplitFrames.add(af);
 -          dnaFrame.setMenusForViewport();
 -          af.setMenusForViewport();
 -          if (af.getViewport().isGatherViewsHere())
 -          {
 -            gatherTo.add(sf);
 -          }
 -        }
 -      }
 -    }
 -
 -    /*
 -     * Open any that we failed to pair up (which shouldn't happen!) as
 -     * standalone AlignFrame's.
 -     */
 -    for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
 -            .entrySet())
 -    {
 -      AlignFrame af = candidate.getValue();
 -      if (!addedToSplitFrames.contains(af))
 -      {
 -        Viewport view = candidate.getKey();
 -        Desktop.addInternalFrame(af, view.getTitle(),
 -                safeInt(view.getWidth()), safeInt(view.getHeight()));
 -        af.setMenusForViewport();
 -        System.err.println("Failed to restore view " + view.getTitle()
 -                + " to split frame");
 -      }
 -    }
 -
 -    /*
 -     * Gather back into tabbed views as flagged.
 -     */
 -    for (SplitFrame sf : gatherTo)
 -    {
 -      Desktop.instance.gatherViews(sf);
 -    }
 -
 -    splitFrameCandidates.clear();
 -  }
 -
 -  /**
 -   * Construct and display one SplitFrame holding DNA and protein alignments.
 -   * 
 -   * @param dnaFrame
 -   * @param proteinFrame
 -   * @return
 -   */
 -  protected SplitFrame createSplitFrame(AlignFrame dnaFrame,
 -          AlignFrame proteinFrame)
 -  {
 -    SplitFrame splitFrame = new SplitFrame(dnaFrame, proteinFrame);
 -    String title = MessageManager.getString("label.linked_view_title");
 -    int width = (int) dnaFrame.getBounds().getWidth();
 -    int height = (int) (dnaFrame.getBounds().getHeight()
 -            + proteinFrame.getBounds().getHeight() + 50);
 -
 -    /*
 -     * SplitFrame location is saved to both enclosed frames
 -     */
 -    splitFrame.setLocation(dnaFrame.getX(), dnaFrame.getY());
 -    Desktop.addInternalFrame(splitFrame, title, width, height);
 -
 -    /*
 -     * And compute cDNA consensus (couldn't do earlier with consensus as
 -     * mappings were not yet present)
 -     */
 -    proteinFrame.getViewport().alignmentChanged(proteinFrame.alignPanel);
 -
 -    return splitFrame;
 -  }
 -
 -  /**
 -   * check errorMessage for a valid error message and raise an error box in the
 -   * GUI or write the current errorMessage to stderr and then clear the error
 -   * state.
 -   */
 -  protected void reportErrors()
 -  {
 -    reportErrors(false);
 -  }
 -
 -  protected void reportErrors(final boolean saving)
 -  {
 -    if (errorMessage != null)
 -    {
 -      final String finalErrorMessage = errorMessage;
 -      if (raiseGUI)
 -      {
 -        javax.swing.SwingUtilities.invokeLater(new Runnable()
 -        {
 -          @Override
 -          public void run()
 -          {
 -            JvOptionPane.showInternalMessageDialog(Desktop.desktop,
 -                    finalErrorMessage,
 -                    "Error " + (saving ? "saving" : "loading")
 -                            + " Jalview file",
 -                    JvOptionPane.WARNING_MESSAGE);
 -          }
 -        });
 -      }
 -      else
 -      {
 -        System.err.println("Problem loading Jalview file: " + errorMessage);
 -      }
 -    }
 -    errorMessage = null;
 -  }
 -
 -  Map<String, String> alreadyLoadedPDB = new HashMap<>();
 -
 -  /**
 -   * when set, local views will be updated from view stored in JalviewXML
 -   * Currently (28th Sep 2008) things will go horribly wrong in vamsas document
 -   * sync if this is set to true.
 -   */
 -  private final boolean updateLocalViews = false;
 -
 -  /**
 -   * Returns the path to a temporary file holding the PDB file for the given PDB
 -   * id. The first time of asking, searches for a file of that name in the
 -   * Jalview project jar, and copies it to a new temporary file. Any repeat
 -   * requests just return the path to the file previously created.
 -   * 
 -   * @param jprovider
 -   * @param pdbId
 -   * @return
 -   */
 -  String loadPDBFile(jarInputStreamProvider jprovider, String pdbId,
 -          String origFile)
 -  {
 -    if (alreadyLoadedPDB.containsKey(pdbId))
 -    {
 -      return alreadyLoadedPDB.get(pdbId).toString();
 -    }
 -
 -    String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb",
 -            origFile);
 -    if (tempFile != null)
 -    {
 -      alreadyLoadedPDB.put(pdbId, tempFile);
 -    }
 -    return tempFile;
 -  }
 -
 -  /**
 -   * Copies the jar entry of given name to a new temporary file and returns the
 -   * path to the file, or null if the entry is not found.
 -   * 
 -   * @param jprovider
 -   * @param jarEntryName
 -   * @param prefix
 -   *          a prefix for the temporary file name, must be at least three
 -   *          characters long
 -   * @param suffixModel
 -   *          null or original file - so new file can be given the same suffix
 -   *          as the old one
 -   * @return
 -   */
 -  protected String copyJarEntry(jarInputStreamProvider jprovider,
 -          String jarEntryName, String prefix, String suffixModel)
 -  {
 -    String suffix = ".tmp";
 -    if (suffixModel == null)
 -    {
 -      suffixModel = jarEntryName;
 -    }
 -    int sfpos = suffixModel.lastIndexOf(".");
 -    if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
 -    {
 -      suffix = "." + suffixModel.substring(sfpos + 1);
 -    }
 -
 -    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));
 -        File outFile = File.createTempFile(prefix, suffix);
 -        outFile.deleteOnExit();
 -        try (OutputStream os = new FileOutputStream(outFile))
 -        {
 -          copyAll(jin, os);
 -        }
 -        String t = outFile.getAbsolutePath();
 -        return t;
 -      }
 -      else
 -      {
 -        Console.warn(
 -                "Couldn't find entry in Jalview Jar for " + jarEntryName);
 -      }
 -    } catch (Exception ex)
 -    {
 -      ex.printStackTrace();
 -    }
 -
 -    return null;
 -  }
 -
 -  private class JvAnnotRow
 -  {
 -    public JvAnnotRow(int i, AlignmentAnnotation jaa)
 -    {
 -      order = i;
 -      template = jaa;
 -    }
 -
 -    /**
 -     * persisted version of annotation row from which to take vis properties
 -     */
 -    public jalview.datamodel.AlignmentAnnotation template;
 -
 -    /**
 -     * original position of the annotation row in the alignment
 -     */
 -    public int order;
 -  }
 -
 -  /**
 -   * Load alignment frame from jalview XML DOM object
 -   * 
 -   * @param jalviewModel
 -   *          DOM
 -   * @param file
 -   *          filename source string
 -   * @param loadTreesAndStructures
 -   *          when false only create Viewport
 -   * @param jprovider
 -   *          data source provider
 -   * @return alignment frame created from view stored in DOM
 -   */
 -  AlignFrame loadFromObject(JalviewModel jalviewModel, String file,
 -          boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
 -  {
 -    SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet()
 -            .get(0);
 -    List<Sequence> vamsasSeqs = vamsasSet.getSequence();
 -
 -    // JalviewModelSequence jms = object.getJalviewModelSequence();
 -
 -    // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
 -    // : null;
 -    Viewport view = (jalviewModel.getViewport().size() > 0)
 -            ? jalviewModel.getViewport().get(0)
 -            : null;
 -
 -    // ////////////////////////////////
 -    // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
 -    //
 -    //
 -    // If we just load in the same jar file again, the sequenceSetId
 -    // will be the same, and we end up with multiple references
 -    // to the same sequenceSet. We must modify this id on load
 -    // so that each load of the file gives a unique id
 -
 -    /**
 -     * used to resolve correct alignment dataset for alignments with multiple
 -     * views
 -     */
 -    String uniqueSeqSetId = null;
 -    String viewId = null;
 -    if (view != null)
 -    {
 -      uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
 -      viewId = (view.getId() == null ? null
 -              : view.getId() + uniqueSetSuffix);
 -    }
 -
 -    // ////////////////////////////////
 -    // LOAD SEQUENCES
 -
 -    List<SequenceI> hiddenSeqs = null;
 -
 -    List<SequenceI> tmpseqs = new ArrayList<>();
 -
 -    boolean multipleView = false;
 -    SequenceI referenceseqForView = null;
 -    // JSeq[] jseqs = object.getJalviewModelSequence().getJSeq();
 -    List<JSeq> jseqs = jalviewModel.getJSeq();
 -    int vi = 0; // counter in vamsasSeq array
 -    for (int i = 0; i < jseqs.size(); i++)
 -    {
 -      JSeq jseq = jseqs.get(i);
 -      String seqId = jseq.getId();
 -
 -      SequenceI tmpSeq = seqRefIds.get(seqId);
 -      if (tmpSeq != null)
 -      {
 -        if (!incompleteSeqs.containsKey(seqId))
 -        {
 -          // may not need this check, but keep it for at least 2.9,1 release
 -          if (tmpSeq.getStart() != jseq.getStart()
 -                  || tmpSeq.getEnd() != jseq.getEnd())
 -          {
 -            System.err.println(String.format(
 -                    "Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
 -                    tmpSeq.getName(), tmpSeq.getStart(), tmpSeq.getEnd(),
 -                    jseq.getStart(), jseq.getEnd()));
 -          }
 -        }
 -        else
 -        {
 -          incompleteSeqs.remove(seqId);
 -        }
 -        if (vamsasSeqs.size() > vi
 -                && vamsasSeqs.get(vi).getId().equals(seqId))
 -        {
 -          // most likely we are reading a dataset XML document so
 -          // update from vamsasSeq section of XML for this sequence
 -          tmpSeq.setName(vamsasSeqs.get(vi).getName());
 -          tmpSeq.setDescription(vamsasSeqs.get(vi).getDescription());
 -          tmpSeq.setSequence(vamsasSeqs.get(vi).getSequence());
 -          vi++;
 -        }
 -        else
 -        {
 -          // reading multiple views, so vamsasSeq set is a subset of JSeq
 -          multipleView = true;
 -        }
 -        tmpSeq.setStart(jseq.getStart());
 -        tmpSeq.setEnd(jseq.getEnd());
 -        tmpseqs.add(tmpSeq);
 -      }
 -      else
 -      {
 -        Sequence vamsasSeq = vamsasSeqs.get(vi);
 -        tmpSeq = new jalview.datamodel.Sequence(vamsasSeq.getName(),
 -                vamsasSeq.getSequence());
 -        tmpSeq.setDescription(vamsasSeq.getDescription());
 -        tmpSeq.setStart(jseq.getStart());
 -        tmpSeq.setEnd(jseq.getEnd());
 -        tmpSeq.setVamsasId(uniqueSetSuffix + seqId);
 -        seqRefIds.put(vamsasSeq.getId(), tmpSeq);
 -        tmpseqs.add(tmpSeq);
 -        vi++;
 -      }
 -
 -      if (safeBoolean(jseq.isViewreference()))
 -      {
 -        referenceseqForView = tmpseqs.get(tmpseqs.size() - 1);
 -      }
 -
 -      if (jseq.isHidden() != null && jseq.isHidden().booleanValue())
 -      {
 -        if (hiddenSeqs == null)
 -        {
 -          hiddenSeqs = new ArrayList<>();
 -        }
  
-   protected void resolveFrefedSequences()
-   {
-     Iterator<SeqFref> nextFref = frefedSequence.iterator();
-     int toresolve = frefedSequence.size();
-     int unresolved = 0, failedtoresolve = 0;
-     while (nextFref.hasNext())
-     {
-       SeqFref ref = nextFref.next();
-       if (ref.isResolvable())
-       {
-         try
-         {
-           if (ref.resolve())
-           {
-             nextFref.remove();
-           }
-           else
-           {
-             failedtoresolve++;
-           }
-         } catch (Exception x)
-         {
-           System.err.println(
-                   "IMPLEMENTATION ERROR: Failed to resolve forward reference for sequence "
-                           + ref.getSref());
-           x.printStackTrace();
-           failedtoresolve++;
-         }
-       }
-       else
-       {
-         unresolved++;
-       }
-     }
-     if (unresolved > 0)
-     {
-       System.err.println("Jalview Project Import: There were " + unresolved
-               + " forward references left unresolved on the stack.");
-     }
-     if (failedtoresolve > 0)
 -        hiddenSeqs.add(tmpSeq);
 -      }
 -    }
 -
 -    // /
 -    // Create the alignment object from the sequence set
 -    // ///////////////////////////////
 -    SequenceI[] orderedSeqs = tmpseqs
 -            .toArray(new SequenceI[tmpseqs.size()]);
 -
 -    AlignmentI al = null;
 -    // so we must create or recover the dataset alignment before going further
 -    // ///////////////////////////////
 -    if (vamsasSet.getDatasetId() == null || vamsasSet.getDatasetId() == "")
 -    {
 -      // older jalview projects do not have a dataset - so creat alignment and
 -      // dataset
 -      al = new Alignment(orderedSeqs);
 -      al.setDataset(null);
 -    }
 -    else
 -    {
 -      boolean isdsal = jalviewModel.getViewport().isEmpty();
 -      if (isdsal)
 -      {
 -        // we are importing a dataset record, so
 -        // recover reference to an alignment already materialsed as dataset
 -        al = getDatasetFor(vamsasSet.getDatasetId());
 -      }
 -      if (al == null)
 -      {
 -        // materialse the alignment
 -        al = new Alignment(orderedSeqs);
 -      }
 -      if (isdsal)
 -      {
 -        addDatasetRef(vamsasSet.getDatasetId(), al);
 -      }
 -
 -      // finally, verify all data in vamsasSet is actually present in al
 -      // passing on flag indicating if it is actually a stored dataset
 -      recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
 -    }
 -
 -    if (referenceseqForView != null)
 -    {
 -      al.setSeqrep(referenceseqForView);
 -    }
 -    // / Add the alignment properties
 -    for (int i = 0; i < vamsasSet.getSequenceSetProperties().size(); i++)
 -    {
 -      SequenceSetProperties ssp = vamsasSet.getSequenceSetProperties()
 -              .get(i);
 -      al.setProperty(ssp.getKey(), ssp.getValue());
 -    }
 -
 -    // ///////////////////////////////
 -
 -    Hashtable pdbloaded = new Hashtable(); // TODO nothing writes to this??
 -    if (!multipleView)
 -    {
 -      // load sequence features, database references and any associated PDB
 -      // structures for the alignment
 -      //
 -      // prior to 2.10, this part would only be executed the first time a
 -      // sequence was encountered, but not afterwards.
 -      // now, for 2.10 projects, this is also done if the xml doc includes
 -      // dataset sequences not actually present in any particular view.
 -      //
 -      for (int i = 0; i < vamsasSeqs.size(); i++)
 -      {
 -        JSeq jseq = jseqs.get(i);
 -        if (jseq.getFeatures().size() > 0)
 -        {
 -          List<Feature> features = jseq.getFeatures();
 -          for (int f = 0; f < features.size(); f++)
 -          {
 -            Feature feat = features.get(f);
 -            SequenceFeature sf = new SequenceFeature(feat.getType(),
 -                    feat.getDescription(), feat.getBegin(), feat.getEnd(),
 -                    safeFloat(feat.getScore()), feat.getFeatureGroup());
 -            sf.setStatus(feat.getStatus());
 -
 -            /*
 -             * load any feature attributes - include map-valued attributes
 -             */
 -            Map<String, Map<String, String>> mapAttributes = new HashMap<>();
 -            for (int od = 0; od < feat.getOtherData().size(); od++)
 -            {
 -              OtherData keyValue = feat.getOtherData().get(od);
 -              String attributeName = keyValue.getKey();
 -              String attributeValue = keyValue.getValue();
 -              if (attributeName.startsWith("LINK"))
 -              {
 -                sf.addLink(attributeValue);
 -              }
 -              else
 -              {
 -                String subAttribute = keyValue.getKey2();
 -                if (subAttribute == null)
 -                {
 -                  // simple string-valued attribute
 -                  sf.setValue(attributeName, attributeValue);
 -                }
 -                else
 -                {
 -                  // attribute 'key' has sub-attribute 'key2'
 -                  if (!mapAttributes.containsKey(attributeName))
 -                  {
 -                    mapAttributes.put(attributeName, new HashMap<>());
 -                  }
 -                  mapAttributes.get(attributeName).put(subAttribute,
 -                          attributeValue);
 -                }
 -              }
 -            }
 -            for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
 -                    .entrySet())
 -            {
 -              sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
 -            }
 -
 -            // adds feature to datasequence's feature set (since Jalview 2.10)
 -            al.getSequenceAt(i).addSequenceFeature(sf);
 -          }
 -        }
 -        if (vamsasSeqs.get(i).getDBRef().size() > 0)
 -        {
 -          // adds dbrefs to datasequence's set (since Jalview 2.10)
 -          addDBRefs(
 -                  al.getSequenceAt(i).getDatasetSequence() == null
 -                          ? al.getSequenceAt(i)
 -                          : al.getSequenceAt(i).getDatasetSequence(),
 -                  vamsasSeqs.get(i));
 -        }
 -        if (jseq.getPdbids().size() > 0)
 -        {
 -          List<Pdbids> ids = jseq.getPdbids();
 -          for (int p = 0; p < ids.size(); p++)
 -          {
 -            Pdbids pdbid = ids.get(p);
 -            jalview.datamodel.PDBEntry entry = new jalview.datamodel.PDBEntry();
 -            entry.setId(pdbid.getId());
 -            if (pdbid.getType() != null)
 -            {
 -              if (PDBEntry.Type.getType(pdbid.getType()) != null)
 -              {
 -                entry.setType(PDBEntry.Type.getType(pdbid.getType()));
 -              }
 -              else
 -              {
 -                entry.setType(PDBEntry.Type.FILE);
 -              }
 -            }
 -            // jprovider is null when executing 'New View'
 -            if (pdbid.getFile() != null && jprovider != null)
 -            {
 -              if (!pdbloaded.containsKey(pdbid.getFile()))
 -              {
 -                entry.setFile(loadPDBFile(jprovider, pdbid.getId(),
 -                        pdbid.getFile()));
 -              }
 -              else
 -              {
 -                entry.setFile(pdbloaded.get(pdbid.getId()).toString());
 -              }
 -            }
 -            /*
 -            if (pdbid.getPdbentryItem() != null)
 -            {
 -              for (PdbentryItem item : pdbid.getPdbentryItem())
 -              {
 -                for (Property pr : item.getProperty())
 -                {
 -                  entry.setProperty(pr.getName(), pr.getValue());
 -                }
 -              }
 -            }
 -            */
 -            for (Property prop : pdbid.getProperty())
 -            {
 -              entry.setProperty(prop.getName(), prop.getValue());
 -            }
 -            StructureSelectionManager
 -                    .getStructureSelectionManager(Desktop.instance)
 -                    .registerPDBEntry(entry);
 -            // adds PDBEntry to datasequence's set (since Jalview 2.10)
 -            if (al.getSequenceAt(i).getDatasetSequence() != null)
 -            {
 -              al.getSequenceAt(i).getDatasetSequence().addPDBId(entry);
 -            }
 -            else
 -            {
 -              al.getSequenceAt(i).addPDBId(entry);
 -            }
 -          }
 -        }
 -      }
 -    } // end !multipleview
 -
 -    // ///////////////////////////////
 -    // LOAD SEQUENCE MAPPINGS
 -
 -    if (vamsasSet.getAlcodonFrame().size() > 0)
 -    {
 -      // TODO Potentially this should only be done once for all views of an
 -      // alignment
 -      List<AlcodonFrame> alc = vamsasSet.getAlcodonFrame();
 -      for (int i = 0; i < alc.size(); i++)
 -      {
 -        AlignedCodonFrame cf = new AlignedCodonFrame();
 -        if (alc.get(i).getAlcodMap().size() > 0)
 -        {
 -          List<AlcodMap> maps = alc.get(i).getAlcodMap();
 -          for (int m = 0; m < maps.size(); m++)
 -          {
 -            AlcodMap map = maps.get(m);
 -            SequenceI dnaseq = seqRefIds.get(map.getDnasq());
 -            // Load Mapping
 -            jalview.datamodel.Mapping mapping = null;
 -            // attach to dna sequence reference.
 -            if (map.getMapping() != null)
 -            {
 -              mapping = addMapping(map.getMapping());
 -              if (dnaseq != null && mapping.getTo() != null)
 -              {
 -                cf.addMap(dnaseq, mapping.getTo(), mapping.getMap());
 -              }
 -              else
 -              {
 -                // defer to later
 -                frefedSequence
 -                        .add(newAlcodMapRef(map.getDnasq(), cf, mapping));
 -              }
 -            }
 -          }
 -          al.addCodonFrame(cf);
 -        }
 -      }
 -    }
 -
 -    // ////////////////////////////////
 -    // LOAD ANNOTATIONS
 -    List<JvAnnotRow> autoAlan = new ArrayList<>();
 -
 -    /*
 -     * store any annotations which forward reference a group's ID
++    /**
++     * Write the data to a new entry of given name in the output jar file
++     * 
++     * @param jout
++     * @param jarEntryName
++     * @param data
++     * @throws IOException
+      */
 -    Map<String, List<AlignmentAnnotation>> groupAnnotRefs = new Hashtable<>();
 -
 -    if (vamsasSet.getAnnotation().size()/*Count()*/ > 0)
 -    {
 -      List<Annotation> an = vamsasSet.getAnnotation();
 -
 -      for (int i = 0; i < an.size(); i++)
 -      {
 -        Annotation annotation = an.get(i);
 -
 -        /**
 -         * test if annotation is automatically calculated for this view only
 -         */
 -        boolean autoForView = false;
 -        if (annotation.getLabel().equals("Quality")
 -                || annotation.getLabel().equals("Conservation")
 -                || annotation.getLabel().equals("Consensus"))
 -        {
 -          // Kludge for pre 2.5 projects which lacked the autocalculated flag
 -          autoForView = true;
 -          // JAXB has no has() test; schema defaults value to false
 -          // if (!annotation.hasAutoCalculated())
 -          // {
 -          // annotation.setAutoCalculated(true);
 -          // }
 -        }
 -        if (autoForView || annotation.isAutoCalculated())
 -        {
 -          // remove ID - we don't recover annotation from other views for
 -          // view-specific annotation
 -          annotation.setId(null);
 -        }
 -
 -        // set visibility for other annotation in this view
 -        String annotationId = annotation.getId();
 -        if (annotationId != null && annotationIds.containsKey(annotationId))
 -        {
 -          AlignmentAnnotation jda = annotationIds.get(annotationId);
 -          // in principle Visible should always be true for annotation displayed
 -          // in multiple views
 -          if (annotation.isVisible() != null)
 -          {
 -            jda.visible = annotation.isVisible();
 -          }
 -
 -          al.addAnnotation(jda);
 -
 -          continue;
 -        }
 -        // Construct new annotation from model.
 -        List<AnnotationElement> ae = annotation.getAnnotationElement();
 -        jalview.datamodel.Annotation[] anot = null;
 -        java.awt.Color firstColour = null;
 -        int anpos;
 -        if (!annotation.isScoreOnly())
 -        {
 -          anot = new jalview.datamodel.Annotation[al.getWidth()];
 -          for (int aa = 0; aa < ae.size() && aa < anot.length; aa++)
 -          {
 -            AnnotationElement annElement = ae.get(aa);
 -            anpos = annElement.getPosition();
 -
 -            if (anpos >= anot.length)
 -            {
 -              continue;
 -            }
 -
 -            float value = safeFloat(annElement.getValue());
 -            anot[anpos] = new jalview.datamodel.Annotation(
 -                    annElement.getDisplayCharacter(),
 -                    annElement.getDescription(),
 -                    (annElement.getSecondaryStructure() == null
 -                            || annElement.getSecondaryStructure()
 -                                    .length() == 0)
 -                                            ? ' '
 -                                            : annElement
 -                                                    .getSecondaryStructure()
 -                                                    .charAt(0),
 -                    value);
 -            anot[anpos].colour = new Color(safeInt(annElement.getColour()));
 -            if (firstColour == null)
 -            {
 -              firstColour = anot[anpos].colour;
 -            }
 -          }
 -        }
 -        jalview.datamodel.AlignmentAnnotation jaa = null;
 -
 -        if (annotation.isGraph())
 -        {
 -          float llim = 0, hlim = 0;
 -          // if (autoForView || an[i].isAutoCalculated()) {
 -          // hlim=11f;
 -          // }
 -          jaa = new jalview.datamodel.AlignmentAnnotation(
 -                  annotation.getLabel(), annotation.getDescription(), anot,
 -                  llim, hlim, safeInt(annotation.getGraphType()));
 -
 -          jaa.graphGroup = safeInt(annotation.getGraphGroup());
 -          jaa._linecolour = firstColour;
 -          if (annotation.getThresholdLine() != null)
 -          {
 -            jaa.setThreshold(new jalview.datamodel.GraphLine(
 -                    safeFloat(annotation.getThresholdLine().getValue()),
 -                    annotation.getThresholdLine().getLabel(),
 -                    new java.awt.Color(safeInt(
 -                            annotation.getThresholdLine().getColour()))));
 -          }
 -          if (autoForView || annotation.isAutoCalculated())
 -          {
 -            // Hardwire the symbol display line to ensure that labels for
 -            // histograms are displayed
 -            jaa.hasText = true;
 -          }
 -        }
 -        else
 -        {
 -          jaa = new jalview.datamodel.AlignmentAnnotation(
 -                  annotation.getLabel(), annotation.getDescription(), anot);
 -          jaa._linecolour = firstColour;
 -        }
 -        // register new annotation
 -        if (annotation.getId() != null)
 -        {
 -          annotationIds.put(annotation.getId(), jaa);
 -          jaa.annotationId = annotation.getId();
 -        }
 -        // recover sequence association
 -        String sequenceRef = annotation.getSequenceRef();
 -        if (sequenceRef != null)
 -        {
 -          // from 2.9 sequenceRef is to sequence id (JAL-1781)
 -          SequenceI sequence = seqRefIds.get(sequenceRef);
 -          if (sequence == null)
 -          {
 -            // in pre-2.9 projects sequence ref is to sequence name
 -            sequence = al.findName(sequenceRef);
 -          }
 -          if (sequence != null)
 -          {
 -            jaa.createSequenceMapping(sequence, 1, true);
 -            sequence.addAlignmentAnnotation(jaa);
 -          }
 -        }
 -        // and make a note of any group association
 -        if (annotation.getGroupRef() != null
 -                && annotation.getGroupRef().length() > 0)
 -        {
 -          List<jalview.datamodel.AlignmentAnnotation> aal = groupAnnotRefs
 -                  .get(annotation.getGroupRef());
 -          if (aal == null)
 -          {
 -            aal = new ArrayList<>();
 -            groupAnnotRefs.put(annotation.getGroupRef(), aal);
 -          }
 -          aal.add(jaa);
 -        }
 -
 -        if (annotation.getScore() != null)
 -        {
 -          jaa.setScore(annotation.getScore().doubleValue());
 -        }
 -        if (annotation.isVisible() != null)
 -        {
 -          jaa.visible = annotation.isVisible().booleanValue();
 -        }
 -
 -        if (annotation.isCentreColLabels() != null)
 -        {
 -          jaa.centreColLabels = annotation.isCentreColLabels()
 -                  .booleanValue();
 -        }
 -
 -        if (annotation.isScaleColLabels() != null)
 -        {
 -          jaa.scaleColLabel = annotation.isScaleColLabels().booleanValue();
 -        }
 -        if (annotation.isAutoCalculated())
 -        {
 -          // newer files have an 'autoCalculated' flag and store calculation
 -          // state in viewport properties
 -          jaa.autoCalculated = true; // means annotation will be marked for
 -          // update at end of load.
 -        }
 -        if (annotation.getGraphHeight() != null)
 -        {
 -          jaa.graphHeight = annotation.getGraphHeight().intValue();
 -        }
 -        jaa.belowAlignment = annotation.isBelowAlignment();
 -        jaa.setCalcId(annotation.getCalcId());
 -        if (annotation.getProperty().size() > 0)
 -        {
 -          for (Annotation.Property prop : annotation.getProperty())
 -          {
 -            jaa.setProperty(prop.getName(), prop.getValue());
 -          }
 -        }
 -        if (jaa.autoCalculated)
 -        {
 -          autoAlan.add(new JvAnnotRow(i, jaa));
 -        }
 -        else
 -        // if (!autoForView)
 -        {
 -          // add autocalculated group annotation and any user created annotation
 -          // for the view
 -          al.addAnnotation(jaa);
 -        }
 -      }
 -    }
 -    // ///////////////////////
 -    // LOAD GROUPS
 -    // Create alignment markup and styles for this view
 -    if (jalviewModel.getJGroup().size() > 0)
 -    {
 -      List<JGroup> groups = jalviewModel.getJGroup();
 -      boolean addAnnotSchemeGroup = false;
 -      for (int i = 0; i < groups.size(); i++)
 -      {
 -        JGroup jGroup = groups.get(i);
 -        ColourSchemeI cs = null;
 -        if (jGroup.getColour() != null)
 -        {
 -          if (jGroup.getColour().startsWith("ucs"))
 -          {
 -            cs = getUserColourScheme(jalviewModel, jGroup.getColour());
 -          }
 -          else if (jGroup.getColour().equals("AnnotationColourGradient")
 -                  && jGroup.getAnnotationColours() != null)
 -          {
 -            addAnnotSchemeGroup = true;
 -          }
 -          else
 -          {
 -            cs = ColourSchemeProperty.getColourScheme(null, al,
 -                    jGroup.getColour());
 -          }
 -        }
 -        int pidThreshold = safeInt(jGroup.getPidThreshold());
 -
 -        Vector<SequenceI> seqs = new Vector<>();
 -
 -        for (int s = 0; s < jGroup.getSeq().size(); s++)
 -        {
 -          String seqId = jGroup.getSeq().get(s);
 -          SequenceI ts = seqRefIds.get(seqId);
 -
 -          if (ts != null)
 -          {
 -            seqs.addElement(ts);
 -          }
 -        }
 -
 -        if (seqs.size() < 1)
 -        {
 -          continue;
 -        }
 -
 -        SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
 -                safeBoolean(jGroup.isDisplayBoxes()),
 -                safeBoolean(jGroup.isDisplayText()),
 -                safeBoolean(jGroup.isColourText()),
 -                safeInt(jGroup.getStart()), safeInt(jGroup.getEnd()));
 -        sg.getGroupColourScheme().setThreshold(pidThreshold, true);
 -        sg.getGroupColourScheme()
 -                .setConservationInc(safeInt(jGroup.getConsThreshold()));
 -        sg.setOutlineColour(new Color(safeInt(jGroup.getOutlineColour())));
 -
 -        sg.textColour = new Color(safeInt(jGroup.getTextCol1()));
 -        sg.textColour2 = new Color(safeInt(jGroup.getTextCol2()));
 -        sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
 -        sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
 -        // attributes with a default in the schema are never null
 -        sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
 -        sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
 -        sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
 -        sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
 -        if (jGroup.getConsThreshold() != null
 -                && jGroup.getConsThreshold().intValue() != 0)
 -        {
 -          Conservation c = new Conservation("All", sg.getSequences(null), 0,
 -                  sg.getWidth() - 1);
 -          c.calculate();
 -          c.verdict(false, 25);
 -          sg.cs.setConservation(c);
 -        }
 -
 -        if (jGroup.getId() != null && groupAnnotRefs.size() > 0)
 -        {
 -          // re-instate unique group/annotation row reference
 -          List<AlignmentAnnotation> jaal = groupAnnotRefs
 -                  .get(jGroup.getId());
 -          if (jaal != null)
 -          {
 -            for (AlignmentAnnotation jaa : jaal)
 -            {
 -              jaa.groupRef = sg;
 -              if (jaa.autoCalculated)
 -              {
 -                // match up and try to set group autocalc alignment row for this
 -                // annotation
 -                if (jaa.label.startsWith("Consensus for "))
 -                {
 -                  sg.setConsensus(jaa);
 -                }
 -                // match up and try to set group autocalc alignment row for this
 -                // annotation
 -                if (jaa.label.startsWith("Conservation for "))
 -                {
 -                  sg.setConservationRow(jaa);
 -                }
 -              }
 -            }
 -          }
 -        }
 -        al.addGroup(sg);
 -        if (addAnnotSchemeGroup)
 -        {
 -          // reconstruct the annotation colourscheme
 -          sg.setColourScheme(
 -                  constructAnnotationColour(jGroup.getAnnotationColours(),
 -                          null, al, jalviewModel, false));
 -        }
 -      }
 -    }
 -    if (view == null)
++    protected void writeJarEntry(JarOutputStream jout, String jarEntryName,
++                               byte[] data) throws IOException
      {
-       System.err.println("SERIOUS! " + failedtoresolve
-               + " resolvable forward references failed to resolve.");
 -      // only dataset in this model, so just return.
 -      return null;
++      if (jout != null)
++          {
++              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();
++          }
      }
-     if (incompleteSeqs != null && incompleteSeqs.size() > 0)
 -    // ///////////////////////////////
 -    // LOAD VIEWPORT
 -
 -    AlignFrame af = null;
 -    AlignViewport av = null;
 -    // now check to see if we really need to create a new viewport.
 -    if (multipleView && viewportsAdded.size() == 0)
 -    {
 -      // We recovered an alignment for which a viewport already exists.
 -      // TODO: fix up any settings necessary for overlaying stored state onto
 -      // state recovered from another document. (may not be necessary).
 -      // we may need a binding from a viewport in memory to one recovered from
 -      // XML.
 -      // and then recover its containing af to allow the settings to be applied.
 -      // TODO: fix for vamsas demo
 -      System.err.println(
 -              "About to recover a viewport for existing alignment: Sequence set ID is "
 -                      + uniqueSeqSetId);
 -      Object seqsetobj = retrieveExistingObj(uniqueSeqSetId);
 -      if (seqsetobj != null)
 -      {
 -        if (seqsetobj instanceof String)
 -        {
 -          uniqueSeqSetId = (String) seqsetobj;
 -          System.err.println(
 -                  "Recovered extant sequence set ID mapping for ID : New Sequence set ID is "
 -                          + uniqueSeqSetId);
 -        }
 -        else
 -        {
 -          System.err.println(
 -                  "Warning : Collision between sequence set ID string and existing jalview object mapping.");
 -        }
 -      }
 -    }
+     /**
 -     * indicate that annotation colours are applied across all groups (pre
 -     * Jalview 2.8.1 behaviour)
++     * Copies input to output, in 4K buffers; handles any data (text or binary)
++     * 
++     * @param in
++     * @param out
++     * @throws IOException
+      */
 -    boolean doGroupAnnColour = Jalview2XML.isVersionStringLaterThan("2.8.1",
 -            jalviewModel.getVersion());
 -
 -    AlignmentPanel ap = null;
 -    boolean isnewview = true;
 -    if (viewId != null)
 -    {
 -      // Check to see if this alignment already has a view id == viewId
 -      jalview.gui.AlignmentPanel views[] = Desktop
 -              .getAlignmentPanels(uniqueSeqSetId);
 -      if (views != null && views.length > 0)
 -      {
 -        for (int v = 0; v < views.length; v++)
 -        {
 -          if (views[v].av.getViewId().equalsIgnoreCase(viewId))
 -          {
 -            // recover the existing alignpanel, alignframe, viewport
 -            af = views[v].alignFrame;
 -            av = views[v].av;
 -            ap = views[v];
 -            // TODO: could even skip resetting view settings if we don't want to
 -            // change the local settings from other jalview processes
 -            isnewview = false;
 -          }
 -        }
 -      }
 -    }
 -
 -    if (isnewview)
++    protected void copyAll(InputStream in, OutputStream out)
++      throws IOException
      {
-       System.err.println(
-               "Jalview Project Import: There are " + incompleteSeqs.size()
-                       + " sequences which may have incomplete metadata.");
-       if (incompleteSeqs.size() < 10)
-       {
-         for (SequenceI s : incompleteSeqs.values())
-         {
-           System.err.println(s.toString());
-         }
-       }
-       else
-       {
-         System.err.println(
-                 "Too many to report. Skipping output of incomplete sequences.");
-       }
 -      af = loadViewport(file, jseqs, hiddenSeqs, al, jalviewModel, view,
 -              uniqueSeqSetId, viewId, autoAlan);
 -      av = af.getViewport();
 -      ap = af.alignPanel;
++      byte[] buffer = new byte[4096];
++      int bytesRead = 0;
++      while ((bytesRead = in.read(buffer)) != -1)
++          {
++              out.write(buffer, 0, bytesRead);
++          }
      }
-   }
  
-   /**
-    * This maintains a map of viewports, the key being the seqSetId. Important to
-    * set historyItem and redoList for multiple views
-    */
-   Map<String, AlignViewport> viewportsAdded = new HashMap<>();
-   Map<String, AlignmentAnnotation> annotationIds = new HashMap<>();
 -    /*
 -     * Load any trees, PDB structures and viewers
++    /**
++     * Save the state of a structure viewer
+      * 
 -     * Not done if flag is false (when this method is used for New View)
++     * @param ap
++     * @param jds
++     * @param pdb
++     *          the archive XML element under which to save the state
++     * @param entry
++     * @param viewIds
++     * @param matchedFile
++     * @param viewFrame
++     * @return
+      */
 -    if (loadTreesAndStructures)
 -    {
 -      loadTrees(jalviewModel, view, af, av, ap);
 -      loadPCAViewers(jalviewModel, ap);
 -      loadPDBStructures(jprovider, jseqs, af, ap);
 -      loadRnaViewers(jprovider, jseqs, ap);
++    protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
++                                       Pdbids pdb, PDBEntry entry, List<String> viewIds,
++                                       String matchedFile, StructureViewerBase viewFrame)
++    {
++      final AAStructureBindingModel bindingModel = viewFrame.getBinding();
++
++      /*
++       * Look for any bindings for this viewer to the PDB file of interest
++       * (including part matches excluding chain id)
++       */
++      for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
++          {
++              final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
++              final String pdbId = pdbentry.getId();
++              if (!pdbId.equals(entry.getId())
++                  && !(entry.getId().length() > 4 && entry.getId().toLowerCase(Locale.ROOT)
++                       .startsWith(pdbId.toLowerCase(Locale.ROOT))))
++                  {
++                      /*
++                       * not interested in a binding to a different PDB entry here
++                       */
++                      continue;
++                  }
++              if (matchedFile == null)
++                  {
++                      matchedFile = pdbentry.getFile();
++                  }
++              else if (!matchedFile.equals(pdbentry.getFile()))
++                  {
++                      Console.warn(
++                                   "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
++                                   + pdbentry.getFile());
++                  }
++              // record the
++              // file so we
++              // can get at it if the ID
++              // match is ambiguous (e.g.
++              // 1QIP==1qipA)
++
++              for (int smap = 0; smap < viewFrame.getBinding()
++                       .getSequence()[peid].length; smap++)
++                  {
++                      // if (jal.findIndex(jmol.jmb.sequence[peid][smap]) > -1)
++                      if (jds == viewFrame.getBinding().getSequence()[peid][smap])
++                          {
++                              StructureState state = new StructureState();
++                              state.setVisible(true);
++                              state.setXpos(viewFrame.getX());
++                              state.setYpos(viewFrame.getY());
++                              state.setWidth(viewFrame.getWidth());
++                              state.setHeight(viewFrame.getHeight());
++                              final String viewId = viewFrame.getViewId();
++                              state.setViewId(viewId);
++                              state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
++                              state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
++                              state.setColourByJmol(viewFrame.isColouredByViewer());
++                              state.setType(viewFrame.getViewerType().toString());
++                              // pdb.addStructureState(state);
++                              pdb.getStructureState().add(state);
++                          }
++                  }
++          }
++      return matchedFile;
+     }
 -    // and finally return.
 -    return af;
 -  }
  
-   String uniqueSetSuffix = "";
 -  /**
 -   * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
 -   * panel is restored from separate jar entries, two (gapped and trimmed) per
 -   * sequence and secondary structure.
 -   * 
 -   * Currently each viewer shows just one sequence and structure (gapped and
 -   * trimmed), however this method is designed to support multiple sequences or
 -   * structures in viewers if wanted in future.
 -   * 
 -   * @param jprovider
 -   * @param jseqs
 -   * @param ap
 -   */
 -  private void loadRnaViewers(jarInputStreamProvider jprovider,
 -          List<JSeq> jseqs, AlignmentPanel ap)
 -  {
 -    /*
 -     * scan the sequences for references to viewers; create each one the first
 -     * time it is referenced, add Rna models to existing viewers
++    /**
++     * Populates the AnnotationColourScheme xml for save. This captures the
++     * settings of the options in the 'Colour by Annotation' dialog.
++     * 
++     * @param acg
++     * @param userColours
++     * @param jm
++     * @return
+      */
 -    for (JSeq jseq : jseqs)
 -    {
 -      for (int i = 0; i < jseq.getRnaViewer().size(); i++)
 -      {
 -        RnaViewer viewer = jseq.getRnaViewer().get(i);
 -        AppVarna appVarna = findOrCreateVarnaViewer(viewer, uniqueSetSuffix,
 -                ap);
++    private AnnotationColourScheme constructAnnotationColours(
++                                                            AnnotationColourGradient acg, List<UserColourScheme> userColours,
++                                                            JalviewModel jm)
++    {
++      AnnotationColourScheme ac = new AnnotationColourScheme();
++      ac.setAboveThreshold(acg.getAboveThreshold());
++      ac.setThreshold(acg.getAnnotationThreshold());
++      // 2.10.2 save annotationId (unique) not annotation label
++      ac.setAnnotation(acg.getAnnotation().annotationId);
++      if (acg.getBaseColour() instanceof UserColourScheme)
++          {
++              ac.setColourScheme(
++                                 setUserColourScheme(acg.getBaseColour(), userColours, jm));
++          }
++      else
++          {
++              ac.setColourScheme(
++                                 ColourSchemeProperty.getColourName(acg.getBaseColour()));
++          }
++
++      ac.setMaxColour(acg.getMaxColour().getRGB());
++      ac.setMinColour(acg.getMinColour().getRGB());
++      ac.setPerSequence(acg.isSeqAssociated());
++      ac.setPredefinedColours(acg.isPredefinedColours());
++      return ac;
++    }
++
++    private void storeAlignmentAnnotation(AlignmentAnnotation[] aa,
++                                        IdentityHashMap<SequenceGroup, String> groupRefs,
++                                        AlignmentViewport av, Set<String> calcIdSet, boolean storeDS,
++                                        SequenceSet vamsasSet)
++    {
++
++      for (int i = 0; i < aa.length; i++)
++          {
++              Annotation an = new Annotation();
++
++              AlignmentAnnotation annotation = aa[i];
++              if (annotation.annotationId != null)
++                  {
++                      annotationIds.put(annotation.annotationId, annotation);
++                  }
++
++              an.setId(annotation.annotationId);
++
++              an.setVisible(annotation.visible);
++
++              an.setDescription(annotation.description);
++
++              if (annotation.sequenceRef != null)
++                  {
++                      // 2.9 JAL-1781 xref on sequence id rather than name
++                      an.setSequenceRef(seqsToIds.get(annotation.sequenceRef));
++                  }
++              if (annotation.groupRef != null)
++                  {
++                      String groupIdr = groupRefs.get(annotation.groupRef);
++                      if (groupIdr == null)
++                          {
++                              // make a locally unique String
++                              groupRefs.put(annotation.groupRef,
++                                            groupIdr = ("" + System.currentTimeMillis()
++                                                        + annotation.groupRef.getName()
++                                                        + groupRefs.size()));
++                          }
++                      an.setGroupRef(groupIdr.toString());
++                  }
++
++              // store all visualization attributes for annotation
++              an.setGraphHeight(annotation.graphHeight);
++              an.setCentreColLabels(annotation.centreColLabels);
++              an.setScaleColLabels(annotation.scaleColLabel);
++              an.setShowAllColLabels(annotation.showAllColLabels);
++              an.setBelowAlignment(annotation.belowAlignment);
++
++              if (annotation.graph > 0)
++                  {
++                      an.setGraph(true);
++                      an.setGraphType(annotation.graph);
++                      an.setGraphGroup(annotation.graphGroup);
++                      if (annotation.getThreshold() != null)
++                          {
++                              ThresholdLine line = new ThresholdLine();
++                              line.setLabel(annotation.getThreshold().label);
++                              line.setValue(annotation.getThreshold().value);
++                              line.setColour(annotation.getThreshold().colour.getRGB());
++                              an.setThresholdLine(line);
++                          }
++                  }
++              else
++                  {
++                      an.setGraph(false);
++                  }
++
++              an.setLabel(annotation.label);
++
++              if (annotation == av.getAlignmentQualityAnnot()
++                  || annotation == av.getAlignmentConservationAnnotation()
++                  || annotation == av.getAlignmentConsensusAnnotation()
++                  || annotation.autoCalculated)
++                  {
++                      // new way of indicating autocalculated annotation -
++                      an.setAutoCalculated(annotation.autoCalculated);
++                  }
++              if (annotation.hasScore())
++                  {
++                      an.setScore(annotation.getScore());
++                  }
++
++              if (annotation.getCalcId() != null)
++                  {
++                      calcIdSet.add(annotation.getCalcId());
++                      an.setCalcId(annotation.getCalcId());
++                  }
++              if (annotation.hasProperties())
++                  {
++                      for (String pr : annotation.getProperties())
++                          {
++                              jalview.xml.binding.jalview.Annotation.Property prop = new jalview.xml.binding.jalview.Annotation.Property();
++                              prop.setName(pr);
++                              prop.setValue(annotation.getProperty(pr));
++                              // an.addProperty(prop);
++                              an.getProperty().add(prop);
++                          }
++                  }
++
++              AnnotationElement ae;
++              if (annotation.annotations != null)
++                  {
++                      an.setScoreOnly(false);
++                      for (int a = 0; a < annotation.annotations.length; a++)
++                          {
++                              if ((annotation == null) || (annotation.annotations[a] == null))
++                                  {
++                                      continue;
++                                  }
++
++                              ae = new AnnotationElement();
++                              if (annotation.annotations[a].description != null)
++                                  {
++                                      ae.setDescription(annotation.annotations[a].description);
++                                  }
++                              if (annotation.annotations[a].displayCharacter != null)
++                                  {
++                                      ae.setDisplayCharacter(
++                                                             annotation.annotations[a].displayCharacter);
++                                  }
++
++                              if (!Float.isNaN(annotation.annotations[a].value))
++                                  {
++                                      ae.setValue(annotation.annotations[a].value);
++                                  }
++
++                              ae.setPosition(a);
++                              if (annotation.annotations[a].secondaryStructure > ' ')
++                                  {
++                                      ae.setSecondaryStructure(
++                                                               annotation.annotations[a].secondaryStructure + "");
++                                  }
++
++                              if (annotation.annotations[a].colour != null
++                                  && annotation.annotations[a].colour != java.awt.Color.black)
++                                  {
++                                      ae.setColour(annotation.annotations[a].colour.getRGB());
++                                  }
++
++                              // an.addAnnotationElement(ae);
++                              an.getAnnotationElement().add(ae);
++                              if (annotation.autoCalculated)
++                                  {
++                                      // only write one non-null entry into the annotation row -
++                                      // sufficient to get the visualization attributes necessary to
++                                      // display data
++                                      continue;
++                                  }
++                          }
++                  }
++              else
++                  {
++                      an.setScoreOnly(true);
++                  }
++              if (!storeDS || (storeDS && !annotation.autoCalculated))
++                  {
++                      // skip autocalculated annotation - these are only provided for
++                      // alignments
++                      // vamsasSet.addAnnotation(an);
++                      vamsasSet.getAnnotation().add(an);
++                  }
++          }
++
++    }
++
++    private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
++    {
++      AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
++      if (settings != null)
++          {
++              CalcIdParam vCalcIdParam = new CalcIdParam();
++              vCalcIdParam.setCalcId(calcId);
++              // vCalcIdParam.addServiceURL(settings.getServiceURI());
++              vCalcIdParam.getServiceURL().add(settings.getServiceURI());
++              // generic URI allowing a third party to resolve another instance of the
++              // service used for this calculation
++              for (String url : settings.getServiceURLs())
++                  {
++                      // vCalcIdParam.addServiceURL(urls);
++                      vCalcIdParam.getServiceURL().add(url);
++                  }
++              vCalcIdParam.setVersion("1.0");
++              if (settings.getPreset() != null)
++                  {
++                      WsParamSetI setting = settings.getPreset();
++                      vCalcIdParam.setName(setting.getName());
++                      vCalcIdParam.setDescription(setting.getDescription());
++                  }
++              else
++                  {
++                      vCalcIdParam.setName("");
++                      vCalcIdParam.setDescription("Last used parameters");
++                  }
++              // need to be able to recover 1) settings 2) user-defined presets or
++              // recreate settings from preset 3) predefined settings provided by
++              // service - or settings that can be transferred (or discarded)
++              vCalcIdParam.setParameters(
++                                         settings.getWsParamFile().replace("\n", "|\\n|"));
++              vCalcIdParam.setAutoUpdate(settings.isAutoUpdate());
++              // todo - decide if updateImmediately is needed for any projects.
++
++              return vCalcIdParam;
++          }
++      return null;
++    }
++
++    private boolean recoverCalcIdParam(CalcIdParam calcIdParam,
++                                     AlignViewport av)
++    {
++      if (calcIdParam.getVersion().equals("1.0"))
++          {
++              final String[] calcIds = calcIdParam.getServiceURL().toArray(new String[0]);
++              ServiceWithParameters service = PreferredServiceRegistry.getRegistry()
++                  .getPreferredServiceFor(calcIds);
++              if (service != null)
++                  {
++                      WsParamSetI parmSet = null;
++                      try
++                          {
++                              parmSet = service.getParamStore().parseServiceParameterFile(
++                                                                                          calcIdParam.getName(), calcIdParam.getDescription(),
++                                                                                          calcIds,
++                                                                                          calcIdParam.getParameters().replace("|\\n|", "\n"));
++                          } catch (IOException x)
++                          {
++                              Console.warn("Couldn't parse parameter data for "
++                                           + calcIdParam.getCalcId(), x);
++                              return false;
++                          }
++                      List<ArgumentI> argList = null;
++                      if (calcIdParam.getName().length() > 0)
++                          {
++                              parmSet = service.getParamStore()
++                                  .getPreset(calcIdParam.getName());
++                              if (parmSet != null)
++                                  {
++                                      // TODO : check we have a good match with settings in AACon -
++                                      // otherwise we'll need to create a new preset
++                                  }
++                          }
++                      else
++                          {
++                              argList = parmSet.getArguments();
++                              parmSet = null;
++                          }
++                      AutoCalcSetting settings = new AAConSettings(
++                                                                   calcIdParam.isAutoUpdate(), service, parmSet, argList);
++                      av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
++                                              calcIdParam.isNeedsUpdate());
++                      return true;
++                  }
++              else
++                  {
++                      Console.warn(
++                                   "Cannot resolve a service for the parameters used in this project. Try configuring a JABAWS server.");
++                      return false;
++                  }
++          }
++      throw new Error(MessageManager.formatMessage(
++                                                   "error.unsupported_version_calcIdparam", new Object[]
++                                                   { calcIdParam.toString() }));
++    }
  
-   /**
-    * List of pdbfiles added to Jar
-    */
-   List<String> pdbfiles = null;
 -        for (int j = 0; j < viewer.getSecondaryStructure().size(); j++)
 -        {
 -          SecondaryStructure ss = viewer.getSecondaryStructure().get(j);
 -          SequenceI seq = seqRefIds.get(jseq.getId());
 -          AlignmentAnnotation ann = this.annotationIds
 -                  .get(ss.getAnnotationId());
++    /**
++     * External mapping between jalview objects and objects yielding a valid and
++     * unique object ID string. This is null for normal Jalview project IO, but
++     * non-null when a jalview project is being read or written as part of a
++     * vamsas session.
++     */
++    IdentityHashMap jv2vobj = null;
  
-   // SAVES SEVERAL ALIGNMENT WINDOWS TO SAME JARFILE
-   public void saveState(File statefile)
-   {
-     FileOutputStream fos = null;
 -          /*
 -           * add the structure to the Varna display (with session state copied
 -           * from the jar to a temporary file)
 -           */
 -          boolean gapped = safeBoolean(ss.isGapped());
 -          String rnaTitle = ss.getTitle();
 -          String sessionState = ss.getViewerState();
 -          String tempStateFile = copyJarEntry(jprovider, sessionState,
 -                  "varna", null);
 -          RnaModel rna = new RnaModel(rnaTitle, ann, seq, null, gapped);
 -          appVarna.addModelSession(rna, rnaTitle, tempStateFile);
 -        }
 -        appVarna.setInitialSelection(safeInt(viewer.getSelectedRna()));
 -      }
++    /**
++     * Construct a unique ID for jvobj using either existing bindings or if none
++     * exist, the result of the hashcode call for the object.
++     * 
++     * @param jvobj
++     *          jalview data object
++     * @return unique ID for referring to jvobj
++     */
++    private String makeHashCode(Object jvobj, String altCode)
++    {
++      if (jv2vobj != null)
++          {
++              Object id = jv2vobj.get(jvobj);
++              if (id != null)
++                  {
++                      return id.toString();
++                  }
++              // check string ID mappings
++              if (jvids2vobj != null && jvobj instanceof String)
++                  {
++                      id = jvids2vobj.get(jvobj);
++                  }
++              if (id != null)
++                  {
++                      return id.toString();
++                  }
++              // give up and warn that something has gone wrong
++              Console.warn(
++                           "Cannot find ID for object in external mapping : " + jvobj);
++          }
++      return altCode;
+     }
 -  }
  
-     try
 -  /**
 -   * Locate and return an already instantiated matching AppVarna, or create one
 -   * if not found
 -   * 
 -   * @param viewer
 -   * @param viewIdSuffix
 -   * @param ap
 -   * @return
 -   */
 -  protected AppVarna findOrCreateVarnaViewer(RnaViewer viewer,
 -          String viewIdSuffix, AlignmentPanel ap)
 -  {
 -    /*
 -     * on each load a suffix is appended to the saved viewId, to avoid conflicts
 -     * if load is repeated
++    /**
++     * return local jalview object mapped to ID, if it exists
++     * 
++     * @param idcode
++     *          (may be null)
++     * @return null or object bound to idcode
+      */
 -    String postLoadId = viewer.getViewId() + viewIdSuffix;
 -    for (JInternalFrame frame : getAllFrames())
++    private Object retrieveExistingObj(String idcode)
      {
 -      if (frame instanceof AppVarna)
 -      {
 -        AppVarna varna = (AppVarna) frame;
 -        if (postLoadId.equals(varna.getViewId()))
 -        {
 -          // this viewer is already instantiated
 -          // could in future here add ap as another 'parent' of the
 -          // AppVarna window; currently just 1-to-many
 -          return varna;
 -        }
 -      }
++      if (idcode != null && vobj2jv != null)
++          {
++              return vobj2jv.get(idcode);
++          }
++      return null;
+     }
  
-       fos = new FileOutputStream(statefile);
 -    /*
 -     * viewer not found - make it
++    /**
++     * binding from ID strings from external mapping table to jalview data model
++     * objects.
+      */
 -    RnaViewerModel model = new RnaViewerModel(postLoadId, viewer.getTitle(),
 -            safeInt(viewer.getXpos()), safeInt(viewer.getYpos()),
 -            safeInt(viewer.getWidth()), safeInt(viewer.getHeight()),
 -            safeInt(viewer.getDividerLocation()));
 -    AppVarna varna = new AppVarna(model, ap);
++    private Hashtable vobj2jv;
++
++    private Sequence createVamsasSequence(String id, SequenceI jds)
++    {
++      return createVamsasSequence(true, id, jds, null);
++    }
++
++    private Sequence createVamsasSequence(boolean recurse, String id,
++                                        SequenceI jds, SequenceI parentseq)
++    {
++      Sequence vamsasSeq = new Sequence();
++      vamsasSeq.setId(id);
++      vamsasSeq.setName(jds.getName());
++      vamsasSeq.setSequence(jds.getSequenceAsString());
++      vamsasSeq.setDescription(jds.getDescription());
++      List<DBRefEntry> dbrefs = null;
++      if (jds.getDatasetSequence() != null)
++          {
++              vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
++          }
++      else
++          {
++              // seqId==dsseqid so we can tell which sequences really are
++              // dataset sequences only
++              vamsasSeq.setDsseqid(id);
++              dbrefs = jds.getDBRefs();
++              if (parentseq == null)
++                  {
++                      parentseq = jds;
++                  }
++          }
++
++      /*
++       * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
++       */
++      if (dbrefs != null)
++          {
++              for (int d = 0, nd = dbrefs.size(); d < nd; d++)
++                  {
++                      DBRef dbref = new DBRef();
++                      DBRefEntry ref = dbrefs.get(d);
++                      dbref.setSource(ref.getSource());
++                      dbref.setVersion(ref.getVersion());
++                      dbref.setAccessionId(ref.getAccessionId());
++                      dbref.setCanonical(ref.isCanonical());
++                      if (ref instanceof GeneLocus)
++                          {
++                              dbref.setLocus(true);
++                          }
++                      if (ref.hasMap())
++                          {
++                              Mapping mp = createVamsasMapping(ref.getMap(), parentseq,
++                                                               jds, recurse);
++                              dbref.setMapping(mp);
++                          }
++                      vamsasSeq.getDBRef().add(dbref);
++                  }
++          }
++      return vamsasSeq;
++    }
++
++    private Mapping createVamsasMapping(jalview.datamodel.Mapping jmp,
++                                      SequenceI parentseq, SequenceI jds, boolean recurse)
++    {
++      Mapping mp = null;
++      if (jmp.getMap() != null)
++          {
++              mp = new Mapping();
++
++              jalview.util.MapList mlst = jmp.getMap();
++              List<int[]> r = mlst.getFromRanges();
++              for (int[] range : r)
++                  {
++                      MapListFrom mfrom = new MapListFrom();
++                      mfrom.setStart(range[0]);
++                      mfrom.setEnd(range[1]);
++                      // mp.addMapListFrom(mfrom);
++                      mp.getMapListFrom().add(mfrom);
++                  }
++              r = mlst.getToRanges();
++              for (int[] range : r)
++                  {
++                      MapListTo mto = new MapListTo();
++                      mto.setStart(range[0]);
++                      mto.setEnd(range[1]);
++                      // mp.addMapListTo(mto);
++                      mp.getMapListTo().add(mto);
++                  }
++              mp.setMapFromUnit(BigInteger.valueOf(mlst.getFromRatio()));
++              mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
++              if (jmp.getTo() != null)
++                  {
++                      // MappingChoice mpc = new MappingChoice();
++
++                      // check/create ID for the sequence referenced by getTo()
++
++                      String jmpid = "";
++                      SequenceI ps = null;
++                      if (parentseq != jmp.getTo()
++                          && parentseq.getDatasetSequence() != jmp.getTo())
++                          {
++                              // chaining dbref rather than a handshaking one
++                              jmpid = seqHash(ps = jmp.getTo());
++                          }
++                      else
++                          {
++                              jmpid = seqHash(ps = parentseq);
++                          }
++                      // mpc.setDseqFor(jmpid);
++                      mp.setDseqFor(jmpid);
++                      if (!seqRefIds.containsKey(jmpid))
++                          {
++                              Console.debug("creatign new DseqFor ID");
++                              seqRefIds.put(jmpid, ps);
++                          }
++                      else
++                          {
++                              Console.debug("reusing DseqFor ID");
++                          }
++
++                      // mp.setMappingChoice(mpc);
++                  }
++          }
++      return mp;
++    }
++
++    String setUserColourScheme(jalview.schemes.ColourSchemeI cs,
++                             List<UserColourScheme> userColours, JalviewModel jm)
++    {
++      String id = null;
++      jalview.schemes.UserColourScheme ucs = (jalview.schemes.UserColourScheme) cs;
++      boolean newucs = false;
++      if (!userColours.contains(ucs))
++          {
++              userColours.add(ucs);
++              newucs = true;
++          }
++      id = "ucs" + userColours.indexOf(ucs);
++      if (newucs)
++          {
++              // actually create the scheme's entry in the XML model
++              java.awt.Color[] colours = ucs.getColours();
++              UserColours uc = new UserColours();
++              // UserColourScheme jbucs = new UserColourScheme();
++              JalviewUserColours jbucs = new JalviewUserColours();
++
++              for (int i = 0; i < colours.length; i++)
++                  {
++                      Colour col = new Colour();
++                      col.setName(ResidueProperties.aa[i]);
++                      col.setRGB(jalview.util.Format.getHexString(colours[i]));
++                      // jbucs.addColour(col);
++                      jbucs.getColour().add(col);
++                  }
++              if (ucs.getLowerCaseColours() != null)
++                  {
++                      colours = ucs.getLowerCaseColours();
++                      for (int i = 0; i < colours.length; i++)
++                          {
++                              Colour col = new Colour();
++                              col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
++                              col.setRGB(jalview.util.Format.getHexString(colours[i]));
++                              // jbucs.addColour(col);
++                              jbucs.getColour().add(col);
++                          }
++                  }
++
++              uc.setId(id);
++              uc.setUserColourScheme(jbucs);
++              // jm.addUserColours(uc);
++              jm.getUserColours().add(uc);
++          }
++
++      return id;
++    }
++
++    jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
++                                                       String id)
++    {
++      List<UserColours> uc = jm.getUserColours();
++      UserColours colours = null;
++      /*
++        for (int i = 0; i < uc.length; i++)
++        {
++        if (uc[i].getId().equals(id))
++        {
++        colours = uc[i];
++        break;
++        }
++        }
++      */
++      for (UserColours c : uc)
++          {
++              if (c.getId().equals(id))
++                  {
++                      colours = c;
++                      break;
++                  }
++          }
++
++      java.awt.Color[] newColours = new java.awt.Color[24];
++
++      for (int i = 0; i < 24; i++)
++          {
++              newColours[i] = new java.awt.Color(Integer.parseInt(
++                                                                  // colours.getUserColourScheme().getColour(i).getRGB(), 16));
++                                                                  colours.getUserColourScheme().getColour().get(i).getRGB(),
++                                                                  16));
++          }
++
++      jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
++                                                                                  newColours);
++
++      if (colours.getUserColourScheme().getColour().size()/*Count()*/ > 24)
++          {
++              newColours = new java.awt.Color[23];
++              for (int i = 0; i < 23; i++)
++                  {
++                      newColours[i] = new java.awt.Color(
++                                                         Integer.parseInt(colours.getUserColourScheme().getColour()
++                                                                          .get(i + 24).getRGB(), 16));
++                  }
++              ucs.setLowerCaseColours(newColours);
++          }
++
++      return ucs;
++    }
  
-       JarOutputStream jout = new JarOutputStream(fos);
-       saveState(jout);
-       fos.close();
 -    return varna;
 -  }
++    /**
++     * Load a jalview project archive from a jar file
++     * 
++     * @param file
++     *          - HTTP URL or filename
++     */
++    public AlignFrame loadJalviewAlign(final Object file)
++    {
++
++      jalview.gui.AlignFrame af = null;
++
++      try
++          {
++              // create list to store references for any new Jmol viewers created
++              newStructureViewers = new Vector<>();
++              // UNMARSHALLER SEEMS TO CLOSE JARINPUTSTREAM, MOST ANNOYING
++              // Workaround is to make sure caller implements the JarInputStreamProvider
++              // interface
++              // so we can re-open the jar input stream for each entry.
++
++              jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
++              af = loadJalviewAlign(jprovider);
++              if (af != null)
++                  {
++                      af.setMenusForViewport();
++                  }
++          } catch (MalformedURLException e)
++          {
++              errorMessage = "Invalid URL format for '" + file + "'";
++              reportErrors();
++          } finally
++          {
++              try
++                  {
++                      // was invokeAndWait
++        
++                      // BH 2019 -- can't wait
++                      SwingUtilities.invokeLater(new Runnable()
++                          {
++                              @Override
++                              public void run()
++                              {
++                                  setLoadingFinishedForNewStructureViewers();
++                              }
++                          });
++                  } catch (Exception x)
++                  {
++                      System.err.println("Error loading alignment: " + x.getMessage());
++                  }
++          }
++      this.jarFile = null;
++      return af;
++    }
++
++    @SuppressWarnings("unused")
++    private jarInputStreamProvider createjarInputStreamProvider(
++                                                              final Object ofile) throws MalformedURLException
++    {
++
++      try
++          {
++              String file = (ofile instanceof File
++                             ? ((File) ofile).getCanonicalPath()
++                             : ofile.toString());
++              byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
++                  : null;
++              if (bytes != null)
++                  {
++                      this.jarFile = (File) ofile;
++                  }
++              URL url;
++              errorMessage = null;
++              uniqueSetSuffix = null;
++              seqRefIds = null;
++              viewportsAdded.clear();
++              frefedSequence = null;
++
++              if (HttpUtils.startsWithHttpOrHttps(file))
++                  {
++                      url = new URL(file);
++                  } else {
++                      url = null;
++              }
++              return new jarInputStreamProvider()
++                  {
++
++                      @Override
++                      public JarInputStream getJarInputStream() throws IOException
++                      {
++                          InputStream is = bytes != null ? new ByteArrayInputStream(bytes)
++                              : (url != null ? url.openStream()
++                                 : new FileInputStream(file));
++                          return new JarInputStream(is);
++                      }
++
++                      @Override
++                      public File getFile()
++                      {
++                          return jarFile;
++                      }
++
++                      @Override
++                      public String getFilename()
++                      {
++                          return file;
++                      }
++              };
++          } catch (IOException e)
++          {
++              e.printStackTrace();
++              return null;
++          }
++    }
  
-     } catch (Exception e)
-     {
-       Cache.log.error("Couln't write Jalview state to " + statefile, e);
-       // TODO: inform user of the problem - they need to know if their data was
-       // not saved !
-       if (errorMessage == null)
-       {
-         errorMessage = "Did't write Jalview Archive to output file '"
-                 + statefile + "' - See console error log for details";
-       }
-       else
-       {
-         errorMessage += "(Didn't write Jalview Archive to output file '"
-                 + statefile + ")";
-       }
-       e.printStackTrace();
-     } finally
-     {
-       if (fos != null)
-       {
-         try
-         {
-           fos.close();
-         } catch (IOException e)
-         {
-           // ignore
-         }
-       }
 -  /**
 -   * Load any saved trees
 -   * 
 -   * @param jm
 -   * @param view
 -   * @param af
 -   * @param av
 -   * @param ap
 -   */
 -  protected void loadTrees(JalviewModel jm, Viewport view, AlignFrame af,
 -          AlignViewport av, AlignmentPanel ap)
 -  {
 -    // TODO result of automated refactoring - are all these parameters needed?
 -    try
 -    {
 -      for (int t = 0; t < jm.getTree().size(); t++)
 -      {
++    /**
++     * Recover jalview session from a jalview project archive. Caller may
++     * initialise uniqueSetSuffix, seqRefIds, viewportsAdded and frefedSequence
++     * themselves. Any null fields will be initialised with default values,
++     * non-null fields are left alone.
++     * 
++     * @param jprovider
++     * @return
++     */
++    public AlignFrame loadJalviewAlign(final jarInputStreamProvider jprovider)
++    {
++      errorMessage = null;
++      if (uniqueSetSuffix == null)
++          {
++              uniqueSetSuffix = System.currentTimeMillis() % 100000 + "";
++          }
++      if (seqRefIds == null)
++          {
++              initSeqRefs();
++          }
++      AlignFrame af = null, _af = null;
++      IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
++      Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
++      String fileName = jprovider.getFilename();
++      File file = jprovider.getFile();
++      List<AlignFrame> alignFrames = new ArrayList<>();
++      try
++          {
++              JarInputStream jin = null;
++              JarEntry jarentry = null;
++              int entryCount = 1;
++
++              // Look for all the entry names ending with ".xml"
++              // This includes all panels and at least one frame.
++              //      Platform.timeCheck(null, Platform.TIME_MARK);
++              do
++                  {
++                      jin = jprovider.getJarInputStream();
++                      for (int i = 0; i < entryCount; i++)
++                          {
++                              jarentry = jin.getNextJarEntry();
++                          }
++                      String name = (jarentry == null ? null : jarentry.getName());
++
++                      //        System.out.println("Jalview2XML opening " + name);
++                      if (name != null && name.endsWith(".xml"))
++                          {
++                              // DataSet for.... is read last.
++          
++          
++                              // The question here is what to do with the two
++                              // .xml files in the jvp file.
++                              // Some number of them, "...Dataset for...", will be the
++                              // Only AlignPanels and will have Viewport.
++                              // One or more will be the source data, with the DBRefs.
++                              //
++                              // JVP file writing (above) ensures tha the AlignPanels are written
++                              // first, then all relevant datasets (which are
++                              // Jalview.datamodel.Alignment).
++                              //
++
++                              //          Platform.timeCheck("Jalview2XML JAXB " + name, Platform.TIME_MARK);
++                              JAXBContext jc = JAXBContext
++                                  .newInstance("jalview.xml.binding.jalview");
++                              XMLStreamReader streamReader = XMLInputFactory.newInstance()
++                                  .createXMLStreamReader(jin);
++                              javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
++                              JAXBElement<JalviewModel> jbe = um
++                                  .unmarshal(streamReader, JalviewModel.class);
++                              JalviewModel model = jbe.getValue();
++
++                              if (true) // !skipViewport(object))
++                                  {
++                                      // Q: Do we have to load from the model, even if it
++                                      // does not have a viewport, could we discover that early on?
++                                      // Q: Do we need to load this object?
++                                      _af = loadFromObject(model, fileName, file, true, jprovider);
++                                      //            Platform.timeCheck("Jalview2XML.loadFromObject",
++                                      // Platform.TIME_MARK);
++
++                                      if (_af != null)
++                                          {
++                                              alignFrames.add(_af);
++                                          }
++                                      if (_af != null && model.getViewport().size() > 0)
++                                          {
++
++                                              // That is, this is one of the AlignmentPanel models
++                                              if (af == null)
++                                                  {
++                                                      // store a reference to the first view
++                                                      af = _af;
++                                                  }
++                                              if (_af.getViewport().isGatherViewsHere())
++                                                  {
++                                                      // if this is a gathered view, keep its reference since
++                                                      // after gathering views, only this frame will remain
++                                                      af = _af;
++                                                      gatherToThisFrame.put(_af.getViewport().getSequenceSetId(),
++                                                                            _af);
++                                                  }
++                                              // Save dataset to register mappings once all resolved
++                                              importedDatasets.put(
++                                                                   af.getViewport().getAlignment().getDataset(),
++                                                                   af.getViewport().getAlignment().getDataset());
++                                          }
++                                  }
++                              entryCount++;
++                          }
++                      else if (jarentry != null)
++                          {
++                              // Some other file here.
++                              entryCount++;
++                          }
++                  } while (jarentry != null);
++              jin.close();
++              resolveFrefedSequences();
++          } catch (IOException ex)
++          {
++              ex.printStackTrace();
++              errorMessage = "Couldn't locate Jalview XML file : " + fileName;
++              System.err.println(
++                                 "Exception whilst loading jalview XML file : " + ex + "\n");
++          } catch (Exception ex)
++          {
++              System.err.println("Parsing as Jalview Version 2 file failed.");
++              ex.printStackTrace(System.err);
++              if (attemptversion1parse)
++                  {
++                      // used to attempt to parse as V1 castor-generated xml
++                  }
++              if (Desktop.getInstance() != null)
++                  {
++                      Desktop.getInstance().stopLoading();
++                  }
++              if (af != null)
++                  {
++                      System.out.println("Successfully loaded archive file");
++                      return af;
++                  }
++              ex.printStackTrace();
++
++              System.err.println(
++                                 "Exception whilst loading jalview XML file : " + ex + "\n");
++          } catch (OutOfMemoryError e)
++          {
++              // Don't use the OOM Window here
++              errorMessage = "Out of memory loading jalview XML file";
++              System.err.println("Out of memory whilst loading jalview XML file");
++              e.printStackTrace();
++          } finally
++          {
++              for (AlignFrame alf : alignFrames)
++                  {
++                      alf.alignPanel.setHoldRepaint(false);
++                  }
++          }
++
++      /*
++       * Regather multiple views (with the same sequence set id) to the frame (if
++       * any) that is flagged as the one to gather to, i.e. convert them to tabbed
++       * views instead of separate frames. Note this doesn't restore a state where
++       * some expanded views in turn have tabbed views - the last "first tab" read
++       * in will play the role of gatherer for all.
++       */
++      for (AlignFrame fr : gatherToThisFrame.values())
++          {
++              Desktop.getInstance().gatherViews(fr);
++          }
++
++      restoreSplitFrames();
++      for (AlignmentI ds : importedDatasets.keySet())
++          {
++              if (ds.getCodonFrames() != null)
++                  {
++                      Desktop.getStructureSelectionManager()
++                          .registerMappings(ds.getCodonFrames());
++                  }
++          }
++      if (errorMessage != null)
++          {
++              reportErrors();
++          }
++
++      if (Desktop.getInstance() != null)
++          {
++              Desktop.getInstance().stopLoading();
++          }
++
++      return af;
 +    }
-     reportErrors();
-   }
-   /**
-    * Writes a jalview project archive to the given Jar output stream.
-    * 
-    * @param jout
-    */
-   public void saveState(JarOutputStream jout)
-   {
-     AlignFrame[] frames = Desktop.getAlignFrames();
  
-     if (frames == null)
-     {
-       return;
 -        Tree tree = jm.getTree().get(t);
++    /**
++     * Try to reconstruct and display SplitFrame windows, where each contains
++     * complementary dna and protein alignments. Done by pairing up AlignFrame
++     * objects (created earlier) which have complementary viewport ids associated.
++     */
++    protected void restoreSplitFrames()
++    {
++      List<SplitFrame> gatherTo = new ArrayList<>();
++      List<AlignFrame> addedToSplitFrames = new ArrayList<>();
++      Map<String, AlignFrame> dna = new HashMap<>();
++
++      /*
++       * Identify the DNA alignments
++       */
++      for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
++               .entrySet())
++          {
++              AlignFrame af = candidate.getValue();
++              if (af.getViewport().getAlignment().isNucleotide())
++                  {
++                      dna.put(candidate.getKey().getId(), af);
++                  }
++          }
++
++      /*
++       * Try to match up the protein complements
++       */
++      for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
++               .entrySet())
++          {
++              AlignFrame af = candidate.getValue();
++              if (!af.getViewport().getAlignment().isNucleotide())
++                  {
++                      String complementId = candidate.getKey().getComplementId();
++                      // only non-null complements should be in the Map
++                      if (complementId != null && dna.containsKey(complementId))
++                          {
++                              final AlignFrame dnaFrame = dna.get(complementId);
++                              SplitFrame sf = createSplitFrame(dnaFrame, af);
++                              addedToSplitFrames.add(dnaFrame);
++                              addedToSplitFrames.add(af);
++                              dnaFrame.setMenusForViewport();
++                              af.setMenusForViewport();
++                              if (af.getViewport().isGatherViewsHere())
++                                  {
++                                      gatherTo.add(sf);
++                                  }
++                          }
++                  }
++          }
++
++      /*
++       * Open any that we failed to pair up (which shouldn't happen!) as
++       * standalone AlignFrame's.
++       */
++      for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
++               .entrySet())
++          {
++              AlignFrame af = candidate.getValue();
++              if (!addedToSplitFrames.contains(af))
++                  {
++                      Viewport view = candidate.getKey();
++                      Desktop.addInternalFrame(af, view.getTitle(),
++                                               safeInt(view.getWidth()), safeInt(view.getHeight()));
++                      af.setMenusForViewport();
++                      System.err.println("Failed to restore view " + view.getTitle()
++                                         + " to split frame");
++                  }
++          }
++
++      /*
++       * Gather back into tabbed views as flagged.
++       */
++      for (SplitFrame sf : gatherTo)
++          {
++              Desktop.getInstance().gatherViews(sf);
++          }
++
++      splitFrameCandidates.clear();
 +    }
-     saveAllFrames(Arrays.asList(frames), jout);
-   }
-   /**
-    * core method for storing state for a set of AlignFrames.
-    * 
-    * @param frames
-    *          - frames involving all data to be exported (including those
-    *          contained in splitframes, though not the split frames themselves)
-    * @param jout
-    *          - project output stream
-    */
-   private void saveAllFrames(List<AlignFrame> frames, JarOutputStream jout)
-   {
-     Hashtable<String, AlignFrame> dsses = new Hashtable<>();
  
-     /*
-      * ensure cached data is clear before starting
 -        TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
 -        if (tp == null)
 -        {
 -          tp = af.showNewickTree(new NewickFile(tree.getNewick()),
 -                  tree.getTitle(), safeInt(tree.getWidth()),
 -                  safeInt(tree.getHeight()), safeInt(tree.getXpos()),
 -                  safeInt(tree.getYpos()));
 -          if (tree.getId() != null)
 -          {
 -            // perhaps bind the tree id to something ?
 -          }
 -        }
 -        else
 -        {
 -          // update local tree attributes ?
 -          // TODO: should check if tp has been manipulated by user - if so its
 -          // settings shouldn't be modified
 -          tp.setTitle(tree.getTitle());
 -          tp.setBounds(new Rectangle(safeInt(tree.getXpos()),
 -                  safeInt(tree.getYpos()), safeInt(tree.getWidth()),
 -                  safeInt(tree.getHeight())));
 -          tp.setViewport(av); // af.viewport;
 -          // TODO: verify 'associate with all views' works still
 -          tp.getTreeCanvas().setViewport(av); // af.viewport;
 -          tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
 -        }
 -        tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
 -        if (tp == null)
 -        {
 -          Console.warn(
 -                  "There was a problem recovering stored Newick tree: \n"
 -                          + tree.getNewick());
 -          continue;
 -        }
++    /**
++     * Construct and display one SplitFrame holding DNA and protein alignments.
++     * 
++     * @param dnaFrame
++     * @param proteinFrame
++     * @return
 +     */
-     // todo tidy up seqRefIds, seqsToIds initialisation / reset
-     rnaSessions.clear();
-     splitFrameCandidates.clear();
-     try
++    protected SplitFrame createSplitFrame(AlignFrame dnaFrame,
++                                        AlignFrame proteinFrame)
 +    {
++      SplitFrame splitFrame = new SplitFrame(dnaFrame, proteinFrame);
++      String title = MessageManager.getString("label.linked_view_title");
++      int width = (int) dnaFrame.getBounds().getWidth();
++      int height = (int) (dnaFrame.getBounds().getHeight()
++                          + proteinFrame.getBounds().getHeight() + 50);
  
-       // NOTE UTF-8 MUST BE USED FOR WRITING UNICODE CHARS
-       // //////////////////////////////////////////////////
-       List<String> shortNames = new ArrayList<>();
-       List<String> viewIds = new ArrayList<>();
-       // REVERSE ORDER
-       for (int i = frames.size() - 1; i > -1; i--)
-       {
-         AlignFrame af = frames.get(i);
-         AlignViewport vp = af.getViewport();
-         // skip ?
-         if (skipList != null && skipList
-                 .containsKey(vp.getSequenceSetId()))
-         {
-           continue;
-         }
-         String shortName = makeFilename(af, shortNames);
-         AlignmentI alignment = vp.getAlignment();
-         List<? extends AlignmentViewPanel> panels = af.getAlignPanels();
-         int apSize = panels.size();
-         for (int ap = 0; ap < apSize; ap++)
-           {
-           AlignmentPanel apanel = (AlignmentPanel) panels.get(ap);
-           String fileName = apSize == 1 ? shortName : ap + shortName;
-           if (!fileName.endsWith(".xml"))
-           {
-             fileName = fileName + ".xml";
-           }
-           saveState(apanel, fileName, jout, viewIds);
 -        tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
 -        tp.fitToWindow_actionPerformed(null);
++      /*
++       * SplitFrame location is saved to both enclosed frames
++       */
++      splitFrame.setLocation(dnaFrame.getX(), dnaFrame.getY());
++      Desktop.addInternalFrame(splitFrame, title, width, height);
  
-         }
-         if (apSize > 0)
-         {
-           // BH moved next bit out of inner loop, not that it really matters.
-           // so we are testing to make sure we actually have an alignment,
-           // apparently.
-           String dssid = getDatasetIdRef(alignment.getDataset());
-           if (!dsses.containsKey(dssid))
-           {
-             // We have not already covered this data by reference from another
-             // frame.
-             dsses.put(dssid, af);
-           }
-         }
-       }
 -        if (tree.getFontName() != null)
 -        {
 -          tp.setTreeFont(
 -                  new Font(tree.getFontName(), safeInt(tree.getFontStyle()),
 -                          safeInt(tree.getFontSize())));
 -        }
 -        else
 -        {
 -          tp.setTreeFont(
 -                  new Font(view.getFontName(), safeInt(view.getFontStyle()),
 -                          safeInt(view.getFontSize())));
 -        }
++      /*
++       * And compute cDNA consensus (couldn't do earlier with consensus as
++       * mappings were not yet present)
++       */
++      proteinFrame.getViewport().alignmentChanged(proteinFrame.alignPanel);
  
-       writeDatasetFor(dsses, "" + jout.hashCode() + " " + uniqueSetSuffix,
-               jout);
-       try
-       {
-         jout.flush();
-       } catch (Exception foo)
-       {
-       }
-       jout.close();
-     } catch (Exception ex)
-     {
-       // TODO: inform user of the problem - they need to know if their data was
-       // not saved !
-       if (errorMessage == null)
-       {
-         errorMessage = "Couldn't write Jalview Archive - see error output for details";
-       }
-       ex.printStackTrace();
 -        tp.showPlaceholders(safeBoolean(tree.isMarkUnlinked()));
 -        tp.showBootstrap(safeBoolean(tree.isShowBootstrap()));
 -        tp.showDistances(safeBoolean(tree.isShowDistances()));
++      return splitFrame;
 +    }
-   }
-   /**
-    * Generates a distinct file name, based on the title of the AlignFrame, by
-    * appending _n for increasing n until an unused name is generated. The new
-    * name (without its extension) is added to the list.
-    * 
-    * @param af
-    * @param namesUsed
-    * @return the generated name, with .xml extension
-    */
-   protected String makeFilename(AlignFrame af, List<String> namesUsed)
-   {
-     String shortName = af.getTitle();
  
-     if (shortName.indexOf(File.separatorChar) > -1)
-     {
-       shortName = shortName
-               .substring(shortName.lastIndexOf(File.separatorChar) + 1);
-     }
 -        tp.getTreeCanvas().setThreshold(safeFloat(tree.getThreshold()));
++    /**
++     * check errorMessage for a valid error message and raise an error box in the
++     * GUI or write the current errorMessage to stderr and then clear the error
++     * state.
++     */
++    protected void reportErrors()
++    {
++      reportErrors(false);
++    }
++
++    protected void reportErrors(final boolean saving)
++    {
++      if (errorMessage != null)
++          {
++              final String finalErrorMessage = errorMessage;
++              if (raiseGUI)
++                  {
++                      javax.swing.SwingUtilities.invokeLater(new Runnable()
++                          {
++                              @Override
++                              public void run()
++                              {
++                                  JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
++                                                                         finalErrorMessage,
++                                                                         "Error " + (saving ? "saving" : "loading")
++                                                                         + " Jalview file",
++                                                                         JvOptionPane.WARNING_MESSAGE);
++                              }
++                          });
++                  }
++              else
++                  {
++                      System.err.println("Problem loading Jalview file: " + errorMessage);
++                  }
++          }
++      errorMessage = null;
++    }
++
++    Map<String, String> alreadyLoadedPDB = new HashMap<>();
  
-     int count = 1;
 -        if (safeBoolean(tree.isCurrentTree()))
 -        {
 -          af.getViewport().setCurrentTree(tp.getTree());
 -        }
 -      }
++    /**
++     * when set, local views will be updated from view stored in JalviewXML
++     * Currently (28th Sep 2008) things will go horribly wrong in vamsas document
++     * sync if this is set to true.
++     */
++    private final boolean updateLocalViews = false;
  
-     while (namesUsed.contains(shortName))
 -    } catch (Exception ex)
++    /**
++     * Returns the path to a temporary file holding the PDB file for the given PDB
++     * id. The first time of asking, searches for a file of that name in the
++     * Jalview project jar, and copies it to a new temporary file. Any repeat
++     * requests just return the path to the file previously created.
++     * 
++     * @param jprovider
++     * @param pdbId
++     * @return
++     */
++    String loadPDBFile(jarInputStreamProvider jprovider, String pdbId,
++                     String origFile)
      {
-       if (shortName.endsWith("_" + (count - 1)))
-       {
-         shortName = shortName.substring(0, shortName.lastIndexOf("_"));
-       }
 -      ex.printStackTrace();
++      if (alreadyLoadedPDB.containsKey(pdbId))
++          {
++              return alreadyLoadedPDB.get(pdbId).toString();
++          }
 +
-       shortName = shortName.concat("_" + count);
-       count++;
++      String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb",
++                                     origFile);
++      if (tempFile != null)
++          {
++              alreadyLoadedPDB.put(pdbId, tempFile);
++          }
++      return tempFile;
      }
 -  }
  
-     namesUsed.add(shortName);
-     if (!shortName.endsWith(".xml"))
-     {
-       shortName = shortName + ".xml";
 -  /**
 -   * Load and link any saved structure viewers.
 -   * 
 -   * @param jprovider
 -   * @param jseqs
 -   * @param af
 -   * @param ap
 -   */
 -  protected void loadPDBStructures(jarInputStreamProvider jprovider,
 -          List<JSeq> jseqs, AlignFrame af, AlignmentPanel ap)
 -  {
 -    /*
 -     * Run through all PDB ids on the alignment, and collect mappings between
 -     * distinct view ids and all sequences referring to that view.
++    /**
++     * Copies the jar entry of given name to a new temporary file and returns the
++     * path to the file, or null if the entry is not found.
++     * 
++     * @param jprovider
++     * @param jarEntryName
++     * @param prefix
++     *          a prefix for the temporary file name, must be at least three
++     *          characters long
++     * @param suffixModel
++     *          null or original file - so new file can be given the same suffix
++     *          as the old one
++     * @return
+      */
 -    Map<String, StructureViewerModel> structureViewers = new LinkedHashMap<>();
++    protected String copyJarEntry(jarInputStreamProvider jprovider,
++                                String jarEntryName, String prefix, String suffixModel)
++    {
++      BufferedReader in = null;
++      PrintWriter out = null;
++      String suffix = ".tmp";
++      if (suffixModel == null)
++          {
++              suffixModel = jarEntryName;
++          }
++      int sfpos = suffixModel.lastIndexOf(".");
++      if (sfpos > -1 && sfpos < (suffixModel.length() - 1))
++          {
++              suffix = "." + suffixModel.substring(sfpos + 1);
++          }
++
++      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));
++                      File outFile = File.createTempFile(prefix, suffix);
++                      outFile.deleteOnExit();
++                      try (OutputStream os = new FileOutputStream(outFile))
++                          {
++                              copyAll(jin, os);
++                          }
++                      String t = outFile.getAbsolutePath();
++                      return t;
++                  }
++              else
++                  {
++                      Console.warn(
++                                   "Couldn't find entry in Jalview Jar for " + jarEntryName);
++                  }
++          } catch (Exception ex)
++          {
++              ex.printStackTrace();
++          }
++
++      return null;
++    }
++
++    private class JvAnnotRow
++    {
++      public JvAnnotRow(int i, AlignmentAnnotation jaa)
++      {
++          order = i;
++          template = jaa;
++      }
++
++      /**
++       * persisted version of annotation row from which to take vis properties
++       */
++      public jalview.datamodel.AlignmentAnnotation template;
++
++      /**
++       * original position of the annotation row in the alignment
++       */
++      public int order;
 +    }
-     return shortName;
-   }
  
-   // USE THIS METHOD TO SAVE A SINGLE ALIGNMENT WINDOW
-   public boolean saveAlignment(AlignFrame af, String jarFile,
-           String fileName)
-   {
-     try
 -    for (int i = 0; i < jseqs.size(); i++)
++    /**
++     * Load alignment frame from jalview XML DOM object. For a DOM object that
++     * includes one or more Viewport elements (one with a title that does NOT
++     * contain "Dataset for"), create the frame.
++     * 
++     * @param jalviewModel
++     *          DOM
++     * @param fileName
++     *          filename source string
++     * @param file 
++     * @param loadTreesAndStructures
++     *          when false only create Viewport
++     * @param jprovider
++     *          data source provider
++     * @return alignment frame created from view stored in DOM
++     */
++    AlignFrame loadFromObject(JalviewModel jalviewModel, String fileName,
++                            File file, boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
      {
-       // create backupfiles object and get new temp filename destination
-       boolean doBackup = BackupFiles.getEnabled();
-       BackupFiles backupfiles = doBackup ? new BackupFiles(jarFile) : null;
-       FileOutputStream fos = new FileOutputStream(doBackup ? 
-               backupfiles.getTempFilePath() : jarFile);
 -      JSeq jseq = jseqs.get(i);
 -      if (jseq.getPdbids().size() > 0)
 -      {
 -        List<Pdbids> ids = jseq.getPdbids();
 -        for (int p = 0; p < ids.size(); p++)
 -        {
 -          Pdbids pdbid = ids.get(p);
 -          final int structureStateCount = pdbid.getStructureState().size();
 -          for (int s = 0; s < structureStateCount; s++)
 -          {
 -            // check to see if we haven't already created this structure view
 -            final StructureState structureState = pdbid.getStructureState()
 -                    .get(s);
 -            String sviewid = (structureState.getViewId() == null) ? null
 -                    : structureState.getViewId() + uniqueSetSuffix;
 -            jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
 -            // Originally : pdbid.getFile()
 -            // : TODO: verify external PDB file recovery still works in normal
 -            // jalview project load
 -            jpdb.setFile(
 -                    loadPDBFile(jprovider, pdbid.getId(), pdbid.getFile()));
 -            jpdb.setId(pdbid.getId());
 -
 -            int x = safeInt(structureState.getXpos());
 -            int y = safeInt(structureState.getYpos());
 -            int width = safeInt(structureState.getWidth());
 -            int height = safeInt(structureState.getHeight());
 -
 -            // Probably don't need to do this anymore...
 -            // Desktop.desktop.getComponentAt(x, y);
 -            // TODO: NOW: check that this recovers the PDB file correctly.
 -            String pdbFile = loadPDBFile(jprovider, pdbid.getId(),
 -                    pdbid.getFile());
 -            jalview.datamodel.SequenceI seq = seqRefIds
 -                    .get(jseq.getId() + "");
 -            if (sviewid == null)
 -            {
 -              sviewid = "_jalview_pre2_4_" + x + "," + y + "," + width + ","
 -                      + height;
 -            }
 -            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(),
 -                              viewerType));
 -              // Legacy pre-2.7 conversion JAL-823 :
 -              // do not assume any view has to be linked for colour by
 -              // sequence
 -            }
++      SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet().get(0);
++      List<Sequence> vamsasSeqs = vamsasSet.getSequence();
  
-       JarOutputStream jout = new JarOutputStream(fos);
-       List<AlignFrame> frames = new ArrayList<>();
 -            // assemble String[] { pdb files }, String[] { id for each
 -            // file }, orig_fileloc, SequenceI[][] {{ seqs_file 1 }, {
 -            // seqs_file 2}, boolean[] {
 -            // linkAlignPanel,superposeWithAlignpanel}} from hash
 -            StructureViewerModel jmoldat = structureViewers.get(sviewid);
 -            jmoldat.setAlignWithPanel(jmoldat.isAlignWithPanel()
 -                    || structureState.isAlignwithAlignPanel());
 -
 -            /*
 -             * Default colour by linked panel to false if not specified (e.g.
 -             * for pre-2.7 projects)
 -             */
 -            boolean colourWithAlignPanel = jmoldat.isColourWithAlignPanel();
 -            colourWithAlignPanel |= structureState.isColourwithAlignPanel();
 -            jmoldat.setColourWithAlignPanel(colourWithAlignPanel);
 -
 -            /*
 -             * Default colour by viewer to true if not specified (e.g. for
 -             * pre-2.7 projects)
 -             */
 -            boolean colourByViewer = jmoldat.isColourByViewer();
 -            colourByViewer &= structureState.isColourByJmol();
 -            jmoldat.setColourByViewer(colourByViewer);
 -
 -            if (jmoldat.getStateData().length() < structureState.getValue()
 -                    /*Content()*/.length())
 -            {
 -              jmoldat.setStateData(structureState.getValue());// Content());
 -            }
 -            if (pdbid.getFile() != null)
 -            {
 -              File mapkey = new File(pdbid.getFile());
 -              StructureData seqstrmaps = jmoldat.getFileData().get(mapkey);
 -              if (seqstrmaps == null)
 -              {
 -                jmoldat.getFileData().put(mapkey,
 -                        seqstrmaps = jmoldat.new StructureData(pdbFile,
 -                                pdbid.getId()));
 -              }
 -              if (!seqstrmaps.getSeqList().contains(seq))
 -              {
 -                seqstrmaps.getSeqList().add(seq);
 -                // TODO and chains?
 -              }
 -            }
 -            else
 -            {
 -              errorMessage = ("The Jmol views in this project were imported\nfrom an older version of Jalview.\nPlease review the sequence colour associations\nin the Colour by section of the Jmol View menu.\n\nIn the case of problems, see note at\nhttp://issues.jalview.org/browse/JAL-747");
 -              Console.warn(errorMessage);
 -            }
 -          }
 -        }
 -      }
 -    }
 -    // Instantiate the associated structure views
 -    for (Entry<String, StructureViewerModel> entry : structureViewers
 -            .entrySet())
 -    {
 -      try
 -      {
 -        createOrLinkStructureViewer(entry, af, ap, jprovider);
 -      } catch (Exception e)
 -      {
 -        System.err.println(
 -                "Error loading structure viewer: " + e.getMessage());
 -        // failed - try the next one
 -      }
 -    }
 -  }
++      // JalviewModelSequence jms = object.getJalviewModelSequence();
  
-       // resolve splitframes
-       if (af.getViewport().getCodingComplement() != null)
-       {
-         frames = ((SplitFrame) af.getSplitViewContainer()).getAlignFrames();
-       }
-       else
-       {
-         frames.add(af);
-       }
-       saveAllFrames(frames, jout);
-       try
-       {
-         jout.flush();
-       } catch (Exception foo)
-       {
-       }
-       jout.close();
-       boolean success = true;
 -  /**
 -   * 
 -   * @param viewerData
 -   * @param af
 -   * @param ap
 -   * @param jprovider
 -   */
 -  protected void createOrLinkStructureViewer(
 -          Entry<String, StructureViewerModel> viewerData, AlignFrame af,
 -          AlignmentPanel ap, jarInputStreamProvider jprovider)
 -  {
 -    final StructureViewerModel stateData = viewerData.getValue();
++      // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
++      // : null;
++      Viewport view = (jalviewModel.getViewport().size() > 0)
++            ? jalviewModel.getViewport().get(0)
++            : null;
  
-       if (doBackup)
-       {
-         backupfiles.setWriteSuccess(success);
-         success = backupfiles.rollBackupsAndRenameTempFile();
-       }
 -    /*
 -     * Search for any viewer windows already open from other alignment views
 -     * that exactly match the stored structure state
 -     */
 -    StructureViewerBase comp = findMatchingViewer(viewerData);
++      // ////////////////////////////////
++      // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
++      //
++      //
++      // If we just load in the same jar file again, the sequenceSetId
++      // will be the same, and we end up with multiple references
++      // to the same sequenceSet. We must modify this id on load
++      // so that each load of the file gives a unique id
++
++      /**
++       * used to resolve correct alignment dataset for alignments with multiple
++       * views
++       */
++      String uniqueSeqSetId = null;
++      String viewId = null;
++      if (view != null)
++          {
++              uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
++              viewId = (view.getId() == null ? null
++                        : view.getId() + uniqueSetSuffix);
++          }
++
++      // ////////////////////////////////
++      // LOAD SEQUENCES
++
++      List<SequenceI> hiddenSeqs = null;
++
++      List<SequenceI> tmpseqs = new ArrayList<>();
++
++      boolean multipleView = false;
++      SequenceI referenceseqForView = null;
++      // JSeq[] jseqs = object.getJalviewModelSequence().getJSeq();
++      List<JSeq> jseqs = jalviewModel.getJSeq();
++      int vi = 0; // counter in vamsasSeq array
++      for (int i = 0; i < jseqs.size(); i++)
++          {
++              JSeq jseq = jseqs.get(i);
++              String seqId = jseq.getId();
++
++              SequenceI tmpSeq = seqRefIds.get(seqId);
++              if (tmpSeq != null)
++                  {
++                      if (!incompleteSeqs.containsKey(seqId))
++                          {
++                              // may not need this check, but keep it for at least 2.9,1 release
++                              if (tmpSeq.getStart() != jseq.getStart()
++                                  || tmpSeq.getEnd() != jseq.getEnd())
++                                  {
++                                      Console.warn(
++                                                   String.format("Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
++                                                                 tmpSeq.getName(), tmpSeq.getStart(),
++                                                                 tmpSeq.getEnd(), jseq.getStart(),
++                                                                 jseq.getEnd()));
++                                  }
++                          }
++                      else
++                          {
++                              incompleteSeqs.remove(seqId);
++                          }
++                      if (vamsasSeqs.size() > vi
++                          && vamsasSeqs.get(vi).getId().equals(seqId))
++                          {
++                              // most likely we are reading a dataset XML document so
++                              // update from vamsasSeq section of XML for this sequence
++                              tmpSeq.setName(vamsasSeqs.get(vi).getName());
++                              tmpSeq.setDescription(vamsasSeqs.get(vi).getDescription());
++                              tmpSeq.setSequence(vamsasSeqs.get(vi).getSequence());
++                              vi++;
++                          }
++                      else
++                          {
++                              // reading multiple views, so vamsasSeq set is a subset of JSeq
++                              multipleView = true;
++                          }
++                      tmpSeq.setStart(jseq.getStart());
++                      tmpSeq.setEnd(jseq.getEnd());
++                      tmpseqs.add(tmpSeq);
++                  }
++              else
++                  {
++                      Sequence vamsasSeq = vamsasSeqs.get(vi);
++                      tmpSeq = new jalview.datamodel.Sequence(vamsasSeq.getName(),
++                                                              vamsasSeq.getSequence());
++                      tmpSeq.setDescription(vamsasSeq.getDescription());
++                      tmpSeq.setStart(jseq.getStart());
++                      tmpSeq.setEnd(jseq.getEnd());
++                      tmpSeq.setVamsasId(uniqueSetSuffix + seqId);
++                      seqRefIds.put(vamsasSeq.getId(), tmpSeq);
++                      tmpseqs.add(tmpSeq);
++                      vi++;
++                  }
++
++              if (safeBoolean(jseq.isViewreference()))
++                  {
++                      referenceseqForView = tmpseqs.get(tmpseqs.size() - 1);
++                  }
++
++              if (jseq.isHidden() != null && jseq.isHidden().booleanValue())
++                  {
++                      if (hiddenSeqs == null)
++                          {
++                              hiddenSeqs = new ArrayList<>();
++                          }
++
++                      hiddenSeqs.add(tmpSeq);
++                  }
++          }
++
++      // /
++      // Create the alignment object from the sequence set
++      // ///////////////////////////////
++      SequenceI[] orderedSeqs = tmpseqs
++            .toArray(new SequenceI[tmpseqs.size()]);
  
-       return success;
-     } catch (Exception ex)
-     {
-       errorMessage = "Couldn't Write alignment view to Jalview Archive - see error output for details";
-       ex.printStackTrace();
-       return false;
 -    if (comp != null)
 -    {
 -      linkStructureViewer(ap, comp, stateData);
 -      return;
++      AlignmentI al = null;
++      // so we must create or recover the dataset alignment before going further
++      // ///////////////////////////////
++      if (vamsasSet.getDatasetId() == null || vamsasSet.getDatasetId() == "")
++          {
++              // older jalview projects do not have a dataset - so creat alignment and
++              // dataset
++              al = new Alignment(orderedSeqs);
++              al.setDataset(null);
++          }
++      else
++          {
++              boolean isdsal = jalviewModel.getViewport().isEmpty();
++              if (isdsal)
++                  {
++                      // we are importing a dataset record, so
++                      // recover reference to an alignment already materialsed as dataset
++                      al = getDatasetFor(vamsasSet.getDatasetId());
++                  }
++              if (al == null)
++                  {
++                      // materialse the alignment
++                      al = new Alignment(orderedSeqs);
++                  }
++              if (isdsal)
++                  {
++                      addDatasetRef(vamsasSet.getDatasetId(), al);
++                  }
++
++              // finally, verify all data in vamsasSet is actually present in al
++              // passing on flag indicating if it is actually a stored dataset
++              recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
++          }
++
++      if (referenceseqForView != null)
++          {
++              al.setSeqrep(referenceseqForView);
++          }
++      // / Add the alignment properties
++      for (int i = 0; i < vamsasSet.getSequenceSetProperties().size(); i++)
++          {
++              SequenceSetProperties ssp = vamsasSet.getSequenceSetProperties()
++                  .get(i);
++              al.setProperty(ssp.getKey(), ssp.getValue());
++          }
++  
++      // ///////////////////////////////
++
++      Hashtable pdbloaded = new Hashtable(); // TODO nothing writes to this??
++      if (!multipleView)
++          {
++              // load sequence features, database references and any associated PDB
++              // structures for the alignment
++              //
++              // prior to 2.10, this part would only be executed the first time a
++              // sequence was encountered, but not afterwards.
++              // now, for 2.10 projects, this is also done if the xml doc includes
++              // dataset sequences not actually present in any particular view.
++              //
++              for (int i = 0; i < vamsasSeqs.size(); i++)
++                  {
++                      JSeq jseq = jseqs.get(i);
++                      if (jseq.getFeatures().size() > 0)
++                          {
++                              List<Feature> features = jseq.getFeatures();
++                              for (int f = 0; f < features.size(); f++)
++                                  {
++                                      Feature feat = features.get(f);
++                                      SequenceFeature sf = new SequenceFeature(feat.getType(),
++                                                                               feat.getDescription(), feat.getBegin(), feat.getEnd(),
++                                                                               safeFloat(feat.getScore()), feat.getFeatureGroup());
++                                      sf.setStatus(feat.getStatus());
++
++                                      /*
++                                       * load any feature attributes - include map-valued attributes
++                                       */
++                                      Map<String, Map<String, String>> mapAttributes = new HashMap<>();
++                                      for (int od = 0; od < feat.getOtherData().size(); od++)
++                                          {
++                                              OtherData keyValue = feat.getOtherData().get(od);
++                                              String attributeName = keyValue.getKey();
++                                              String attributeValue = keyValue.getValue();
++                                              if (attributeName.startsWith("LINK"))
++                                                  {
++                                                      sf.addLink(attributeValue);
++                                                  }
++                                              else
++                                                  {
++                                                      String subAttribute = keyValue.getKey2();
++                                                      if (subAttribute == null)
++                                                          {
++                                                              // simple string-valued attribute
++                                                              sf.setValue(attributeName, attributeValue);
++                                                          }
++                                                      else
++                                                          {
++                                                              // attribute 'key' has sub-attribute 'key2'
++                                                              if (!mapAttributes.containsKey(attributeName))
++                                                                  {
++                                                                      mapAttributes.put(attributeName, new HashMap<>());
++                                                                  }
++                                                              mapAttributes.get(attributeName).put(subAttribute,
++                                                                                                   attributeValue);
++                                                          }
++                                                  }
++                                          }
++                                      for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
++                                               .entrySet())
++                                          {
++                                              sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
++                                          }
++
++                                      // adds feature to datasequence's feature set (since Jalview 2.10)
++                                      al.getSequenceAt(i).addSequenceFeature(sf);
++                                  }
++                          }
++                      if (vamsasSeqs.get(i).getDBRef().size() > 0)
++                          {
++                              // adds dbrefs to datasequence's set (since Jalview 2.10)
++                              addDBRefs(
++                                        al.getSequenceAt(i).getDatasetSequence() == null
++                                        ? al.getSequenceAt(i)
++                                        : al.getSequenceAt(i).getDatasetSequence(),
++                                        vamsasSeqs.get(i));
++                          }
++                      if (jseq.getPdbids().size() > 0)
++                          {
++                              List<Pdbids> ids = jseq.getPdbids();
++                              for (int p = 0; p < ids.size(); p++)
++                                  {
++                                      Pdbids pdbid = ids.get(p);
++                                      jalview.datamodel.PDBEntry entry = new jalview.datamodel.PDBEntry();
++                                      entry.setId(pdbid.getId());
++                                      if (pdbid.getType() != null)
++                                          {
++                                              if (PDBEntry.Type.getType(pdbid.getType()) != null)
++                                                  {
++                                                      entry.setType(PDBEntry.Type.getType(pdbid.getType()));
++                                                  }
++                                              else
++                                                  {
++                                                      entry.setType(PDBEntry.Type.FILE);
++                                                  }
++                                          }
++                                      // jprovider is null when executing 'New View'
++                                      if (pdbid.getFile() != null && jprovider != null)
++                                          {
++                                              if (!pdbloaded.containsKey(pdbid.getFile()))
++                                                  {
++                                                      entry.setFile(loadPDBFile(jprovider, pdbid.getId(),
++                                                                                pdbid.getFile()));
++                                                  }
++                                              else
++                                                  {
++                                                      entry.setFile(pdbloaded.get(pdbid.getId()).toString());
++                                                  }
++                                          }
++                                      /*
++                                        if (pdbid.getPdbentryItem() != null)
++                                        {
++                                        for (PdbentryItem item : pdbid.getPdbentryItem())
++                                        {
++                                        for (Property pr : item.getProperty())
++                                        {
++                                        entry.setProperty(pr.getName(), pr.getValue());
++                                        }
++                                        }
++                                        }
++                                      */
++                                      for (Property prop : pdbid.getProperty())
++                                          {
++                                              entry.setProperty(prop.getName(), prop.getValue());
++                                          }
++                                      Desktop.getStructureSelectionManager()
++                                          .registerPDBEntry(entry);
++                                      // adds PDBEntry to datasequence's set (since Jalview 2.10)
++                                      if (al.getSequenceAt(i).getDatasetSequence() != null)
++                                          {
++                                              al.getSequenceAt(i).getDatasetSequence().addPDBId(entry);
++                                          }
++                                      else
++                                          {
++                                              al.getSequenceAt(i).addPDBId(entry);
++                                          }
++                                  }
++                          }
++                      /*
++                       * load any HMMER profile
++                       */
++                      // TODO fix this
++
++                      String hmmJarFile = jseqs.get(i).getHmmerProfile();
++                      if (hmmJarFile != null && jprovider != null)
++                          {
++                              loadHmmerProfile(jprovider, hmmJarFile, al.getSequenceAt(i));
++                          }
++                  }
++          } // end !multipleview
++  
++      // ///////////////////////////////
++      // LOAD SEQUENCE MAPPINGS
++
++      if (vamsasSet.getAlcodonFrame().size() > 0)
++          {
++              // TODO Potentially this should only be done once for all views of an
++              // alignment
++              List<AlcodonFrame> alc = vamsasSet.getAlcodonFrame();
++              for (int i = 0; i < alc.size(); i++)
++                  {
++                      AlignedCodonFrame cf = new AlignedCodonFrame();
++                      if (alc.get(i).getAlcodMap().size() > 0)
++                          {
++                              List<AlcodMap> maps = alc.get(i).getAlcodMap();
++                              for (int m = 0; m < maps.size(); m++)
++                                  {
++                                      AlcodMap map = maps.get(m);
++                                      SequenceI dnaseq = seqRefIds.get(map.getDnasq());
++                                      // Load Mapping
++                                      jalview.datamodel.Mapping mapping = null;
++                                      // attach to dna sequence reference.
++                                      if (map.getMapping() != null)
++                                          {
++                                              mapping = addMapping(map.getMapping());
++                                              if (dnaseq != null && mapping.getTo() != null)
++                                                  {
++                                                      cf.addMap(dnaseq, mapping.getTo(), mapping.getMap());
++                                                  }
++                                              else
++                                                  {
++                                                      // defer to later
++                                                      frefedSequence.add(
++                                                                         newAlcodMapRef(map.getDnasq(), cf, mapping));
++                                                  }
++                                          }
++                                  }
++                              al.addCodonFrame(cf);
++                          }
++                  }
++          }
++
++      // ////////////////////////////////
++      // LOAD ANNOTATIONS
++      List<JvAnnotRow> autoAlan = new ArrayList<>();
++
++      /*
++       * store any annotations which forward reference a group's ID
++       */
++      Map<String, List<AlignmentAnnotation>> groupAnnotRefs = new Hashtable<>();
++
++      if (vamsasSet.getAnnotation().size()/*Count()*/ > 0)
++          {
++              List<Annotation> an = vamsasSet.getAnnotation();
++
++              for (int i = 0; i < an.size(); i++)
++                  {
++                      Annotation annotation = an.get(i);
++
++                      /**
++                       * test if annotation is automatically calculated for this view only
++                       */
++                      boolean autoForView = false;
++                      if (annotation.getLabel().equals("Quality")
++                          || annotation.getLabel().equals("Conservation")
++                          || annotation.getLabel().equals("Consensus"))
++                          {
++                              // Kludge for pre 2.5 projects which lacked the autocalculated flag
++                              autoForView = true;
++                              // JAXB has no has() test; schema defaults value to false
++                              // if (!annotation.hasAutoCalculated())
++                              // {
++                              // annotation.setAutoCalculated(true);
++                              // }
++                          }
++                      if (autoForView || annotation.isAutoCalculated())
++                          {
++                              // remove ID - we don't recover annotation from other views for
++                              // view-specific annotation
++                              annotation.setId(null);
++                          }
++
++                      // set visibility for other annotation in this view
++                      String annotationId = annotation.getId();
++                      if (annotationId != null && annotationIds.containsKey(annotationId))
++                          {
++                              AlignmentAnnotation jda = annotationIds.get(annotationId);
++                              // in principle Visible should always be true for annotation displayed
++                              // in multiple views
++                              if (annotation.isVisible() != null)
++                                  {
++                                      jda.visible = annotation.isVisible();
++                                  }
++
++                              al.addAnnotation(jda);
++
++                              continue;
++                          }
++                      // Construct new annotation from model.
++                      List<AnnotationElement> ae = annotation.getAnnotationElement();
++                      jalview.datamodel.Annotation[] anot = null;
++                      java.awt.Color firstColour = null;
++                      int anpos;
++                      if (!annotation.isScoreOnly())
++                          {
++                              anot = new jalview.datamodel.Annotation[al.getWidth()];
++                              for (int aa = 0; aa < ae.size() && aa < anot.length; aa++)
++                                  {
++                                      AnnotationElement annElement = ae.get(aa);
++                                      anpos = annElement.getPosition();
++
++                                      if (anpos >= anot.length)
++                                          {
++                                              continue;
++                                          }
++
++                                      float value = safeFloat(annElement.getValue());
++                                      anot[anpos] = new jalview.datamodel.Annotation(
++                                                                                     annElement.getDisplayCharacter(),
++                                                                                     annElement.getDescription(),
++                                                                                     (annElement.getSecondaryStructure() == null
++                                                                                      || annElement.getSecondaryStructure()
++                                                                                      .length() == 0)
++                                                                                     ? ' '
++                                                                                     : annElement
++                                                                                     .getSecondaryStructure()
++                                                                                     .charAt(0),
++                                                                                     value);
++                                      anot[anpos].colour = new Color(safeInt(annElement.getColour()));
++                                      if (firstColour == null)
++                                          {
++                                              firstColour = anot[anpos].colour;
++                                          }
++                                  }
++                          }
++                      // create the new AlignmentAnnotation
++                      jalview.datamodel.AlignmentAnnotation jaa = null;
++
++                      if (annotation.isGraph())
++                          {
++                              float llim = 0, hlim = 0;
++                              // if (autoForView || an[i].isAutoCalculated()) {
++                              // hlim=11f;
++                              // }
++                              jaa = new jalview.datamodel.AlignmentAnnotation(
++                                                                              annotation.getLabel(), annotation.getDescription(), anot,
++                                                                              llim, hlim, safeInt(annotation.getGraphType()));
++
++                              jaa.graphGroup = safeInt(annotation.getGraphGroup());
++                              jaa._linecolour = firstColour;
++                              if (annotation.getThresholdLine() != null)
++                                  {
++                                      jaa.setThreshold(new jalview.datamodel.GraphLine(
++                                                                                       safeFloat(annotation.getThresholdLine().getValue()),
++                                                                                       annotation.getThresholdLine().getLabel(),
++                                                                                       new java.awt.Color(safeInt(
++                                                                                                                  annotation.getThresholdLine().getColour()))));
++                                  }
++                              if (autoForView || annotation.isAutoCalculated())
++                                  {
++                                      // Hardwire the symbol display line to ensure that labels for
++                                      // histograms are displayed
++                                      jaa.hasText = true;
++                                  }
++                          }
++                      else
++                          {
++                              jaa = new jalview.datamodel.AlignmentAnnotation(
++                                                                              annotation.getLabel(), annotation.getDescription(), anot);
++                              jaa._linecolour = firstColour;
++                          }
++                      // register new annotation
++                      // Annotation graphs such as Conservation will not have id.
++                      if (annotation.getId() != null)
++                          {
++                              annotationIds.put(annotation.getId(), jaa);
++                              jaa.annotationId = annotation.getId();
++                          }
++                      // recover sequence association
++                      String sequenceRef = annotation.getSequenceRef();
++                      if (sequenceRef != null)
++                          {
++                              // from 2.9 sequenceRef is to sequence id (JAL-1781)
++                              SequenceI sequence = seqRefIds.get(sequenceRef);
++                              if (sequence == null)
++                                  {
++                                      // in pre-2.9 projects sequence ref is to sequence name
++                                      sequence = al.findName(sequenceRef);
++                                  }
++                              if (sequence != null)
++                                  {
++                                      jaa.createSequenceMapping(sequence, 1, true);
++                                      sequence.addAlignmentAnnotation(jaa);
++                                  }
++                          }
++                      // and make a note of any group association
++                      if (annotation.getGroupRef() != null
++                          && annotation.getGroupRef().length() > 0)
++                          {
++                              List<jalview.datamodel.AlignmentAnnotation> aal = groupAnnotRefs
++                                  .get(annotation.getGroupRef());
++                              if (aal == null)
++                                  {
++                                      aal = new ArrayList<>();
++                                      groupAnnotRefs.put(annotation.getGroupRef(), aal);
++                                  }
++                              aal.add(jaa);
++                          }
++
++                      if (annotation.getScore() != null)
++                          {
++                              jaa.setScore(annotation.getScore().doubleValue());
++                          }
++                      if (annotation.isVisible() != null)
++                          {
++                              jaa.visible = annotation.isVisible().booleanValue();
++                          }
++
++                      if (annotation.isCentreColLabels() != null)
++                          {
++                              jaa.centreColLabels = annotation.isCentreColLabels()
++                                  .booleanValue();
++                          }
++
++                      if (annotation.isScaleColLabels() != null)
++                          {
++                              jaa.scaleColLabel = annotation.isScaleColLabels().booleanValue();
++                          }
++                      if (annotation.isAutoCalculated())
++                          {
++                              // newer files have an 'autoCalculated' flag and store calculation
++                              // state in viewport properties
++                              jaa.autoCalculated = true; // means annotation will be marked for
++                              // update at end of load.
++                          }
++                      if (annotation.getGraphHeight() != null)
++                          {
++                              jaa.graphHeight = annotation.getGraphHeight().intValue();
++                          }
++                      jaa.belowAlignment = annotation.isBelowAlignment();
++                      jaa.setCalcId(annotation.getCalcId());
++                      if (annotation.getProperty().size() > 0)
++                          {
++                              for (Annotation.Property prop : annotation
++                                       .getProperty())
++                                  {
++                                      jaa.setProperty(prop.getName(), prop.getValue());
++                                  }
++                          }
++                      if (jaa.autoCalculated)
++                          {
++                              autoAlan.add(new JvAnnotRow(i, jaa));
++                          }
++                      else
++                          // if (!autoForView)
++                          {
++                              // add autocalculated group annotation and any user created annotation
++                              // for the view
++                              al.addAnnotation(jaa);
++                          }
++                  }
++          }
++      // ///////////////////////
++      // LOAD GROUPS
++      // Create alignment markup and styles for this view
++      if (jalviewModel.getJGroup().size() > 0)
++          {
++              List<JGroup> groups = jalviewModel.getJGroup();
++              boolean addAnnotSchemeGroup = false;
++              for (int i = 0; i < groups.size(); i++)
++                  {
++                      JGroup jGroup = groups.get(i);
++                      ColourSchemeI cs = null;
++                      if (jGroup.getColour() != null)
++                          {
++                              if (jGroup.getColour().startsWith("ucs"))
++                                  {
++                                      cs = getUserColourScheme(jalviewModel, jGroup.getColour());
++                                  }
++                              else if (jGroup.getColour().equals("AnnotationColourGradient")
++                                       && jGroup.getAnnotationColours() != null)
++                                  {
++                                      addAnnotSchemeGroup = true;
++                                  }
++                              else
++                                  {
++                                      cs = ColourSchemeProperty.getColourScheme(null, al,
++                                                                                jGroup.getColour());
++                                  }
++                          }
++                      int pidThreshold = safeInt(jGroup.getPidThreshold());
++
++                      Vector<SequenceI> seqs = new Vector<>();
++
++                      for (int s = 0; s < jGroup.getSeq().size(); s++)
++                          {
++                              String seqId = jGroup.getSeq().get(s);
++                              SequenceI ts = seqRefIds.get(seqId);
++
++                              if (ts != null)
++                                  {
++                                      seqs.addElement(ts);
++                                  }
++                          }
++
++                      if (seqs.size() < 1)
++                          {
++                              continue;
++                          }
++
++                      SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
++                                                           safeBoolean(jGroup.isDisplayBoxes()),
++                                                           safeBoolean(jGroup.isDisplayText()),
++                                                           safeBoolean(jGroup.isColourText()),
++                                                           safeInt(jGroup.getStart()), safeInt(jGroup.getEnd()));
++                      sg.getGroupColourScheme().setThreshold(pidThreshold, true);
++                      sg.getGroupColourScheme()
++                          .setConservationInc(safeInt(jGroup.getConsThreshold()));
++                      sg.setOutlineColour(new Color(safeInt(jGroup.getOutlineColour())));
++
++                      sg.textColour = new Color(safeInt(jGroup.getTextCol1()));
++                      sg.textColour2 = new Color(safeInt(jGroup.getTextCol2()));
++                      sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
++                      sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
++                      // attributes with a default in the schema are never null
++                      sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
++                      sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
++                      sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
++                      sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
++                      if (jGroup.getConsThreshold() != null
++                          && jGroup.getConsThreshold().intValue() != 0)
++                          {
++                              Conservation c = new Conservation("All", sg.getSequences(null), 0,
++                                                                sg.getWidth() - 1);
++                              c.calculate();
++                              c.verdict(false, 25);
++                              sg.cs.setConservation(c);
++                          }
++
++                      if (jGroup.getId() != null && groupAnnotRefs.size() > 0)
++                          {
++                              // re-instate unique group/annotation row reference
++                              List<AlignmentAnnotation> jaal = groupAnnotRefs
++                                  .get(jGroup.getId());
++                              if (jaal != null)
++                                  {
++                                      for (AlignmentAnnotation jaa : jaal)
++                                          {
++                                              jaa.groupRef = sg;
++                                              if (jaa.autoCalculated)
++                                                  {
++                                                      // match up and try to set group autocalc alignment row for this
++                                                      // annotation
++                                                      if (jaa.label.startsWith("Consensus for "))
++                                                          {
++                                                              sg.setConsensus(jaa);
++                                                          }
++                                                      // match up and try to set group autocalc alignment row for this
++                                                      // annotation
++                                                      if (jaa.label.startsWith("Conservation for "))
++                                                          {
++                                                              sg.setConservationRow(jaa);
++                                                          }
++                                                  }
++                                          }
++                                  }
++                          }
++                      al.addGroup(sg);
++                      if (addAnnotSchemeGroup)
++                          {
++                              // reconstruct the annotation colourscheme
++                              sg.setColourScheme(
++                                                 constructAnnotationColour(jGroup.getAnnotationColours(),
++                                                                           null, al, jalviewModel, false));
++                          }
++                  }
++          }
++      if (view == null)
++          {
++              // only dataset in this model, so just return.
++              return null;
++          }
++      // ///////////////////////////////
++      // LOAD VIEWPORT
++
++      // now check to see if we really need to create a new viewport.
++      if (multipleView && viewportsAdded.size() == 0)
++          {
++              // We recovered an alignment for which a viewport already exists.
++              // TODO: fix up any settings necessary for overlaying stored state onto
++              // state recovered from another document. (may not be necessary).
++              // we may need a binding from a viewport in memory to one recovered from
++              // XML.
++              // and then recover its containing af to allow the settings to be applied.
++              // TODO: fix for vamsas demo
++              System.err.println(
++                                 "About to recover a viewport for existing alignment: Sequence set ID is "
++                                 + uniqueSeqSetId);
++              Object seqsetobj = retrieveExistingObj(uniqueSeqSetId);
++              if (seqsetobj != null)
++                  {
++                      if (seqsetobj instanceof String)
++                          {
++                              uniqueSeqSetId = (String) seqsetobj;
++                              System.err.println(
++                                                 "Recovered extant sequence set ID mapping for ID : New Sequence set ID is "
++                                                 + uniqueSeqSetId);
++                          }
++                      else
++                          {
++                              System.err.println(
++                                                 "Warning : Collision between sequence set ID string and existing jalview object mapping.");
++                          }
++
++                  }
++          }
++      /**
++       * indicate that annotation colours are applied across all groups (pre
++       * Jalview 2.8.1 behaviour)
++       */
++      boolean doGroupAnnColour = Jalview2XML.isVersionStringLaterThan("2.8.1",
++                                                                      jalviewModel.getVersion());
++
++      AlignFrame af = null;
++      AlignmentPanel ap = null;
++      AlignViewport av = null;
++      if (viewId != null)
++          {
++              // Check to see if this alignment already has a view id == viewId
++              jalview.gui.AlignmentPanel views[] = Desktop
++                  .getAlignmentPanels(uniqueSeqSetId);
++              if (views != null && views.length > 0)
++                  {
++                      for (int v = 0; v < views.length; v++)
++                          {
++                              ap = views[v];
++                              av = ap.av;
++                              if (av.getViewId().equalsIgnoreCase(viewId))
++                                  {
++                                      // recover the existing alignpanel, alignframe, viewport
++                                      af = ap.alignFrame;
++                                      break;
++                                      // TODO: could even skip resetting view settings if we don't want to
++                                      // change the local settings from other jalview processes
++                                  }
++                          }
++                  }
++          }
++
++      if (af == null)
++          {
++              af = loadViewport(fileName, file, jseqs, hiddenSeqs, al, jalviewModel, view,
++                                uniqueSeqSetId, viewId, autoAlan);
++              av = af.getViewport();
++              // note that this only retrieves the most recently accessed
++              // tab of an AlignFrame.
++              ap = af.alignPanel;
++          }
++
++      /*
++       * Load any trees, PDB structures and viewers
++       * 
++       * Not done if flag is false (when this method is used for New View)
++       */
++      final AlignFrame af0 = af;
++      final AlignViewport av0 = av;
++      final AlignmentPanel ap0 = ap;
++      //    Platform.timeCheck("Jalview2XML.loadFromObject-beforetree",
++      //            Platform.TIME_MARK);
++      if (loadTreesAndStructures)
++          {
++              if (!jalviewModel.getTree().isEmpty())
++                  {
++                      SwingUtilities.invokeLater(new Runnable()
++                          {
++                              @Override
++                              public void run()
++                              {
++                                  //            Platform.timeCheck(null, Platform.TIME_MARK);
++                                  loadTrees(jalviewModel, view, af0, av0, ap0);
++                                  //            Platform.timeCheck("Jalview2XML.loadTrees", Platform.TIME_MARK);
++                              }
++                          });
++                  }
++              if (!jalviewModel.getPcaViewer().isEmpty())
++                  {
++                      SwingUtilities.invokeLater(new Runnable()
++                          {
++                              @Override
++                              public void run()
++                              {
++                                  //            Platform.timeCheck(null, Platform.TIME_MARK);
++                                  loadPCAViewers(jalviewModel, ap0);
++                                  //            Platform.timeCheck("Jalview2XML.loadPCA", Platform.TIME_MARK);
++                              }
++                          });
++                  }
++              SwingUtilities.invokeLater(new Runnable()
++                  {
++                      @Override
++                      public void run()
++                      {
++                          //          Platform.timeCheck(null, Platform.TIME_MARK);
++                          loadPDBStructures(jprovider, jseqs, af0, ap0);
++                          //          Platform.timeCheck("Jalview2XML.loadPDB", Platform.TIME_MARK);
++                      }
++                  });
++              SwingUtilities.invokeLater(new Runnable()
++                  {
++                      @Override
++                      public void run()
++                      {
++                          loadRnaViewers(jprovider, jseqs, ap0);
++                      }
++                  });
++          }
++      // and finally return.
++      // but do not set holdRepaint true just yet, because this could be the
++      // initial frame with just its dataset.
++      return af;
      }
-   }
  
-   /**
-    * Each AlignFrame has a single data set associated with it. Note that none of
-    * these frames are split frames, because Desktop.getAlignFrames() collects
-    * top and bottom separately here.
-    * 
-    * @param dsses
-    * @param fileName
-    * @param jout
-    */
-   private void writeDatasetFor(Hashtable<String, AlignFrame> dsses,
-           String fileName, JarOutputStream jout)
-   {
-     // Note that in saveAllFrames we have associated each specific dataset to
-     // ONE of its associated frames.
-     for (String dssids : dsses.keySet())
-     {
-       AlignFrame _af = dsses.get(dssids);
-       String jfileName = fileName + " Dataset for " + _af.getTitle();
-       if (!jfileName.endsWith(".xml"))
-       {
-         jfileName = jfileName + ".xml";
-       }
-       saveState(_af.alignPanel, jfileName, true, jout, null);
 -    String type = stateData.getType();
 -    try
 -    {
 -      ViewerType viewerType = ViewerType.valueOf(type);
 -      createStructureViewer(viewerType, viewerData, af, jprovider);
 -    } catch (IllegalArgumentException | NullPointerException e)
 -    {
 -      // TODO JAL-3619 show error dialog / offer an alternative viewer
 -      Console.error("Invalid structure viewer type: " + type);
++    /**
++     * Loads a HMMER profile from a file stored in the project, and associates it
++     * with the specified sequence
++     * 
++     * @param jprovider
++     * @param hmmJarFile
++     * @param seq
++     */
++    protected void loadHmmerProfile(jarInputStreamProvider jprovider,
++                                  String hmmJarFile, SequenceI seq)
++    {
++      try
++          {
++              String hmmFile = copyJarEntry(jprovider, hmmJarFile, "hmm", null);
++              HMMFile parser = new HMMFile(hmmFile, DataSourceType.FILE);
++              HiddenMarkovModel hmmModel = parser.getHMM();
++              hmmModel = new HiddenMarkovModel(hmmModel, seq);
++              seq.setHMM(hmmModel);
++          } catch (IOException e)
++          {
++              Console.warn("Error loading HMM profile for " + seq.getName() + ": "
++                           + e.getMessage());
++          }
      }
--  }
--
--  /**
-    * create a JalviewModel from an alignment view and marshall it to a
-    * JarOutputStream
-    * 
-    * @param ap
-    *          panel to create jalview model for
-    * @param fileName
-    *          name of alignment panel written to output stream
-    * @param jout
-    *          jar output stream
-    * @param viewIds
-    * @param out
-    *          jar entry name
-    */
-   protected JalviewModel saveState(AlignmentPanel ap, String fileName,
-           JarOutputStream jout, List<String> viewIds)
-   {
-     return saveState(ap, fileName, false, jout, viewIds);
-   }
 -   * Generates a name for the entry in the project jar file to hold state
 -   * information for a structure viewer
 -   * 
 -   * @param viewId
 -   * @return
 -   */
 -  protected String getViewerJarEntryName(String viewId)
 -  {
 -    return VIEWER_PREFIX + viewId;
 -  }
  
--  /**
-    * create a JalviewModel from an alignment view and marshall it to a
-    * JarOutputStream
-    * 
-    * @param ap
-    *          panel to create jalview model for
-    * @param fileName
-    *          name of alignment panel written to output stream
-    * @param storeDS
-    *          when true, only write the dataset for the alignment, not the data
-    *          associated with the view.
-    * @param jout
-    *          jar output stream
-    * @param out
-    *          jar entry name
-    */
-   protected JalviewModel saveState(AlignmentPanel ap, String fileName,
-           boolean storeDS, JarOutputStream jout, List<String> viewIds)
-   {
-     if (viewIds == null)
-     {
-       viewIds = new ArrayList<>();
 -   * Returns any open frame that matches given structure viewer data. The match
 -   * is based on the unique viewId, or (for older project versions) the frame's
 -   * geometry.
 -   * 
 -   * @param viewerData
 -   * @return
 -   */
 -  protected StructureViewerBase findMatchingViewer(
 -          Entry<String, StructureViewerModel> viewerData)
 -  {
 -    final String sviewid = viewerData.getKey();
 -    final StructureViewerModel svattrib = viewerData.getValue();
 -    StructureViewerBase comp = null;
 -    JInternalFrame[] frames = getAllFrames();
 -    for (JInternalFrame frame : frames)
 -    {
 -      if (frame instanceof StructureViewerBase)
 -      {
 -        /*
 -         * Post jalview 2.4 schema includes structure view id
 -         */
 -        if (sviewid != null && ((StructureViewerBase) frame).getViewId()
 -                .equals(sviewid))
 -        {
 -          comp = (StructureViewerBase) frame;
 -          break; // break added in 2.9
 -        }
 -        /*
 -         * Otherwise test for matching position and size of viewer frame
 -         */
 -        else if (frame.getX() == svattrib.getX()
 -                && frame.getY() == svattrib.getY()
 -                && frame.getHeight() == svattrib.getHeight()
 -                && frame.getWidth() == svattrib.getWidth())
 -        {
 -          comp = (StructureViewerBase) frame;
 -          // no break in faint hope of an exact match on viewId
 -        }
 -      }
++    /**
++     * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
++     * panel is restored from separate jar entries, two (gapped and trimmed) per
++     * sequence and secondary structure.
++     * 
++     * Currently each viewer shows just one sequence and structure (gapped and
++     * trimmed), however this method is designed to support multiple sequences or
++     * structures in viewers if wanted in future.
++     * 
++     * @param jprovider
++     * @param jseqs
++     * @param ap
++     */
++    protected void loadRnaViewers(jarInputStreamProvider jprovider,
++                                List<JSeq> jseqs, AlignmentPanel ap)
++    {
++      /*
++       * scan the sequences for references to viewers; create each one the first
++       * time it is referenced, add Rna models to existing viewers
++       */
++      for (JSeq jseq : jseqs)
++          {
++              for (int i = 0; i < jseq.getRnaViewer().size(); i++)
++                  {
++                      RnaViewer viewer = jseq.getRnaViewer().get(i);
++                      AppVarna appVarna = findOrCreateVarnaViewer(viewer, uniqueSetSuffix,
++                                                                  ap);
++
++                      for (int j = 0; j < viewer.getSecondaryStructure().size(); j++)
++                          {
++                              SecondaryStructure ss = viewer.getSecondaryStructure().get(j);
++                              SequenceI seq = seqRefIds.get(jseq.getId());
++                              AlignmentAnnotation ann = this.annotationIds
++                                  .get(ss.getAnnotationId());
++
++                              /*
++                               * add the structure to the Varna display (with session state copied
++                               * from the jar to a temporary file)
++                               */
++                              boolean gapped = safeBoolean(ss.isGapped());
++                              String rnaTitle = ss.getTitle();
++                              String sessionState = ss.getViewerState();
++                              String tempStateFile = copyJarEntry(jprovider, sessionState,
++                                                                  "varna", null);
++                              RnaModel rna = new RnaModel(rnaTitle, ann, seq, null, gapped);
++                              appVarna.addModelSession(rna, rnaTitle, tempStateFile);
++                          }
++                      appVarna.setInitialSelection(safeInt(viewer.getSelectedRna()));
++                  }
++          }
      }
 -    return comp;
 -  }
  
-     initSeqRefs();
-     List<UserColourScheme> userColours = new ArrayList<>();
-     AlignViewport av = ap.av;
-     ViewportRanges vpRanges = av.getRanges();
-     final ObjectFactory objectFactory = new ObjectFactory();
-     JalviewModel object = objectFactory.createJalviewModel();
-     object.setVamsasModel(new VAMSAS());
-     // object.setCreationDate(new java.util.Date(System.currentTimeMillis()));
-     try
-     {
-       GregorianCalendar c = new GregorianCalendar();
-       DatatypeFactory datatypeFactory = DatatypeFactory.newInstance();
-       XMLGregorianCalendar now = datatypeFactory.newXMLGregorianCalendar(c);// gregorianCalendar);
-       object.setCreationDate(now);
-     } catch (DatatypeConfigurationException e)
-     {
-       System.err.println("error writing date: " + e.toString());
 -  /**
 -   * Link an AlignmentPanel to an existing structure viewer.
 -   * 
 -   * @param ap
 -   * @param viewer
 -   * @param oldFiles
 -   * @param useinViewerSuperpos
 -   * @param usetoColourbyseq
 -   * @param viewerColouring
 -   */
 -  protected void linkStructureViewer(AlignmentPanel ap,
 -          StructureViewerBase viewer, StructureViewerModel stateData)
 -  {
 -    // NOTE: if the jalview project is part of a shared session then
 -    // view synchronization should/could be done here.
++    /**
++     * Locate and return an already instantiated matching AppVarna, or create one
++     * if not found
++     * 
++     * @param viewer
++     * @param viewIdSuffix
++     * @param ap
++     * @return
++     */
++    protected AppVarna findOrCreateVarnaViewer(RnaViewer viewer,
++                                             String viewIdSuffix, AlignmentPanel ap)
++    {
++      /*
++       * on each load a suffix is appended to the saved viewId, to avoid conflicts
++       * if load is repeated
++       */
++      String postLoadId = viewer.getViewId() + viewIdSuffix;
++      for (JInternalFrame frame : getAllFrames())
++          {
++              if (frame instanceof AppVarna)
++                  {
++                      AppVarna varna = (AppVarna) frame;
++                      if (postLoadId.equals(varna.getViewId()))
++                          {
++                              // this viewer is already instantiated
++                              // could in future here add ap as another 'parent' of the
++                              // AppVarna window; currently just 1-to-many
++                              return varna;
++                          }
++                  }
++          }
++  
++      /*
++       * viewer not found - make it
++       */
++      RnaViewerModel model = new RnaViewerModel(postLoadId, viewer.getTitle(),
++                                                safeInt(viewer.getXpos()), safeInt(viewer.getYpos()),
++                                                safeInt(viewer.getWidth()), safeInt(viewer.getHeight()),
++                                                safeInt(viewer.getDividerLocation()));
++      AppVarna varna = new AppVarna(model, ap);
++
++      return varna;
 +    }
-     object.setVersion(
-             jalview.bin.Cache.getDefault("VERSION", "Development Build"));
  
 -    final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
 -    final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
 -    final boolean viewerColouring = stateData.isColourByViewer();
 -    Map<File, StructureData> oldFiles = stateData.getFileData();
 +    /**
-      * rjal is full height alignment, jal is actual alignment with full metadata
-      * but excludes hidden sequences.
++     * Load any saved trees
++     * 
++     * @param jm
++     * @param view
++     * @param af
++     * @param av
++     * @param ap
 +     */
-     jalview.datamodel.AlignmentI rjal = av.getAlignment(), jal = rjal;
-     if (av.hasHiddenRows())
-     {
-       rjal = jal.getHiddenSequences().getFullAlignment();
++    protected void loadTrees(JalviewModel jm, Viewport view,
++                           AlignFrame af, AlignViewport av, AlignmentPanel ap)
++    {
++      // TODO result of automated refactoring - are all these parameters needed?
++      try
++          {
++              for (int t = 0; t < jm.getTree().size(); t++)
++                  {
++
++                      Tree tree = jm.getTree().get(t);
++
++                      TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
++                      if (tp == null)
++                          {
++                              tp = af.showNewickTree(new NewickFile(tree.getNewick()),
++                                                     tree.getTitle(), safeInt(tree.getWidth()),
++                                                     safeInt(tree.getHeight()), safeInt(tree.getXpos()),
++                                                     safeInt(tree.getYpos()));
++                              if (tp == null)
++                                  {
++                                      Console.warn("There was a problem recovering stored Newick tree: \n"
++                                                   + tree.getNewick());
++                                      continue;
++                                  }
++                              if (tree.getId() != null)
++                                  {
++                                      // perhaps bind the tree id to something ?
++                                  }
++                          }
++                      else
++                          {
++                              // update local tree attributes ?
++                              // TODO: should check if tp has been manipulated by user - if so its
++                              // settings shouldn't be modified
++                              tp.setTitle(tree.getTitle());
++                              tp.setBounds(new Rectangle(safeInt(tree.getXpos()),
++                                                         safeInt(tree.getYpos()), safeInt(tree.getWidth()),
++                                                         safeInt(tree.getHeight())));
++                              tp.setViewport(av); // af.viewport;
++                              // TODO: verify 'associate with all views' works still
++                              tp.getTreeCanvas().setViewport(av); // af.viewport;
++                              tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
++                          }
++                      tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
++
++                      tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
++                      tp.fitToWindow_actionPerformed(null);
++
++                      if (tree.getFontName() != null)
++                          {
++                              tp.setTreeFont(
++                                             new Font(tree.getFontName(), safeInt(tree.getFontStyle()),
++                                                      safeInt(tree.getFontSize())));
++                          }
++                      else
++                          {
++                              tp.setTreeFont(
++                                             new Font(view.getFontName(), safeInt(view.getFontStyle()),
++                                                      safeInt(view.getFontSize())));
++                          }
++
++                      tp.showPlaceholders(safeBoolean(tree.isMarkUnlinked()));
++                      tp.showBootstrap(safeBoolean(tree.isShowBootstrap()));
++                      tp.showDistances(safeBoolean(tree.isShowDistances()));
++
++                      tp.getTreeCanvas().setThreshold(safeFloat(tree.getThreshold()));
++
++                      if (safeBoolean(tree.isCurrentTree()))
++                          {
++                              af.getViewport().setCurrentTree(tp.getTree());
++                          }
++                  }
++
++          } catch (Exception ex)
++          {
++              ex.printStackTrace();
++          }
 +    }
  
-     SequenceSet vamsasSet = new SequenceSet();
-     Sequence vamsasSeq;
-     // JalviewModelSequence jms = new JalviewModelSequence();
-     vamsasSet.setGapChar(jal.getGapCharacter() + "");
-     if (jal.getDataset() != null)
-     {
-       // dataset id is the dataset's hashcode
-       vamsasSet.setDatasetId(getDatasetIdRef(jal.getDataset()));
-       if (storeDS)
-       {
-         // switch jal and the dataset
-         jal = jal.getDataset();
-         rjal = jal;
-       }
-     }
-     if (jal.getProperties() != null)
-     {
-       Enumeration en = jal.getProperties().keys();
-       while (en.hasMoreElements())
-       {
-         String key = en.nextElement().toString();
-         SequenceSetProperties ssp = new SequenceSetProperties();
-         ssp.setKey(key);
-         ssp.setValue(jal.getProperties().get(key).toString());
-         // vamsasSet.addSequenceSetProperties(ssp);
-         vamsasSet.getSequenceSetProperties().add(ssp);
-       }
 -    /*
 -     * Add mapping for sequences in this view to an already open viewer
++    /**
++     * Load and link any saved structure viewers.
++     * 
++     * @param jprovider
++     * @param jseqs
++     * @param af
++     * @param ap
+      */
 -    final AAStructureBindingModel binding = viewer.getBinding();
 -    for (File id : oldFiles.keySet())
 -    {
 -      // add this and any other pdb files that should be present in the
 -      // viewer
 -      StructureData filedat = oldFiles.get(id);
 -      String pdbFile = filedat.getFilePath();
 -      SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
 -      binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
 -              null);
 -      binding.addSequenceForStructFile(pdbFile, seq);
++    protected void loadPDBStructures(jarInputStreamProvider jprovider,
++                                   List<JSeq> jseqs, AlignFrame af, AlignmentPanel ap)
++    {
++      /*
++       * Run through all PDB ids on the alignment, and collect mappings between
++       * distinct view ids and all sequences referring to that view.
++       */
++      Map<String, StructureViewerModel> structureViewers = new LinkedHashMap<>();
++
++      for (int i = 0; i < jseqs.size(); i++)
++          {
++              JSeq jseq = jseqs.get(i);
++              if (jseq.getPdbids().size() > 0)
++                  {
++                      List<Pdbids> ids = jseq.getPdbids();
++                      for (int p = 0; p < ids.size(); p++)
++                          {
++                              Pdbids pdbid = ids.get(p);
++                              final int structureStateCount = pdbid.getStructureState().size();
++                              for (int s = 0; s < structureStateCount; s++)
++                                  {
++                                      // check to see if we haven't already created this structure view
++                                      final StructureState structureState = pdbid
++                                          .getStructureState().get(s);
++                                      String sviewid = (structureState.getViewId() == null) ? null
++                                          : structureState.getViewId() + uniqueSetSuffix;
++                                      jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
++                                      // Originally : pdbid.getFile()
++                                      // : TODO: verify external PDB file recovery still works in normal
++                                      // jalview project load
++                                      jpdb.setFile(
++                                                   loadPDBFile(jprovider, pdbid.getId(), pdbid.getFile()));
++                                      jpdb.setId(pdbid.getId());
++
++                                      int x = safeInt(structureState.getXpos());
++                                      int y = safeInt(structureState.getYpos());
++                                      int width = safeInt(structureState.getWidth());
++                                      int height = safeInt(structureState.getHeight());
++
++                                      // Probably don't need to do this anymore...
++                                      // Desktop.getDesktop().getComponentAt(x, y);
++                                      // TODO: NOW: check that this recovers the PDB file correctly.
++                                      String pdbFile = loadPDBFile(jprovider, pdbid.getId(),
++                                                                   pdbid.getFile());
++                                      jalview.datamodel.SequenceI seq = seqRefIds
++                                          .get(jseq.getId() + "");
++                                      if (sviewid == null)
++                                          {
++                                              sviewid = "_jalview_pre2_4_" + x + "," + y + "," + width + ","
++                                                  + height;
++                                          }
++                                      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(),
++                                                                                            viewerType));
++                                              // Legacy pre-2.7 conversion JAL-823 :
++                                              // do not assume any view has to be linked for colour by
++                                              // sequence
++                                          }
++
++                                      // assemble String[] { pdb files }, String[] { id for each
++                                      // file }, orig_fileloc, SequenceI[][] {{ seqs_file 1 }, {
++                                      // seqs_file 2}, boolean[] {
++                                      // linkAlignPanel,superposeWithAlignpanel}} from hash
++                                      StructureViewerModel jmoldat = structureViewers.get(sviewid);
++                                      jmoldat.setAlignWithPanel(jmoldat.isAlignWithPanel()
++                                                                || structureState.isAlignwithAlignPanel());
++
++                                      /*
++                                       * Default colour by linked panel to false if not specified (e.g.
++                                       * for pre-2.7 projects)
++                                       */
++                                      boolean colourWithAlignPanel = jmoldat.isColourWithAlignPanel();
++                                      colourWithAlignPanel |= structureState.isColourwithAlignPanel();
++                                      jmoldat.setColourWithAlignPanel(colourWithAlignPanel);
++
++                                      /*
++                                       * Default colour by viewer to true if not specified (e.g. for
++                                       * pre-2.7 projects)
++                                       */
++                                      boolean colourByViewer = jmoldat.isColourByViewer();
++                                      colourByViewer &= structureState.isColourByJmol();
++                                      jmoldat.setColourByViewer(colourByViewer);
++
++                                      if (jmoldat.getStateData().length() < structureState
++                                          .getValue()/*Content()*/.length())
++                                          {
++                                              jmoldat.setStateData(structureState.getValue());// Content());
++                                          }
++                                      if (pdbid.getFile() != null)
++                                          {
++                                              File mapkey = new File(pdbid.getFile());
++                                              StructureData seqstrmaps = jmoldat.getFileData().get(mapkey);
++                                              if (seqstrmaps == null)
++                                                  {
++                                                      jmoldat.getFileData().put(mapkey,
++                                                                                seqstrmaps = jmoldat.new StructureData(pdbFile,
++                                                                                                                       pdbid.getId()));
++                                                  }
++                                              if (!seqstrmaps.getSeqList().contains(seq))
++                                                  {
++                                                      seqstrmaps.getSeqList().add(seq);
++                                                      // TODO and chains?
++                                                  }
++                                          }
++                                      else
++                                          {
++                                              errorMessage = ("The Jmol views in this project were imported\nfrom an older version of Jalview.\nPlease review the sequence colour associations\nin the Colour by section of the Jmol View menu.\n\nIn the case of problems, see note at\nhttp://issues.jalview.org/browse/JAL-747");
++                                              Console.warn(errorMessage);
++                                          }
++                                  }
++                          }
++                  }
++          }
++      // Instantiate the associated structure views
++      for (Entry<String, StructureViewerModel> entry : structureViewers
++               .entrySet())
++          {
++              try
++                  {
++                      createOrLinkStructureViewer(entry, af, ap, jprovider);
++                  } catch (Exception e)
++                  {
++                      System.err.println(
++                                         "Error loading structure viewer: " + e.getMessage());
++                      // failed - try the next one
++                  }
++          }
      }
 -    // and add the AlignmentPanel's reference to the view panel
 -    viewer.addAlignmentPanel(ap);
 -    if (useinViewerSuperpos)
 -    {
 -      viewer.useAlignmentPanelForSuperposition(ap);
 +
-     JSeq jseq;
-     Set<String> calcIdSet = new HashSet<>();
-     // record the set of vamsas sequence XML POJO we create.
-     HashMap<String, Sequence> vamsasSetIds = new HashMap<>();
-     // SAVE SEQUENCES
-     for (final SequenceI jds : rjal.getSequences())
-     {
-       final SequenceI jdatasq = jds.getDatasetSequence() == null ? jds
-               : jds.getDatasetSequence();
-       String id = seqHash(jds);
-       if (vamsasSetIds.get(id) == null)
-       {
-         if (seqRefIds.get(id) != null && !storeDS)
-         {
-           // This happens for two reasons: 1. multiple views are being
-           // serialised.
-           // 2. the hashCode has collided with another sequence's code. This
-           // DOES
-           // HAPPEN! (PF00072.15.stk does this)
-           // JBPNote: Uncomment to debug writing out of files that do not read
-           // back in due to ArrayOutOfBoundExceptions.
-           // System.err.println("vamsasSeq backref: "+id+"");
-           // System.err.println(jds.getName()+"
-           // "+jds.getStart()+"-"+jds.getEnd()+" "+jds.getSequenceAsString());
-           // System.err.println("Hashcode: "+seqHash(jds));
-           // SequenceI rsq = (SequenceI) seqRefIds.get(id + "");
-           // System.err.println(rsq.getName()+"
-           // "+rsq.getStart()+"-"+rsq.getEnd()+" "+rsq.getSequenceAsString());
-           // System.err.println("Hashcode: "+seqHash(rsq));
-         }
-         else
-         {
-           vamsasSeq = createVamsasSequence(id, jds);
- //          vamsasSet.addSequence(vamsasSeq);
-           vamsasSet.getSequence().add(vamsasSeq);
-           vamsasSetIds.put(id, vamsasSeq);
-           seqRefIds.put(id, jds);
-         }
-       }
-       jseq = new JSeq();
-       jseq.setStart(jds.getStart());
-       jseq.setEnd(jds.getEnd());
-       jseq.setColour(av.getSequenceColour(jds).getRGB());
-       jseq.setId(id); // jseq id should be a string not a number
-       if (!storeDS)
-       {
-         // Store any sequences this sequence represents
-         if (av.hasHiddenRows())
-         {
-           // use rjal, contains the full height alignment
-           jseq.setHidden(
-                   av.getAlignment().getHiddenSequences().isHidden(jds));
-           if (av.isHiddenRepSequence(jds))
-           {
-             jalview.datamodel.SequenceI[] reps = av
-                     .getRepresentedSequences(jds).getSequencesInOrder(rjal);
-             for (int h = 0; h < reps.length; h++)
-             {
-               if (reps[h] != jds)
-               {
-                 // jseq.addHiddenSequences(rjal.findIndex(reps[h]));
-                 jseq.getHiddenSequences().add(rjal.findIndex(reps[h]));
-               }
-             }
-           }
-         }
-         // mark sequence as reference - if it is the reference for this view
-         if (jal.hasSeqrep())
-         {
-           jseq.setViewreference(jds == jal.getSeqrep());
-         }
-       }
-       // TODO: omit sequence features from each alignment view's XML dump if we
-       // are storing dataset
-       List<SequenceFeature> sfs = jds.getSequenceFeatures();
-       for (SequenceFeature sf : sfs)
-       {
-         // Features features = new Features();
-         Feature features = new Feature();
-         features.setBegin(sf.getBegin());
-         features.setEnd(sf.getEnd());
-         features.setDescription(sf.getDescription());
-         features.setType(sf.getType());
-         features.setFeatureGroup(sf.getFeatureGroup());
-         features.setScore(sf.getScore());
-         if (sf.links != null)
-         {
-           for (int l = 0; l < sf.links.size(); l++)
-           {
-             OtherData keyValue = new OtherData();
-             keyValue.setKey("LINK_" + l);
-             keyValue.setValue(sf.links.elementAt(l).toString());
-             // features.addOtherData(keyValue);
-             features.getOtherData().add(keyValue);
-           }
-         }
-         if (sf.otherDetails != null)
-         {
-           /*
-            * save feature attributes, which may be simple strings or
-            * map valued (have sub-attributes)
-            */
-           for (Entry<String, Object> entry : sf.otherDetails.entrySet())
-           {
-             String key = entry.getKey();
-             Object value = entry.getValue();
-             if (value instanceof Map<?, ?>)
-             {
-               for (Entry<String, Object> subAttribute : ((Map<String, Object>) value)
-                       .entrySet())
-               {
-                 OtherData otherData = new OtherData();
-                 otherData.setKey(key);
-                 otherData.setKey2(subAttribute.getKey());
-                 otherData.setValue(subAttribute.getValue().toString());
-                 // features.addOtherData(otherData);
-                 features.getOtherData().add(otherData);
-               }
-             }
-             else
-             {
-               OtherData otherData = new OtherData();
-               otherData.setKey(key);
-               otherData.setValue(value.toString());
-               // features.addOtherData(otherData);
-               features.getOtherData().add(otherData);
-             }
-           }
-         }
-         // jseq.addFeatures(features);
-         jseq.getFeatures().add(features);
-       }
-       /*
-        * save PDB entries for sequence
-        */
-       if (jdatasq.getAllPDBEntries() != null)
-       {
-         Enumeration<PDBEntry> en = jdatasq.getAllPDBEntries().elements();
-         while (en.hasMoreElements())
-         {
-           Pdbids pdb = new Pdbids();
-           jalview.datamodel.PDBEntry entry = en.nextElement();
-           String pdbId = entry.getId();
-           pdb.setId(pdbId);
-           pdb.setType(entry.getType());
-           /*
-            * Store any structure views associated with this sequence. This
-            * section copes with duplicate entries in the project, so a dataset
-            * only view *should* be coped with sensibly.
-            */
-           // This must have been loaded, is it still visible?
-           JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
-           String matchedFile = null;
-           for (int f = frames.length - 1; f > -1; f--)
-           {
-             if (frames[f] instanceof StructureViewerBase)
-             {
-               StructureViewerBase viewFrame = (StructureViewerBase) frames[f];
-               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);
-                 File viewerState = viewFrame.saveSession();
-                 if (viewerState != null)
-                 {
-                   copyFileToJar(jout, viewerState.getPath(),
-                           getViewerJarEntryName(viewId), viewerType);
-                 }
-                 else
-                 {
-                   Cache.log.error(
-                           "Failed to save viewer state for " + viewerType);
-                 }
-               }
-             }
-           }
-           if (matchedFile != null || entry.getFile() != null)
-           {
-             if (entry.getFile() != null)
-             {
-               // use entry's file
-               matchedFile = entry.getFile();
-             }
-             pdb.setFile(matchedFile); // entry.getFile());
-             if (pdbfiles == null)
-             {
-               pdbfiles = new ArrayList<>();
-             }
-             if (!pdbfiles.contains(pdbId))
-             {
-               pdbfiles.add(pdbId);
-               copyFileToJar(jout, matchedFile, pdbId, pdbId);
-             }
-           }
-           Enumeration<String> props = entry.getProperties();
-           if (props.hasMoreElements())
-           {
-             // PdbentryItem item = new PdbentryItem();
-             while (props.hasMoreElements())
-             {
-               Property prop = new Property();
-               String key = props.nextElement();
-               prop.setName(key);
-               prop.setValue(entry.getProperty(key).toString());
-               // item.addProperty(prop);
-               pdb.getProperty().add(prop);
-             }
-             // pdb.addPdbentryItem(item);
-           }
-           // jseq.addPdbids(pdb);
-           jseq.getPdbids().add(pdb);
-         }
-       }
-       saveRnaViewers(jout, jseq, jds, viewIds, ap, storeDS);
-       if (jds.hasHMMProfile())
-       {
-         saveHmmerProfile(jout, jseq, jds);
-       }
-       // jms.addJSeq(jseq);
-       object.getJSeq().add(jseq);
++    /**
++     * 
++     * @param viewerData
++     * @param af
++     * @param ap
++     * @param jprovider
++     */
++    protected void createOrLinkStructureViewer(
++                                             Entry<String, StructureViewerModel> viewerData, AlignFrame af,
++                                             AlignmentPanel ap, jarInputStreamProvider jprovider)
++    {
++      final StructureViewerModel stateData = viewerData.getValue();
++
++      /*
++       * Search for any viewer windows already open from other alignment views
++       * that exactly match the stored structure state
++       */
++      StructureViewerBase comp = findMatchingViewer(viewerData);
++
++      if (comp != null)
++          {
++              linkStructureViewer(ap, comp, stateData);
++              return;
++          }
++
++      String type = stateData.getType();
++      try
++          {
++              ViewerType viewerType = ViewerType.valueOf(type);
++              createStructureViewer(viewerType, viewerData, af, jprovider);
++          } catch (IllegalArgumentException | NullPointerException e)
++          {
++              // TODO JAL-3619 show error dialog / offer an alternative viewer
++              Console.error("Invalid structure viewer type: " + type);
++          }
      }
-     if (!storeDS && av.hasHiddenRows())
-     {
-       jal = av.getAlignment();
-     }
-     // SAVE MAPPINGS
-     // FOR DATASET
-     if (storeDS && jal.getCodonFrames() != null)
-     {
-       List<AlignedCodonFrame> jac = jal.getCodonFrames();
-       for (AlignedCodonFrame acf : jac)
-       {
-         AlcodonFrame alc = new AlcodonFrame();
-         if (acf.getProtMappings() != null
-                 && acf.getProtMappings().length > 0)
-         {
-           boolean hasMap = false;
-           SequenceI[] dnas = acf.getdnaSeqs();
-           jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
-           for (int m = 0; m < pmaps.length; m++)
-           {
-             AlcodMap alcmap = new AlcodMap();
-             alcmap.setDnasq(seqHash(dnas[m]));
-             alcmap.setMapping(
-                     createVamsasMapping(pmaps[m], dnas[m], null, false));
-             // alc.addAlcodMap(alcmap);
-             alc.getAlcodMap().add(alcmap);
-             hasMap = true;
-           }
-           if (hasMap)
-           {
-             // vamsasSet.addAlcodonFrame(alc);
-             vamsasSet.getAlcodonFrame().add(alc);
-           }
-         }
-         // TODO: delete this ? dead code from 2.8.3->2.9 ?
-         // {
-         // AlcodonFrame alc = new AlcodonFrame();
-         // vamsasSet.addAlcodonFrame(alc);
-         // for (int p = 0; p < acf.aaWidth; p++)
-         // {
-         // Alcodon cmap = new Alcodon();
-         // if (acf.codons[p] != null)
-         // {
-         // // Null codons indicate a gapped column in the translated peptide
-         // // alignment.
-         // cmap.setPos1(acf.codons[p][0]);
-         // cmap.setPos2(acf.codons[p][1]);
-         // cmap.setPos3(acf.codons[p][2]);
-         // }
-         // alc.addAlcodon(cmap);
-         // }
-         // if (acf.getProtMappings() != null
-         // && acf.getProtMappings().length > 0)
-         // {
-         // SequenceI[] dnas = acf.getdnaSeqs();
-         // jalview.datamodel.Mapping[] pmaps = acf.getProtMappings();
-         // for (int m = 0; m < pmaps.length; m++)
-         // {
-         // AlcodMap alcmap = new AlcodMap();
-         // alcmap.setDnasq(seqHash(dnas[m]));
-         // alcmap.setMapping(createVamsasMapping(pmaps[m], dnas[m], null,
-         // false));
-         // alc.addAlcodMap(alcmap);
-         // }
-         // }
-       }
-     }
-     // SAVE TREES
-     // /////////////////////////////////
-     if (!storeDS && av.getCurrentTree() != null)
-     {
-       // FIND ANY ASSOCIATED TREES
-       // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
-       if (Desktop.getDesktopPane() != null)
-       {
-         JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
-         for (int t = 0; t < frames.length; t++)
-         {
-           if (frames[t] instanceof TreePanel)
-           {
-             TreePanel tp = (TreePanel) frames[t];
-             if (tp.getTreeCanvas().getViewport().getAlignment() == jal)
-             {
-               JalviewModel.Tree tree = new JalviewModel.Tree();
-               tree.setTitle(tp.getTitle());
-               tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
-               tree.setNewick(tp.getTree().print());
-               tree.setThreshold(tp.getTreeCanvas().getThreshold());
-               tree.setFitToWindow(tp.fitToWindow.getState());
-               tree.setFontName(tp.getTreeFont().getName());
-               tree.setFontSize(tp.getTreeFont().getSize());
-               tree.setFontStyle(tp.getTreeFont().getStyle());
-               tree.setMarkUnlinked(tp.placeholdersMenu.getState());
-               tree.setShowBootstrap(tp.bootstrapMenu.getState());
-               tree.setShowDistances(tp.distanceMenu.getState());
-               tree.setHeight(tp.getHeight());
-               tree.setWidth(tp.getWidth());
-               tree.setXpos(tp.getX());
-               tree.setYpos(tp.getY());
-               tree.setId(makeHashCode(tp, null));
-               tree.setLinkToAllViews(
-                       tp.getTreeCanvas().isApplyToAllViews());
-               // jms.addTree(tree);
-               object.getTree().add(tree);
-             }
-           }
-         }
-       }
-     }
-     /*
-      * save PCA viewers
-      */
-     if (!storeDS && Desktop.getDesktopPane() != null)
-     {
-       for (JInternalFrame frame : Desktop.getDesktopPane().getAllFrames())
-       {
-         if (frame instanceof PCAPanel)
-         {
-           PCAPanel panel = (PCAPanel) frame;
-           if (panel.getAlignViewport().getAlignment() == jal)
-           {
-             savePCA(panel, object);
-           }
-         }
-       }
-     }
-     // SAVE ANNOTATIONS
 -    else
 +    /**
-      * store forward refs from an annotationRow to any groups
-      */
-     IdentityHashMap<SequenceGroup, String> groupRefs = new IdentityHashMap<>();
-     if (storeDS)
-     {
-       for (SequenceI sq : jal.getSequences())
-       {
-         // Store annotation on dataset sequences only
-         AlignmentAnnotation[] aa = sq.getAnnotation();
-         if (aa != null && aa.length > 0)
-         {
-           storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
-                   vamsasSet);
-         }
-       }
-     }
-     else
-     {
-       if (jal.getAlignmentAnnotation() != null)
-       {
-         // Store the annotation shown on the alignment.
-         AlignmentAnnotation[] aa = jal.getAlignmentAnnotation();
-         storeAlignmentAnnotation(aa, groupRefs, av, calcIdSet, storeDS,
-                 vamsasSet);
-       }
-     }
-     // SAVE GROUPS
-     if (jal.getGroups() != null)
-     {
-       JGroup[] groups = new JGroup[jal.getGroups().size()];
-       int i = -1;
-       for (jalview.datamodel.SequenceGroup sg : jal.getGroups())
-       {
-         JGroup jGroup = new JGroup();
-         groups[++i] = jGroup;
-         jGroup.setStart(sg.getStartRes());
-         jGroup.setEnd(sg.getEndRes());
-         jGroup.setName(sg.getName());
-         if (groupRefs.containsKey(sg))
-         {
-           // group has references so set its ID field
-           jGroup.setId(groupRefs.get(sg));
-         }
-         ColourSchemeI colourScheme = sg.getColourScheme();
-         if (colourScheme != null)
-         {
-           ResidueShaderI groupColourScheme = sg.getGroupColourScheme();
-           if (groupColourScheme.conservationApplied())
-           {
-             jGroup.setConsThreshold(groupColourScheme.getConservationInc());
-             if (colourScheme instanceof jalview.schemes.UserColourScheme)
-             {
-               jGroup.setColour(
-                       setUserColourScheme(colourScheme, userColours,
-                               object));
-             }
-             else
-             {
-               jGroup.setColour(colourScheme.getSchemeName());
-             }
-           }
-           else if (colourScheme instanceof jalview.schemes.AnnotationColourGradient)
-           {
-             jGroup.setColour("AnnotationColourGradient");
-             jGroup.setAnnotationColours(constructAnnotationColours(
-                     (jalview.schemes.AnnotationColourGradient) colourScheme,
-                     userColours, object));
-           }
-           else if (colourScheme instanceof jalview.schemes.UserColourScheme)
-           {
-             jGroup.setColour(
-                     setUserColourScheme(colourScheme, userColours, object));
-           }
-           else
-           {
-             jGroup.setColour(colourScheme.getSchemeName());
-           }
-           jGroup.setPidThreshold(groupColourScheme.getThreshold());
-         }
-         jGroup.setOutlineColour(sg.getOutlineColour().getRGB());
-         jGroup.setDisplayBoxes(sg.getDisplayBoxes());
-         jGroup.setDisplayText(sg.getDisplayText());
-         jGroup.setColourText(sg.getColourText());
-         jGroup.setTextCol1(sg.textColour.getRGB());
-         jGroup.setTextCol2(sg.textColour2.getRGB());
-         jGroup.setTextColThreshold(sg.thresholdTextColour);
-         jGroup.setShowUnconserved(sg.getShowNonconserved());
-         jGroup.setIgnoreGapsinConsensus(sg.getIgnoreGapsConsensus());
-         jGroup.setShowConsensusHistogram(sg.isShowConsensusHistogram());
-         jGroup.setShowSequenceLogo(sg.isShowSequenceLogo());
-         jGroup.setNormaliseSequenceLogo(sg.isNormaliseSequenceLogo());
-         for (SequenceI seq : sg.getSequences())
-         {
-           // jGroup.addSeq(seqHash(seq));
-           jGroup.getSeq().add(seqHash(seq));
-         }
-       }
-       //jms.setJGroup(groups);
-       Object group;
-       for (JGroup grp : groups)
-       {
-         object.getJGroup().add(grp);
-       }
-     }
-     if (!storeDS)
-     {
-       // /////////SAVE VIEWPORT
-       Viewport view = new Viewport();
-       view.setTitle(ap.alignFrame.getTitle());
-       view.setSequenceSetId(
-               makeHashCode(av.getSequenceSetId(), av.getSequenceSetId()));
-       view.setId(av.getViewId());
-       if (av.getCodingComplement() != null)
-       {
-         view.setComplementId(av.getCodingComplement().getViewId());
-       }
-       view.setViewName(av.getViewName());
-       view.setGatheredViews(av.isGatherViewsHere());
-       Rectangle size = ap.av.getExplodedGeometry();
-       Rectangle position = size;
-       if (size == null)
-       {
-         size = ap.alignFrame.getBounds();
-         if (av.getCodingComplement() != null)
-         {
-           position = ((SplitFrame) ap.alignFrame.getSplitViewContainer())
-                   .getBounds();
-         }
-         else
-         {
-           position = size;
-         }
-       }
-       view.setXpos(position.x);
-       view.setYpos(position.y);
-       view.setWidth(size.width);
-       view.setHeight(size.height);
-       view.setStartRes(vpRanges.getStartRes());
-       view.setStartSeq(vpRanges.getStartSeq());
-       if (av.getGlobalColourScheme() instanceof jalview.schemes.UserColourScheme)
-       {
-         view.setBgColour(setUserColourScheme(av.getGlobalColourScheme(),
-                 userColours, object));
-       }
-       else if (av
-               .getGlobalColourScheme() instanceof jalview.schemes.AnnotationColourGradient)
-       {
-         AnnotationColourScheme ac = constructAnnotationColours(
-                 (jalview.schemes.AnnotationColourGradient) av
-                         .getGlobalColourScheme(),
-                 userColours, object);
-         view.setAnnotationColours(ac);
-         view.setBgColour("AnnotationColourGradient");
-       }
-       else
-       {
-         view.setBgColour(ColourSchemeProperty
-                 .getColourName(av.getGlobalColourScheme()));
-       }
-       ResidueShaderI vcs = av.getResidueShading();
-       ColourSchemeI cs = av.getGlobalColourScheme();
-       if (cs != null)
-       {
-         if (vcs.conservationApplied())
-         {
-           view.setConsThreshold(vcs.getConservationInc());
-           if (cs instanceof jalview.schemes.UserColourScheme)
-           {
-             view.setBgColour(setUserColourScheme(cs, userColours, object));
-           }
-         }
-         view.setPidThreshold(vcs.getThreshold());
-       }
-       view.setConservationSelected(av.getConservationSelected());
-       view.setPidSelected(av.getAbovePIDThreshold());
-       final Font font = av.getFont();
-       view.setFontName(font.getName());
-       view.setFontSize(font.getSize());
-       view.setFontStyle(font.getStyle());
-       view.setScaleProteinAsCdna(av.getViewStyle().isScaleProteinAsCdna());
-       view.setRenderGaps(av.isRenderGaps());
-       view.setShowAnnotation(av.isShowAnnotation());
-       view.setShowBoxes(av.getShowBoxes());
-       view.setShowColourText(av.getColourText());
-       view.setShowFullId(av.getShowJVSuffix());
-       view.setRightAlignIds(av.isRightAlignIds());
-       view.setShowSequenceFeatures(av.isShowSequenceFeatures());
-       view.setShowText(av.getShowText());
-       view.setShowUnconserved(av.getShowUnconserved());
-       view.setWrapAlignment(av.getWrapAlignment());
-       view.setTextCol1(av.getTextColour().getRGB());
-       view.setTextCol2(av.getTextColour2().getRGB());
-       view.setTextColThreshold(av.getThresholdTextColour());
-       view.setShowConsensusHistogram(av.isShowConsensusHistogram());
-       view.setShowSequenceLogo(av.isShowSequenceLogo());
-       view.setNormaliseSequenceLogo(av.isNormaliseSequenceLogo());
-       view.setShowGroupConsensus(av.isShowGroupConsensus());
-       view.setShowGroupConservation(av.isShowGroupConservation());
-       view.setShowNPfeatureTooltip(av.isShowNPFeats());
-       view.setShowDbRefTooltip(av.isShowDBRefs());
-       view.setFollowHighlight(av.isFollowHighlight());
-       view.setFollowSelection(av.followSelection);
-       view.setIgnoreGapsinConsensus(av.isIgnoreGapsConsensus());
-       view.setShowComplementFeatures(av.isShowComplementFeatures());
-       view.setShowComplementFeaturesOnTop(
-               av.isShowComplementFeaturesOnTop());
-       if (av.getFeaturesDisplayed() != null)
-       {
-         FeatureSettings fs = new FeatureSettings();
-         FeatureRendererModel fr = ap.getSeqPanel().seqCanvas
-                 .getFeatureRenderer();
-         String[] renderOrder = fr.getRenderOrder().toArray(new String[0]);
-         Vector<String> settingsAdded = new Vector<>();
-         if (renderOrder != null)
-         {
-           for (String featureType : renderOrder)
-           {
-             FeatureSettings.Setting setting = new FeatureSettings.Setting();
-             setting.setType(featureType);
-             /*
-              * save any filter for the feature type
-              */
-             FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
-             if (filter != null)  {
-               Iterator<FeatureMatcherI> filters = filter.getMatchers().iterator();
-               FeatureMatcherI firstFilter = filters.next();
-               setting.setMatcherSet(Jalview2XML.marshalFilter(
-                       firstFilter, filters, filter.isAnded()));
-             }
-             /*
-              * save colour scheme for the feature type
-              */
-             FeatureColourI fcol = fr.getFeatureStyle(featureType);
-             if (!fcol.isSimpleColour())
-             {
-               setting.setColour(fcol.getMaxColour().getRGB());
-               setting.setMincolour(fcol.getMinColour().getRGB());
-               setting.setMin(fcol.getMin());
-               setting.setMax(fcol.getMax());
-               setting.setColourByLabel(fcol.isColourByLabel());
-               if (fcol.isColourByAttribute())
-               {
-                 String[] attName = fcol.getAttributeName();
-                 setting.getAttributeName().add(attName[0]);
-                 if (attName.length > 1)
-                 {
-                   setting.getAttributeName().add(attName[1]);
-                 }
-               }
-               setting.setAutoScale(fcol.isAutoScaled());
-               setting.setThreshold(fcol.getThreshold());
-               Color noColour = fcol.getNoColour();
-               if (noColour == null)
-               {
-                 setting.setNoValueColour(NoValueColour.NONE);
-               }
-               else if (noColour.equals(fcol.getMaxColour()))
-               {
-                 setting.setNoValueColour(NoValueColour.MAX);
-               }
-               else
-               {
-                 setting.setNoValueColour(NoValueColour.MIN);
-               }
-               // -1 = No threshold, 0 = Below, 1 = Above
-               setting.setThreshstate(fcol.isAboveThreshold() ? 1
-                       : (fcol.isBelowThreshold() ? 0 : -1));
-             }
-             else
-             {
-               setting.setColour(fcol.getColour().getRGB());
-             }
-             setting.setDisplay(
-                     av.getFeaturesDisplayed().isVisible(featureType));
-             float rorder = fr
-                     .getOrder(featureType);
-             if (rorder > -1)
-             {
-               setting.setOrder(rorder);
-             }
-             /// fs.addSetting(setting);
-             fs.getSetting().add(setting);
-             settingsAdded.addElement(featureType);
-           }
-         }
-         // is groups actually supposed to be a map here ?
-         Iterator<String> en = fr.getFeatureGroups().iterator();
-         Vector<String> groupsAdded = new Vector<>();
-         while (en.hasNext())
-         {
-           String grp = en.next();
-           if (groupsAdded.contains(grp))
-           {
-             continue;
-           }
-           Group g = new Group();
-           g.setName(grp);
-           g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
-                           .booleanValue());
-           // fs.addGroup(g);
-           fs.getGroup().add(g);
-           groupsAdded.addElement(grp);
-         }
-         // jms.setFeatureSettings(fs);
-         object.setFeatureSettings(fs);
-       }
-       if (av.hasHiddenColumns())
-       {
-         jalview.datamodel.HiddenColumns hidden = av.getAlignment()
-                 .getHiddenColumns();
-         if (hidden == null)
-         {
-           warn("REPORT BUG: avoided null columnselection bug (DMAM reported). Please contact Jim about this.");
-         }
-         else
-         {
-           Iterator<int[]> hiddenRegions = hidden.iterator();
-           while (hiddenRegions.hasNext())
-           {
-             int[] region = hiddenRegions.next();
-             HiddenColumns hc = new HiddenColumns();
-             hc.setStart(region[0]);
-             hc.setEnd(region[1]);
-             // view.addHiddenColumns(hc);
-             view.getHiddenColumns().add(hc);
-           }
-         }
-       }
-       if (calcIdSet.size() > 0)
-       {
-         for (String calcId : calcIdSet)
-         {
-           if (calcId.trim().length() > 0)
-           {
-             CalcIdParam cidp = createCalcIdParam(calcId, av);
-             // Some calcIds have no parameters.
-             if (cidp != null)
-             {
-               // view.addCalcIdParam(cidp);
-               view.getCalcIdParam().add(cidp);
-             }
-           }
-         }
-       }
-       // jms.addViewport(view);
-       object.getViewport().add(view);
-     }
-     // object.setJalviewModelSequence(jms);
-     // object.getVamsasModel().addSequenceSet(vamsasSet);
-     object.getVamsasModel().getSequenceSet().add(vamsasSet);
-     if (jout != null && fileName != null)
-     {
-       // We may not want to write the object to disk,
-       // eg we can copy the alignViewport to a new view object
-       // using save and then load
-       try
-       {
-         fileName = fileName.replace('\\', '/');
-         System.out.println("Writing jar entry " + fileName);
-         JarEntry entry = new JarEntry(fileName);
-         jout.putNextEntry(entry);
-         PrintWriter pout = new PrintWriter(
-                 new OutputStreamWriter(jout, UTF_8));
-         JAXBContext jaxbContext = JAXBContext
-                 .newInstance(JalviewModel.class);
-         Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
-         // output pretty printed
-         // jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
-         jaxbMarshaller.marshal(
-                 new ObjectFactory().createJalviewModel(object), pout);
-         // jaxbMarshaller.marshal(object, pout);
-         // marshaller.marshal(object);
-         pout.flush();
-         jout.closeEntry();
-       } catch (Exception ex)
-       {
-         // TODO: raise error in GUI if marshalling failed.
-         System.err.println("Error writing Jalview project");
-         ex.printStackTrace();
-       }
-     }
-     return object;
-   }
-   /**
-    * Saves the HMMER profile associated with the sequence as a file in the jar,
-    * in HMMER format, and saves the name of the file as a child element of the
-    * XML sequence element
-    * 
-    * @param jout
-    * @param xmlSeq
-    * @param seq
-    */
-   protected void saveHmmerProfile(JarOutputStream jout, JSeq xmlSeq,
-           SequenceI seq)
-   {
-     HiddenMarkovModel profile = seq.getHMM();
-     if (profile == null)
-     {
-       warn("Want to save HMM profile for " + seq.getName()
-               + " but none found");
-       return;
-     }
-     HMMFile hmmFile = new HMMFile(profile);
-     String hmmAsString = hmmFile.print();
-     String jarEntryName = HMMER_PREFIX + nextCounter();
-     try
-     {
-       writeJarEntry(jout, jarEntryName, hmmAsString.getBytes());
-       xmlSeq.setHmmerProfile(jarEntryName);
-     } catch (IOException e)
-     {
-       warn("Error saving HMM profile: " + e.getMessage());
-     }
-   }
-     
-   /**
-    * Writes PCA viewer attributes and computed values to an XML model object and
-    * adds it to the JalviewModel. Any exceptions are reported by logging.
-    */
-   protected void savePCA(PCAPanel panel, JalviewModel object)
-   {
-     try
-     {
-       PcaViewer viewer = new PcaViewer();
-       viewer.setHeight(panel.getHeight());
-       viewer.setWidth(panel.getWidth());
-       viewer.setXpos(panel.getX());
-       viewer.setYpos(panel.getY());
-       viewer.setTitle(panel.getTitle());
-       PCAModel pcaModel = panel.getPcaModel();
-       viewer.setScoreModelName(pcaModel.getScoreModelName());
-       viewer.setXDim(panel.getSelectedDimensionIndex(X));
-       viewer.setYDim(panel.getSelectedDimensionIndex(Y));
-       viewer.setZDim(panel.getSelectedDimensionIndex(Z));
-       viewer.setBgColour(
-               panel.getRotatableCanvas().getBackgroundColour().getRGB());
-       viewer.setScaleFactor(panel.getRotatableCanvas().getScaleFactor());
-       float[] spMin = panel.getRotatableCanvas().getSeqMin();
-       SeqPointMin spmin = new SeqPointMin();
-       spmin.setXPos(spMin[0]);
-       spmin.setYPos(spMin[1]);
-       spmin.setZPos(spMin[2]);
-       viewer.setSeqPointMin(spmin);
-       float[] spMax = panel.getRotatableCanvas().getSeqMax();
-       SeqPointMax spmax = new SeqPointMax();
-       spmax.setXPos(spMax[0]);
-       spmax.setYPos(spMax[1]);
-       spmax.setZPos(spMax[2]);
-       viewer.setSeqPointMax(spmax);
-       viewer.setShowLabels(panel.getRotatableCanvas().isShowLabels());
-       viewer.setLinkToAllViews(
-               panel.getRotatableCanvas().isApplyToAllViews());
-       SimilarityParamsI sp = pcaModel.getSimilarityParameters();
-       viewer.setIncludeGaps(sp.includeGaps());
-       viewer.setMatchGaps(sp.matchGaps());
-       viewer.setIncludeGappedColumns(sp.includeGappedColumns());
-       viewer.setDenominateByShortestLength(sp.denominateByShortestLength());
-       /*
-        * sequence points on display
-        */
-       for (jalview.datamodel.SequencePoint spt : pcaModel
-               .getSequencePoints())
-       {
-         SequencePoint point = new SequencePoint();
-         point.setSequenceRef(seqHash(spt.getSequence()));
-         point.setXPos(spt.coord.x);
-         point.setYPos(spt.coord.y);
-         point.setZPos(spt.coord.z);
-         viewer.getSequencePoint().add(point);
-       }
-       /*
-        * (end points of) axes on display
-        */
-       for (Point p : panel.getRotatableCanvas().getAxisEndPoints())
-       {
-         Axis axis = new Axis();
-         axis.setXPos(p.x);
-         axis.setYPos(p.y);
-         axis.setZPos(p.z);
-         viewer.getAxis().add(axis);
-       }
-       /*
-        * raw PCA data (note we are not restoring PCA inputs here -
-        * alignment view, score model, similarity parameters)
-        */
-       PcaDataType data = new PcaDataType();
-       viewer.setPcaData(data);
-       PCA pca = pcaModel.getPcaData();
-       DoubleMatrix pm = new DoubleMatrix();
-       saveDoubleMatrix(pca.getPairwiseScores(), pm);
-       data.setPairwiseMatrix(pm);
-       DoubleMatrix tm = new DoubleMatrix();
-       saveDoubleMatrix(pca.getTridiagonal(), tm);
-       data.setTridiagonalMatrix(tm);
-       DoubleMatrix eigenMatrix = new DoubleMatrix();
-       data.setEigenMatrix(eigenMatrix);
-       saveDoubleMatrix(pca.getEigenmatrix(), eigenMatrix);
-       object.getPcaViewer().add(viewer);
-     } catch (Throwable t)
-     {
-       Cache.log.error("Error saving PCA: " + t.getMessage());
-     }
-   }
-   /**
-    * Stores values from a matrix into an XML element, including (if present) the
-    * D or E vectors
-    * 
-    * @param m
-    * @param xmlMatrix
-    * @see #loadDoubleMatrix(DoubleMatrix)
-    */
-   protected void saveDoubleMatrix(MatrixI m, DoubleMatrix xmlMatrix)
-   {
-     xmlMatrix.setRows(m.height());
-     xmlMatrix.setColumns(m.width());
-     for (int i = 0; i < m.height(); i++)
-     {
-       DoubleVector row = new DoubleVector();
-       for (int j = 0; j < m.width(); j++)
-       {
-         row.getV().add(m.getValue(i, j));
-       }
-       xmlMatrix.getRow().add(row);
-     }
-     if (m.getD() != null)
-     {
-       DoubleVector dVector = new DoubleVector();
-       for (double d : m.getD())
-       {
-         dVector.getV().add(d);
-       }
-       xmlMatrix.setD(dVector);
-     }
-     if (m.getE() != null)
-     {
-       DoubleVector eVector = new DoubleVector();
-       for (double e : m.getE())
-       {
-         eVector.getV().add(e);
-       }
-       xmlMatrix.setE(eVector);
-     }
-   }
-   /**
-    * Loads XML matrix data into a new Matrix object, including the D and/or E
-    * vectors (if present)
-    * 
-    * @param mData
-    * @return
-    * @see Jalview2XML#saveDoubleMatrix(MatrixI, DoubleMatrix)
-    */
-   protected MatrixI loadDoubleMatrix(DoubleMatrix mData)
-   {
-     int rows = mData.getRows();
-     double[][] vals = new double[rows][];
-     for (int i = 0; i < rows; i++)
-     {
-       List<Double> dVector = mData.getRow().get(i).getV();
-       vals[i] = new double[dVector.size()];
-       int dvi = 0;
-       for (Double d : dVector)
-       {
-         vals[i][dvi++] = d;
-       }
-     }
-     MatrixI m = new Matrix(vals);
-     if (mData.getD() != null)
-     {
-       List<Double> dVector = mData.getD().getV();
-       double[] vec = new double[dVector.size()];
-       int dvi = 0;
-       for (Double d : dVector)
-       {
-         vec[dvi++] = d;
-       }
-       m.setD(vec);
-     }
-     if (mData.getE() != null)
-     {
-       List<Double> dVector = mData.getE().getV();
-       double[] vec = new double[dVector.size()];
-       int dvi = 0;
-       for (Double d : dVector)
-       {
-         vec[dvi++] = d;
-       }
-       m.setE(vec);
-     }
-     return m;
-   }
-   /**
-    * Save any Varna viewers linked to this sequence. Writes an rnaViewer element
-    * for each viewer, with
-    * <ul>
-    * <li>viewer geometry (position, size, split pane divider location)</li>
-    * <li>index of the selected structure in the viewer (currently shows gapped
-    * or ungapped)</li>
-    * <li>the id of the annotation holding RNA secondary structure</li>
-    * <li>(currently only one SS is shown per viewer, may be more in future)</li>
-    * </ul>
-    * Varna viewer state is also written out (in native Varna XML) to separate
-    * project jar entries. A separate entry is written for each RNA structure
-    * displayed, with the naming convention
-    * <ul>
-    * <li>rna_viewId_sequenceId_annotationId_[gapped|trimmed]</li>
-    * </ul>
-    * 
-    * @param jout
-    * @param jseq
-    * @param jds
-    * @param viewIds
-    * @param ap
-    * @param storeDataset
-    */
-   protected void saveRnaViewers(JarOutputStream jout, JSeq jseq,
-           final SequenceI jds, List<String> viewIds, AlignmentPanel ap,
-           boolean storeDataset)
-   {
-     if (Desktop.getDesktopPane() == null)
-     {
-       return;
-     }
-     JInternalFrame[] frames = Desktop.getDesktopPane().getAllFrames();
-     for (int f = frames.length - 1; f > -1; f--)
-     {
-       if (frames[f] instanceof AppVarna)
-       {
-         AppVarna varna = (AppVarna) frames[f];
-         /*
-          * link the sequence to every viewer that is showing it and is linked to
-          * its alignment panel
-          */
-         if (varna.isListeningFor(jds) && ap == varna.getAlignmentPanel())
-         {
-           String viewId = varna.getViewId();
-           RnaViewer rna = new RnaViewer();
-           rna.setViewId(viewId);
-           rna.setTitle(varna.getTitle());
-           rna.setXpos(varna.getX());
-           rna.setYpos(varna.getY());
-           rna.setWidth(varna.getWidth());
-           rna.setHeight(varna.getHeight());
-           rna.setDividerLocation(varna.getDividerLocation());
-           rna.setSelectedRna(varna.getSelectedIndex());
-           // jseq.addRnaViewer(rna);
-           jseq.getRnaViewer().add(rna);
-           /*
-            * Store each Varna panel's state once in the project per sequence.
-            * First time through only (storeDataset==false)
-            */
-           // boolean storeSessions = false;
-           // String sequenceViewId = viewId + seqsToIds.get(jds);
-           // if (!storeDataset && !viewIds.contains(sequenceViewId))
-           // {
-           // viewIds.add(sequenceViewId);
-           // storeSessions = true;
-           // }
-           for (RnaModel model : varna.getModels())
-           {
-             if (model.seq == jds)
-             {
-               /*
-                * VARNA saves each view (sequence or alignment secondary
-                * structure, gapped or trimmed) as a separate XML file
-                */
-               String jarEntryName = rnaSessions.get(model);
-               if (jarEntryName == null)
-               {
-                 String varnaStateFile = varna.getStateInfo(model.rna);
-                 jarEntryName = RNA_PREFIX + viewId + "_" + nextCounter();
-                 copyFileToJar(jout, varnaStateFile, jarEntryName, "Varna");
-                 rnaSessions.put(model, jarEntryName);
-               }
-               SecondaryStructure ss = new SecondaryStructure();
-               String annotationId = varna.getAnnotation(jds).annotationId;
-               ss.setAnnotationId(annotationId);
-               ss.setViewerState(jarEntryName);
-               ss.setGapped(model.gapped);
-               ss.setTitle(model.title);
-               // rna.addSecondaryStructure(ss);
-               rna.getSecondaryStructure().add(ss);
-             }
-           }
-         }
-       }
-     }
-   }
-   /**
-    * Copy the contents of a file to a new entry added to the output jar
-    * 
-    * @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 msg)
-   {
-     try (InputStream is = new FileInputStream(infilePath))
-     {
-       File file = new File(infilePath);
-       if (file.exists() && jout != null)
-       {
-         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();
-     }
-   }
-   /**
-    * Copies input to output, in 4K buffers; handles any data (text or binary)
-    * 
-    * @param in
-    * @param out
-    * @throws IOException
-    */
-   protected void copyAll(InputStream in, OutputStream out)
-           throws IOException
-   {
-     byte[] buffer = new byte[4096];
-     int bytesRead = 0;
-     while ((bytesRead = in.read(buffer)) != -1)
-     {
-       out.write(buffer, 0, bytesRead);
-     }
-   }
-   /**
-    * Save the state of a structure viewer
-    * 
-    * @param ap
-    * @param jds
-    * @param pdb
-    *          the archive XML element under which to save the state
-    * @param entry
-    * @param viewIds
-    * @param matchedFile
-    * @param viewFrame
-    * @return
-    */
-   protected String saveStructureViewer(AlignmentPanel ap, SequenceI jds,
-           Pdbids pdb, PDBEntry entry, List<String> viewIds,
-           String matchedFile, StructureViewerBase viewFrame)
-   {
-     final AAStructureBindingModel bindingModel = viewFrame.getBinding();
-     /*
-      * Look for any bindings for this viewer to the PDB file of interest
-      * (including part matches excluding chain id)
-      */
-     for (int peid = 0; peid < bindingModel.getPdbCount(); peid++)
-     {
-       final PDBEntry pdbentry = bindingModel.getPdbEntry(peid);
-       final String pdbId = pdbentry.getId();
-       if (!pdbId.equals(entry.getId())
-               && !(entry.getId().length() > 4 && entry.getId().toLowerCase(Locale.ROOT)
-                       .startsWith(pdbId.toLowerCase(Locale.ROOT))))
-       {
-         /*
-          * not interested in a binding to a different PDB entry here
-          */
-         continue;
-       }
-       if (matchedFile == null)
-       {
-         matchedFile = pdbentry.getFile();
-       }
-       else if (!matchedFile.equals(pdbentry.getFile()))
-       {
-         Cache.log.warn(
-                 "Probably lost some PDB-Sequence mappings for this structure file (which apparently has same PDB Entry code): "
-                         + pdbentry.getFile());
-       }
-       // record the
-       // file so we
-       // can get at it if the ID
-       // match is ambiguous (e.g.
-       // 1QIP==1qipA)
-       for (int smap = 0; smap < viewFrame.getBinding()
-               .getSequence()[peid].length; smap++)
-       {
-         // if (jal.findIndex(jmol.jmb.sequence[peid][smap]) > -1)
-         if (jds == viewFrame.getBinding().getSequence()[peid][smap])
-         {
-           StructureState state = new StructureState();
-           state.setVisible(true);
-           state.setXpos(viewFrame.getX());
-           state.setYpos(viewFrame.getY());
-           state.setWidth(viewFrame.getWidth());
-           state.setHeight(viewFrame.getHeight());
-           final String viewId = viewFrame.getViewId();
-           state.setViewId(viewId);
-           state.setAlignwithAlignPanel(viewFrame.isUsedforaligment(ap));
-           state.setColourwithAlignPanel(viewFrame.isUsedForColourBy(ap));
-           state.setColourByJmol(viewFrame.isColouredByViewer());
-           state.setType(viewFrame.getViewerType().toString());
-           // pdb.addStructureState(state);
-           pdb.getStructureState().add(state);
-         }
-       }
-     }
-     return matchedFile;
-   }
-   /**
-    * Populates the AnnotationColourScheme xml for save. This captures the
-    * settings of the options in the 'Colour by Annotation' dialog.
-    * 
-    * @param acg
-    * @param userColours
-    * @param jm
-    * @return
-    */
-   private AnnotationColourScheme constructAnnotationColours(
-           AnnotationColourGradient acg, List<UserColourScheme> userColours,
-           JalviewModel jm)
-   {
-     AnnotationColourScheme ac = new AnnotationColourScheme();
-     ac.setAboveThreshold(acg.getAboveThreshold());
-     ac.setThreshold(acg.getAnnotationThreshold());
-     // 2.10.2 save annotationId (unique) not annotation label
-     ac.setAnnotation(acg.getAnnotation().annotationId);
-     if (acg.getBaseColour() instanceof UserColourScheme)
-     {
-       ac.setColourScheme(
-               setUserColourScheme(acg.getBaseColour(), userColours, jm));
-     }
-     else
-     {
-       ac.setColourScheme(
-               ColourSchemeProperty.getColourName(acg.getBaseColour()));
-     }
-     ac.setMaxColour(acg.getMaxColour().getRGB());
-     ac.setMinColour(acg.getMinColour().getRGB());
-     ac.setPerSequence(acg.isSeqAssociated());
-     ac.setPredefinedColours(acg.isPredefinedColours());
-     return ac;
-   }
-   private void storeAlignmentAnnotation(AlignmentAnnotation[] aa,
-           IdentityHashMap<SequenceGroup, String> groupRefs,
-           AlignmentViewport av, Set<String> calcIdSet, boolean storeDS,
-           SequenceSet vamsasSet)
-   {
-     for (int i = 0; i < aa.length; i++)
-     {
-       Annotation an = new Annotation();
-       AlignmentAnnotation annotation = aa[i];
-       if (annotation.annotationId != null)
-       {
-         annotationIds.put(annotation.annotationId, annotation);
-       }
-       an.setId(annotation.annotationId);
-       an.setVisible(annotation.visible);
-       an.setDescription(annotation.description);
-       if (annotation.sequenceRef != null)
-       {
-         // 2.9 JAL-1781 xref on sequence id rather than name
-         an.setSequenceRef(seqsToIds.get(annotation.sequenceRef));
-       }
-       if (annotation.groupRef != null)
-       {
-         String groupIdr = groupRefs.get(annotation.groupRef);
-         if (groupIdr == null)
-         {
-           // make a locally unique String
-           groupRefs.put(annotation.groupRef,
-                   groupIdr = ("" + System.currentTimeMillis()
-                           + annotation.groupRef.getName()
-                           + groupRefs.size()));
-         }
-         an.setGroupRef(groupIdr.toString());
-       }
-       // store all visualization attributes for annotation
-       an.setGraphHeight(annotation.graphHeight);
-       an.setCentreColLabels(annotation.centreColLabels);
-       an.setScaleColLabels(annotation.scaleColLabel);
-       an.setShowAllColLabels(annotation.showAllColLabels);
-       an.setBelowAlignment(annotation.belowAlignment);
-       if (annotation.graph > 0)
-       {
-         an.setGraph(true);
-         an.setGraphType(annotation.graph);
-         an.setGraphGroup(annotation.graphGroup);
-         if (annotation.getThreshold() != null)
-         {
-           ThresholdLine line = new ThresholdLine();
-           line.setLabel(annotation.getThreshold().label);
-           line.setValue(annotation.getThreshold().value);
-           line.setColour(annotation.getThreshold().colour.getRGB());
-           an.setThresholdLine(line);
-         }
-       }
-       else
-       {
-         an.setGraph(false);
-       }
-       an.setLabel(annotation.label);
-       if (annotation == av.getAlignmentQualityAnnot()
-               || annotation == av.getAlignmentConservationAnnotation()
-               || annotation == av.getAlignmentConsensusAnnotation()
-               || annotation.autoCalculated)
-       {
-         // new way of indicating autocalculated annotation -
-         an.setAutoCalculated(annotation.autoCalculated);
-       }
-       if (annotation.hasScore())
-       {
-         an.setScore(annotation.getScore());
-       }
-       if (annotation.getCalcId() != null)
-       {
-         calcIdSet.add(annotation.getCalcId());
-         an.setCalcId(annotation.getCalcId());
-       }
-       if (annotation.hasProperties())
-       {
-         for (String pr : annotation.getProperties())
-         {
-           jalview.xml.binding.jalview.Annotation.Property prop = new jalview.xml.binding.jalview.Annotation.Property();
-           prop.setName(pr);
-           prop.setValue(annotation.getProperty(pr));
-           // an.addProperty(prop);
-           an.getProperty().add(prop);
-         }
-       }
-       AnnotationElement ae;
-       if (annotation.annotations != null)
-       {
-         an.setScoreOnly(false);
-         for (int a = 0; a < annotation.annotations.length; a++)
-         {
-           if ((annotation == null) || (annotation.annotations[a] == null))
-           {
-             continue;
-           }
-           ae = new AnnotationElement();
-           if (annotation.annotations[a].description != null)
-           {
-             ae.setDescription(annotation.annotations[a].description);
-           }
-           if (annotation.annotations[a].displayCharacter != null)
-           {
-             ae.setDisplayCharacter(
-                     annotation.annotations[a].displayCharacter);
-           }
-           if (!Float.isNaN(annotation.annotations[a].value))
-           {
-             ae.setValue(annotation.annotations[a].value);
-           }
-           ae.setPosition(a);
-           if (annotation.annotations[a].secondaryStructure > ' ')
-           {
-             ae.setSecondaryStructure(
-                     annotation.annotations[a].secondaryStructure + "");
-           }
-           if (annotation.annotations[a].colour != null
-                   && annotation.annotations[a].colour != java.awt.Color.black)
-           {
-             ae.setColour(annotation.annotations[a].colour.getRGB());
-           }
-           // an.addAnnotationElement(ae);
-           an.getAnnotationElement().add(ae);
-           if (annotation.autoCalculated)
-           {
-             // only write one non-null entry into the annotation row -
-             // sufficient to get the visualization attributes necessary to
-             // display data
-             continue;
-           }
-         }
-       }
-       else
-       {
-         an.setScoreOnly(true);
-       }
-       if (!storeDS || (storeDS && !annotation.autoCalculated))
-       {
-         // skip autocalculated annotation - these are only provided for
-         // alignments
-         // vamsasSet.addAnnotation(an);
-         vamsasSet.getAnnotation().add(an);
-       }
-     }
-   }
-   private CalcIdParam createCalcIdParam(String calcId, AlignViewport av)
-   {
-     AutoCalcSetting settings = av.getCalcIdSettingsFor(calcId);
-     if (settings != null)
-     {
-       CalcIdParam vCalcIdParam = new CalcIdParam();
-       vCalcIdParam.setCalcId(calcId);
-       // vCalcIdParam.addServiceURL(settings.getServiceURI());
-       vCalcIdParam.getServiceURL().add(settings.getServiceURI());
-       // generic URI allowing a third party to resolve another instance of the
-       // service used for this calculation
-       for (String url : settings.getServiceURLs())
-       {
-         // vCalcIdParam.addServiceURL(urls);
-         vCalcIdParam.getServiceURL().add(url);
-       }
-       vCalcIdParam.setVersion("1.0");
-       if (settings.getPreset() != null)
-       {
-         WsParamSetI setting = settings.getPreset();
-         vCalcIdParam.setName(setting.getName());
-         vCalcIdParam.setDescription(setting.getDescription());
-       }
-       else
-       {
-         vCalcIdParam.setName("");
-         vCalcIdParam.setDescription("Last used parameters");
-       }
-       // need to be able to recover 1) settings 2) user-defined presets or
-       // recreate settings from preset 3) predefined settings provided by
-       // service - or settings that can be transferred (or discarded)
-       vCalcIdParam.setParameters(
-               settings.getWsParamFile().replace("\n", "|\\n|"));
-       vCalcIdParam.setAutoUpdate(settings.isAutoUpdate());
-       // todo - decide if updateImmediately is needed for any projects.
-       return vCalcIdParam;
-     }
-     return null;
-   }
-   private boolean recoverCalcIdParam(CalcIdParam calcIdParam,
-           AlignViewport av)
-   {
-     if (calcIdParam.getVersion().equals("1.0"))
-     {
-       final String[] calcIds = calcIdParam.getServiceURL().toArray(new String[0]);
-       ServiceWithParameters service = PreferredServiceRegistry.getRegistry()
-               .getPreferredServiceFor(calcIds);
-       if (service != null)
-       {
-         WsParamSetI parmSet = null;
-         try
-         {
-           parmSet = service.getParamStore().parseServiceParameterFile(
-                   calcIdParam.getName(), calcIdParam.getDescription(),
-                   calcIds,
-                   calcIdParam.getParameters().replace("|\\n|", "\n"));
-         } catch (IOException x)
-         {
-           warn("Couldn't parse parameter data for "
-                   + calcIdParam.getCalcId(), x);
-           return false;
-         }
-         List<ArgumentI> argList = null;
-         if (calcIdParam.getName().length() > 0)
-         {
-           parmSet = service.getParamStore()
-                   .getPreset(calcIdParam.getName());
-           if (parmSet != null)
-           {
-             // TODO : check we have a good match with settings in AACon -
-             // otherwise we'll need to create a new preset
-           }
-         }
-         else
-         {
-           argList = parmSet.getArguments();
-           parmSet = null;
-         }
-         AutoCalcSetting settings = new AAConSettings(
-                 calcIdParam.isAutoUpdate(), service, parmSet, argList);
-         av.setCalcIdSettingsFor(calcIdParam.getCalcId(), settings,
-                 calcIdParam.isNeedsUpdate());
-         return true;
-       }
-       else
-       {
-         warn("Cannot resolve a service for the parameters used in this project. Try configuring a server in the Web Services preferences tab.");
-         return false;
-       }
-     }
-     throw new Error(MessageManager.formatMessage(
-             "error.unsupported_version_calcIdparam", new Object[]
-             { calcIdParam.toString() }));
-   }
-   /**
-    * External mapping between jalview objects and objects yielding a valid and
-    * unique object ID string. This is null for normal Jalview project IO, but
-    * non-null when a jalview project is being read or written as part of a
-    * vamsas session.
-    */
-   IdentityHashMap jv2vobj = null;
-   /**
-    * Construct a unique ID for jvobj using either existing bindings or if none
-    * exist, the result of the hashcode call for the object.
-    * 
-    * @param jvobj
-    *          jalview data object
-    * @return unique ID for referring to jvobj
-    */
-   private String makeHashCode(Object jvobj, String altCode)
-   {
-     if (jv2vobj != null)
-     {
-       Object id = jv2vobj.get(jvobj);
-       if (id != null)
-       {
-         return id.toString();
-       }
-       // check string ID mappings
-       if (jvids2vobj != null && jvobj instanceof String)
-       {
-         id = jvids2vobj.get(jvobj);
-       }
-       if (id != null)
-       {
-         return id.toString();
-       }
-       // give up and warn that something has gone wrong
-       warn("Cannot find ID for object in external mapping : " + jvobj);
-     }
-     return altCode;
-   }
-   /**
-    * return local jalview object mapped to ID, if it exists
-    * 
-    * @param idcode
-    *          (may be null)
-    * @return null or object bound to idcode
-    */
-   private Object retrieveExistingObj(String idcode)
-   {
-     if (idcode != null && vobj2jv != null)
-     {
-       return vobj2jv.get(idcode);
-     }
-     return null;
-   }
-   /**
-    * binding from ID strings from external mapping table to jalview data model
-    * objects.
-    */
-   private Hashtable vobj2jv;
-   private Sequence createVamsasSequence(String id, SequenceI jds)
-   {
-     return createVamsasSequence(true, id, jds, null);
-   }
-   private Sequence createVamsasSequence(boolean recurse, String id,
-           SequenceI jds, SequenceI parentseq)
-   {
-     Sequence vamsasSeq = new Sequence();
-     vamsasSeq.setId(id);
-     vamsasSeq.setName(jds.getName());
-     vamsasSeq.setSequence(jds.getSequenceAsString());
-     vamsasSeq.setDescription(jds.getDescription());
-     List<DBRefEntry> dbrefs = null;
-     if (jds.getDatasetSequence() != null)
-     {
-       vamsasSeq.setDsseqid(seqHash(jds.getDatasetSequence()));
-     }
-     else
-     {
-       // seqId==dsseqid so we can tell which sequences really are
-       // dataset sequences only
-       vamsasSeq.setDsseqid(id);
-       dbrefs = jds.getDBRefs();
-       if (parentseq == null)
-       {
-         parentseq = jds;
-       }
-     }
-     /*
-      * save any dbrefs; special subclass GeneLocus is flagged as 'locus'
-      */
-     if (dbrefs != null)
-     {
-       for (int d = 0, nd = dbrefs.size(); d < nd; d++)
-       {
-         DBRef dbref = new DBRef();
-         DBRefEntry ref = dbrefs.get(d);
-         dbref.setSource(ref.getSource());
-         dbref.setVersion(ref.getVersion());
-         dbref.setAccessionId(ref.getAccessionId());
-         dbref.setCanonical(ref.isCanonical());
-         if (ref instanceof GeneLocus)
-         {
-           dbref.setLocus(true);
-         }
-         if (ref.hasMap())
-         {
-           Mapping mp = createVamsasMapping(ref.getMap(), parentseq,
-                   jds, recurse);
-           dbref.setMapping(mp);
-         }
-         vamsasSeq.getDBRef().add(dbref);
-       }
-     }
-     return vamsasSeq;
-   }
-   private Mapping createVamsasMapping(jalview.datamodel.Mapping jmp,
-           SequenceI parentseq, SequenceI jds, boolean recurse)
-   {
-     Mapping mp = null;
-     if (jmp.getMap() != null)
-     {
-       mp = new Mapping();
-       jalview.util.MapList mlst = jmp.getMap();
-       List<int[]> r = mlst.getFromRanges();
-       for (int[] range : r)
-       {
-         MapListFrom mfrom = new MapListFrom();
-         mfrom.setStart(range[0]);
-         mfrom.setEnd(range[1]);
-         // mp.addMapListFrom(mfrom);
-         mp.getMapListFrom().add(mfrom);
-       }
-       r = mlst.getToRanges();
-       for (int[] range : r)
-       {
-         MapListTo mto = new MapListTo();
-         mto.setStart(range[0]);
-         mto.setEnd(range[1]);
-         // mp.addMapListTo(mto);
-         mp.getMapListTo().add(mto);
-       }
-       mp.setMapFromUnit(BigInteger.valueOf(mlst.getFromRatio()));
-       mp.setMapToUnit(BigInteger.valueOf(mlst.getToRatio()));
-       if (jmp.getTo() != null)
-       {
-         // MappingChoice mpc = new MappingChoice();
-         // check/create ID for the sequence referenced by getTo()
-         String jmpid = "";
-         SequenceI ps = null;
-         if (parentseq != jmp.getTo()
-                 && parentseq.getDatasetSequence() != jmp.getTo())
-         {
-           // chaining dbref rather than a handshaking one
-           jmpid = seqHash(ps = jmp.getTo());
-         }
-         else
-         {
-           jmpid = seqHash(ps = parentseq);
-         }
-         // mpc.setDseqFor(jmpid);
-         mp.setDseqFor(jmpid);
-         if (!seqRefIds.containsKey(jmpid))
-         {
-           jalview.bin.Cache.log.debug("creatign new DseqFor ID");
-           seqRefIds.put(jmpid, ps);
-         }
-         else
-         {
-           jalview.bin.Cache.log.debug("reusing DseqFor ID");
-         }
-         // mp.setMappingChoice(mpc);
-       }
-     }
-     return mp;
-   }
-   String setUserColourScheme(jalview.schemes.ColourSchemeI cs,
-           List<UserColourScheme> userColours, JalviewModel jm)
-   {
-     String id = null;
-     jalview.schemes.UserColourScheme ucs = (jalview.schemes.UserColourScheme) cs;
-     boolean newucs = false;
-     if (!userColours.contains(ucs))
-     {
-       userColours.add(ucs);
-       newucs = true;
-     }
-     id = "ucs" + userColours.indexOf(ucs);
-     if (newucs)
-     {
-       // actually create the scheme's entry in the XML model
-       java.awt.Color[] colours = ucs.getColours();
-       UserColours uc = new UserColours();
-       // UserColourScheme jbucs = new UserColourScheme();
-       JalviewUserColours jbucs = new JalviewUserColours();
-       for (int i = 0; i < colours.length; i++)
-       {
-         Colour col = new Colour();
-         col.setName(ResidueProperties.aa[i]);
-         col.setRGB(jalview.util.Format.getHexString(colours[i]));
-         // jbucs.addColour(col);
-         jbucs.getColour().add(col);
-       }
-       if (ucs.getLowerCaseColours() != null)
-       {
-         colours = ucs.getLowerCaseColours();
-         for (int i = 0; i < colours.length; i++)
-         {
-           Colour col = new Colour();
-           col.setName(ResidueProperties.aa[i].toLowerCase(Locale.ROOT));
-           col.setRGB(jalview.util.Format.getHexString(colours[i]));
-           // jbucs.addColour(col);
-           jbucs.getColour().add(col);
-         }
-       }
-       uc.setId(id);
-       uc.setUserColourScheme(jbucs);
-       // jm.addUserColours(uc);
-       jm.getUserColours().add(uc);
-     }
-     return id;
-   }
-   jalview.schemes.UserColourScheme getUserColourScheme(JalviewModel jm,
-           String id)
-   {
-     List<UserColours> uc = jm.getUserColours();
-     UserColours colours = null;
-     /*
-     for (int i = 0; i < uc.length; i++)
-     {
-       if (uc[i].getId().equals(id))
-       {
-         colours = uc[i];
-         break;
-       }
-     }
-     */
-     for (UserColours c : uc)
-     {
-       if (c.getId().equals(id))
-       {
-         colours = c;
-         break;
-       }
-     }
-     java.awt.Color[] newColours = new java.awt.Color[24];
-     for (int i = 0; i < 24; i++)
-     {
-       newColours[i] = new java.awt.Color(Integer.parseInt(
-               // colours.getUserColourScheme().getColour(i).getRGB(), 16));
-               colours.getUserColourScheme().getColour().get(i).getRGB(),
-               16));
-     }
-     jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(
-             newColours);
-     if (colours.getUserColourScheme().getColour().size()/*Count()*/ > 24)
-     {
-       newColours = new java.awt.Color[23];
-       for (int i = 0; i < 23; i++)
-       {
-         newColours[i] = new java.awt.Color(
-                 Integer.parseInt(colours.getUserColourScheme().getColour()
-                         .get(i + 24).getRGB(), 16));
-       }
-       ucs.setLowerCaseColours(newColours);
-     }
-     return ucs;
-   }
-   /**
-    * contains last error message (if any) encountered by XML loader.
-    */
-   String errorMessage = null;
-   /**
-    * flag to control whether the Jalview2XML_V1 parser should be deferred to if
-    * exceptions are raised during project XML parsing
-    */
-   public boolean attemptversion1parse = false;
-   /**
-    * Load a jalview project archive from a jar file
-    * 
-    * @param file
-    *          - HTTP URL or filename
-    */
-   public AlignFrame loadJalviewAlign(final Object file)
-   {
-     jalview.gui.AlignFrame af = null;
-     try
-     {
-       // create list to store references for any new Jmol viewers created
-       newStructureViewers = new Vector<>();
-       // UNMARSHALLER SEEMS TO CLOSE JARINPUTSTREAM, MOST ANNOYING
-       // Workaround is to make sure caller implements the JarInputStreamProvider
-       // interface
-       // so we can re-open the jar input stream for each entry.
-       jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
-       af = loadJalviewAlign(jprovider);
-       if (af != null)
-       {
-         af.setMenusForViewport();
-       }
-     } catch (MalformedURLException e)
-     {
-       errorMessage = "Invalid URL format for '" + file + "'";
-       reportErrors();
-     } finally
-     {
-       try
-       {
- // was invokeAndWait
-         
-         // BH 2019 -- can't wait
-         SwingUtilities.invokeLater(new Runnable()
-         {
-           @Override
-           public void run()
-           {
-             setLoadingFinishedForNewStructureViewers();
-           }
-         });
-       } catch (Exception x)
-       {
-         System.err.println("Error loading alignment: " + x.getMessage());
-       }
-     }
-     this.jarFile = null;
-     return af;
-   }
-       @SuppressWarnings("unused")
-   private jarInputStreamProvider createjarInputStreamProvider(
-           final Object ofile) throws MalformedURLException
-   {
-     try
-     {
-       String file = (ofile instanceof File
-               ? ((File) ofile).getCanonicalPath()
-               : ofile.toString());
-       byte[] bytes = Platform.isJS() ? Platform.getFileBytes((File) ofile)
-               : null;
-       if (bytes != null)
-       {
-         this.jarFile = (File) ofile;
-       }
-       URL url = null;
-       errorMessage = null;
-       uniqueSetSuffix = null;
-       seqRefIds = null;
-       viewportsAdded.clear();
-       frefedSequence = null;
-       if (HttpUtils.startsWithHttpOrHttps(file))
-       {
-         url = new URL(file);
-       }
-       return new jarInputStreamProvider()
-       {
-         @Override
-         public JarInputStream getJarInputStream() throws IOException
-         {
-           InputStream is = bytes != null ? new ByteArrayInputStream(bytes)
-                   : (url != null ? url.openStream()
-                           : new FileInputStream(file));
-           return new JarInputStream(is);
-         }
-         @Override
-         public File getFile()
-         {
-           return jarFile;
-         }
-         @Override
-         public String getFilename()
-         {
-           return file;
-         }
-       };
-     } catch (IOException e)
-     {
-       e.printStackTrace();
-       return null;
-     }
-   }
-   /**
-    * Recover jalview session from a jalview project archive. Caller may
-    * initialise uniqueSetSuffix, seqRefIds, viewportsAdded and frefedSequence
-    * themselves. Any null fields will be initialised with default values,
-    * non-null fields are left alone.
-    * 
-    * @param jprovider
-    * @return
-    */
-   public AlignFrame loadJalviewAlign(final jarInputStreamProvider jprovider)
-   {
-     errorMessage = null;
-     if (uniqueSetSuffix == null)
-     {
-       uniqueSetSuffix = System.currentTimeMillis() % 100000 + "";
-     }
-     if (seqRefIds == null)
-     {
-       initSeqRefs();
-     }
-     AlignFrame af = null, _af = null;
-     IdentityHashMap<AlignmentI, AlignmentI> importedDatasets = new IdentityHashMap<>();
-     Map<String, AlignFrame> gatherToThisFrame = new HashMap<>();
-     String fileName = jprovider.getFilename();
-     File file = jprovider.getFile();
-     List<AlignFrame> alignFrames = new ArrayList<>();
-     try
-     {
-       JarInputStream jin = null;
-       JarEntry jarentry = null;
-       int entryCount = 1;
-       // Look for all the entry names ending with ".xml"
-       // This includes all panels and at least one frame.
- //      Platform.timeCheck(null, Platform.TIME_MARK);
-       do
-       {
-         jin = jprovider.getJarInputStream();
-         for (int i = 0; i < entryCount; i++)
-         {
-           jarentry = jin.getNextJarEntry();
-         }
-         String name = (jarentry == null ? null : jarentry.getName());
- //        System.out.println("Jalview2XML opening " + name);
-         if (name != null && name.endsWith(".xml"))
-         {
-           // DataSet for.... is read last.
-           
-           
-           // The question here is what to do with the two
-           // .xml files in the jvp file.
-           // Some number of them, "...Dataset for...", will be the
-           // Only AlignPanels and will have Viewport.
-           // One or more will be the source data, with the DBRefs.
-           //
-           // JVP file writing (above) ensures tha the AlignPanels are written
-           // first, then all relevant datasets (which are
-           // Jalview.datamodel.Alignment).
-           //
- //          Platform.timeCheck("Jalview2XML JAXB " + name, Platform.TIME_MARK);
-           JAXBContext jc = JAXBContext
-                   .newInstance("jalview.xml.binding.jalview");
-           XMLStreamReader streamReader = XMLInputFactory.newInstance()
-                   .createXMLStreamReader(jin);
-           javax.xml.bind.Unmarshaller um = jc.createUnmarshaller();
-           JAXBElement<JalviewModel> jbe = um
-                   .unmarshal(streamReader, JalviewModel.class);
-           JalviewModel model = jbe.getValue();
-           if (true) // !skipViewport(object))
-           {
-             // Q: Do we have to load from the model, even if it
-             // does not have a viewport, could we discover that early on?
-             // Q: Do we need to load this object?
-             _af = loadFromObject(model, fileName, file, true, jprovider);
- //            Platform.timeCheck("Jalview2XML.loadFromObject",
-             // Platform.TIME_MARK);
-             if (_af != null)
-             {
-               alignFrames.add(_af);
-             }
-             if (_af != null && model.getViewport().size() > 0)
-             {
-               // That is, this is one of the AlignmentPanel models
-               if (af == null)
-               {
-                 // store a reference to the first view
-                 af = _af;
-               }
-               if (_af.getViewport().isGatherViewsHere())
-               {
-                 // if this is a gathered view, keep its reference since
-                 // after gathering views, only this frame will remain
-                 af = _af;
-                 gatherToThisFrame.put(_af.getViewport().getSequenceSetId(),
-                         _af);
-               }
-               // Save dataset to register mappings once all resolved
-               importedDatasets.put(
-                       af.getViewport().getAlignment().getDataset(),
-                       af.getViewport().getAlignment().getDataset());
-             }
-           }
-           entryCount++;
-         }
-         else if (jarentry != null)
-         {
-           // Some other file here.
-           entryCount++;
-         }
-       } while (jarentry != null);
-       resolveFrefedSequences();
-     } catch (IOException ex)
-     {
-       ex.printStackTrace();
-       errorMessage = "Couldn't locate Jalview XML file : " + fileName;
-       System.err.println(
-               "Exception whilst loading jalview XML file : " + ex + "\n");
-     } catch (Exception ex)
-     {
-       System.err.println("Parsing as Jalview Version 2 file failed.");
-       ex.printStackTrace(System.err);
-       if (attemptversion1parse)
-       {
-         // used to attempt to parse as V1 castor-generated xml
-       }
-       if (Desktop.getInstance() != null)
-       {
-         Desktop.getInstance().stopLoading();
-       }
-       if (af != null)
-       {
-         System.out.println("Successfully loaded archive file");
-         return af;
-       }
-       ex.printStackTrace();
-       System.err.println(
-               "Exception whilst loading jalview XML file : " + ex + "\n");
-     } catch (OutOfMemoryError e)
-     {
-       // Don't use the OOM Window here
-       errorMessage = "Out of memory loading jalview XML file";
-       System.err.println("Out of memory whilst loading jalview XML file");
-       e.printStackTrace();
-     } finally
-     {
-       for (AlignFrame alf : alignFrames)
-       {
-         alf.alignPanel.setHoldRepaint(false);
-       }
-     }
-     /*
-      * Regather multiple views (with the same sequence set id) to the frame (if
-      * any) that is flagged as the one to gather to, i.e. convert them to tabbed
-      * views instead of separate frames. Note this doesn't restore a state where
-      * some expanded views in turn have tabbed views - the last "first tab" read
-      * in will play the role of gatherer for all.
-      */
-     for (AlignFrame fr : gatherToThisFrame.values())
-     {
-       Desktop.getInstance().gatherViews(fr);
-     }
-     restoreSplitFrames();
-     for (AlignmentI ds : importedDatasets.keySet())
-     {
-       if (ds.getCodonFrames() != null)
-       {
-         Desktop.getStructureSelectionManager()
-                 .registerMappings(ds.getCodonFrames());
-       }
-     }
-     if (errorMessage != null)
-     {
-       reportErrors();
-     }
-     if (Desktop.getInstance() != null)
-     {
-       Desktop.getInstance().stopLoading();
-     }
-     return af;
-   }
-   /**
-    * Try to reconstruct and display SplitFrame windows, where each contains
-    * complementary dna and protein alignments. Done by pairing up AlignFrame
-    * objects (created earlier) which have complementary viewport ids associated.
-    */
-   protected void restoreSplitFrames()
-   {
-     List<SplitFrame> gatherTo = new ArrayList<>();
-     List<AlignFrame> addedToSplitFrames = new ArrayList<>();
-     Map<String, AlignFrame> dna = new HashMap<>();
-     /*
-      * Identify the DNA alignments
-      */
-     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
-             .entrySet())
-     {
-       AlignFrame af = candidate.getValue();
-       if (af.getViewport().getAlignment().isNucleotide())
-       {
-         dna.put(candidate.getKey().getId(), af);
-       }
-     }
-     /*
-      * Try to match up the protein complements
-      */
-     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
-             .entrySet())
-     {
-       AlignFrame af = candidate.getValue();
-       if (!af.getViewport().getAlignment().isNucleotide())
-       {
-         String complementId = candidate.getKey().getComplementId();
-         // only non-null complements should be in the Map
-         if (complementId != null && dna.containsKey(complementId))
-         {
-           final AlignFrame dnaFrame = dna.get(complementId);
-           SplitFrame sf = createSplitFrame(dnaFrame, af);
-           addedToSplitFrames.add(dnaFrame);
-           addedToSplitFrames.add(af);
-           dnaFrame.setMenusForViewport();
-           af.setMenusForViewport();
-           if (af.getViewport().isGatherViewsHere())
-           {
-             gatherTo.add(sf);
-           }
-         }
-       }
-     }
-     /*
-      * Open any that we failed to pair up (which shouldn't happen!) as
-      * standalone AlignFrame's.
-      */
-     for (Entry<Viewport, AlignFrame> candidate : splitFrameCandidates
-             .entrySet())
-     {
-       AlignFrame af = candidate.getValue();
-       if (!addedToSplitFrames.contains(af))
-       {
-         Viewport view = candidate.getKey();
-         Desktop.addInternalFrame(af, view.getTitle(),
-                 safeInt(view.getWidth()), safeInt(view.getHeight()));
-         af.setMenusForViewport();
-         System.err.println("Failed to restore view " + view.getTitle()
-                 + " to split frame");
-       }
-     }
-     /*
-      * Gather back into tabbed views as flagged.
-      */
-     for (SplitFrame sf : gatherTo)
-     {
-       Desktop.getInstance().gatherViews(sf);
-     }
-     splitFrameCandidates.clear();
-   }
-   /**
-    * Construct and display one SplitFrame holding DNA and protein alignments.
-    * 
-    * @param dnaFrame
-    * @param proteinFrame
-    * @return
-    */
-   protected SplitFrame createSplitFrame(AlignFrame dnaFrame,
-           AlignFrame proteinFrame)
-   {
-     SplitFrame splitFrame = new SplitFrame(dnaFrame, proteinFrame);
-     String title = MessageManager.getString("label.linked_view_title");
-     int width = (int) dnaFrame.getBounds().getWidth();
-     int height = (int) (dnaFrame.getBounds().getHeight()
-             + proteinFrame.getBounds().getHeight() + 50);
-     /*
-      * SplitFrame location is saved to both enclosed frames
-      */
-     splitFrame.setLocation(dnaFrame.getX(), dnaFrame.getY());
-     Desktop.addInternalFrame(splitFrame, title, width, height);
-     /*
-      * And compute cDNA consensus (couldn't do earlier with consensus as
-      * mappings were not yet present)
-      */
-     proteinFrame.getViewport().alignmentChanged(proteinFrame.alignPanel);
-     return splitFrame;
-   }
-   /**
-    * check errorMessage for a valid error message and raise an error box in the
-    * GUI or write the current errorMessage to stderr and then clear the error
-    * state.
-    */
-   protected void reportErrors()
-   {
-     reportErrors(false);
-   }
-   protected void reportErrors(final boolean saving)
-   {
-     if (errorMessage != null)
-     {
-       final String finalErrorMessage = errorMessage;
-       if (raiseGUI)
-       {
-         javax.swing.SwingUtilities.invokeLater(new Runnable()
-         {
-           @Override
-           public void run()
-           {
-             JvOptionPane.showInternalMessageDialog(Desktop.getDesktopPane(),
-                     finalErrorMessage,
-                     "Error " + (saving ? "saving" : "loading")
-                             + " Jalview file",
-                     JvOptionPane.WARNING_MESSAGE);
-           }
-         });
-       }
-       else
-       {
-         System.err.println("Problem loading Jalview file: " + errorMessage);
-       }
-     }
-     errorMessage = null;
-   }
-   Map<String, String> alreadyLoadedPDB = new HashMap<>();
-   /**
-    * when set, local views will be updated from view stored in JalviewXML
-    * Currently (28th Sep 2008) things will go horribly wrong in vamsas document
-    * sync if this is set to true.
-    */
-   private final boolean updateLocalViews = false;
-   /**
-    * Returns the path to a temporary file holding the PDB file for the given PDB
-    * id. The first time of asking, searches for a file of that name in the
-    * Jalview project jar, and copies it to a new temporary file. Any repeat
-    * requests just return the path to the file previously created.
-    * 
-    * @param jprovider
-    * @param pdbId
-    * @return
-    */
-   String loadPDBFile(jarInputStreamProvider jprovider, String pdbId,
-           String origFile)
-   {
-     if (alreadyLoadedPDB.containsKey(pdbId))
-     {
-       return alreadyLoadedPDB.get(pdbId).toString();
-     }
-     String tempFile = copyJarEntry(jprovider, pdbId, "jalview_pdb",
-             origFile);
-     if (tempFile != null)
-     {
-       alreadyLoadedPDB.put(pdbId, tempFile);
-     }
-     return tempFile;
-   }
-   /**
-    * Copies the jar entry of given name to a new temporary file and returns the
-    * path to the file, or null if the entry is not found.
-    * 
-    * @param jprovider
-    * @param jarEntryName
-    * @param prefix
-    *          a prefix for the temporary file name, must be at least three
-    *          characters long
-    * @param origFile
-    *          null or original file - so new file can be given the same suffix
-    *          as the old one
-    * @return
-    */
-   protected String copyJarEntry(jarInputStreamProvider jprovider,
-           String jarEntryName, String prefix, String origFile)
-   {
-     BufferedReader in = null;
-     PrintWriter out = null;
-     String suffix = ".tmp";
-     if (origFile == null)
-     {
-       origFile = jarEntryName;
-     }
-     int sfpos = origFile.lastIndexOf(".");
-     if (sfpos > -1 && sfpos < (origFile.length() - 3))
-     {
-       suffix = "." + origFile.substring(sfpos + 1);
-     }
-     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));
-         File outFile = File.createTempFile(prefix, suffix);
-         outFile.deleteOnExit();
-         try (OutputStream os = new FileOutputStream(outFile))
-         {
-           copyAll(jin, os);
-         }
-         String t = outFile.getAbsolutePath();
-         return t;
-       }
-       else
-       {
-         warn("Couldn't find entry in Jalview Jar for " + jarEntryName);
-       }
-     } catch (Exception ex)
-     {
-       ex.printStackTrace();
-     }
-     return null;
-   }
-   private class JvAnnotRow
-   {
-     public JvAnnotRow(int i, AlignmentAnnotation jaa)
-     {
-       order = i;
-       template = jaa;
-     }
-     /**
-      * persisted version of annotation row from which to take vis properties
-      */
-     public jalview.datamodel.AlignmentAnnotation template;
-     /**
-      * original position of the annotation row in the alignment
-      */
-     public int order;
-   }
-   /**
-    * Load alignment frame from jalview XML DOM object. For a DOM object that
-    * includes one or more Viewport elements (one with a title that does NOT
-    * contain "Dataset for"), create the frame.
-    * 
-    * @param jalviewModel
-    *          DOM
-    * @param fileName
-    *          filename source string
-    * @param file 
-    * @param loadTreesAndStructures
-    *          when false only create Viewport
-    * @param jprovider
-    *          data source provider
-    * @return alignment frame created from view stored in DOM
-    */
-   AlignFrame loadFromObject(JalviewModel jalviewModel, String fileName,
-           File file, boolean loadTreesAndStructures, jarInputStreamProvider jprovider)
-   {
-     SequenceSet vamsasSet = jalviewModel.getVamsasModel().getSequenceSet().get(0);
-     List<Sequence> vamsasSeqs = vamsasSet.getSequence();
-     // JalviewModelSequence jms = object.getJalviewModelSequence();
-     // Viewport view = (jms.getViewportCount() > 0) ? jms.getViewport(0)
-     // : null;
-     Viewport view = (jalviewModel.getViewport().size() > 0)
-             ? jalviewModel.getViewport().get(0)
-             : null;
-     // ////////////////////////////////
-     // INITIALISE ALIGNMENT SEQUENCESETID AND VIEWID
-     //
-     //
-     // If we just load in the same jar file again, the sequenceSetId
-     // will be the same, and we end up with multiple references
-     // to the same sequenceSet. We must modify this id on load
-     // so that each load of the file gives a unique id
-     /**
-      * used to resolve correct alignment dataset for alignments with multiple
-      * views
-      */
-     String uniqueSeqSetId = null;
-     String viewId = null;
-     if (view != null)
-     {
-       uniqueSeqSetId = view.getSequenceSetId() + uniqueSetSuffix;
-       viewId = (view.getId() == null ? null
-               : view.getId() + uniqueSetSuffix);
-     }
-     // ////////////////////////////////
-     // LOAD SEQUENCES
-     List<SequenceI> hiddenSeqs = null;
-     List<SequenceI> tmpseqs = new ArrayList<>();
-     boolean multipleView = false;
-     SequenceI referenceseqForView = null;
-     // JSeq[] jseqs = object.getJalviewModelSequence().getJSeq();
-     List<JSeq> jseqs = jalviewModel.getJSeq();
-     int vi = 0; // counter in vamsasSeq array
-     for (int i = 0; i < jseqs.size(); i++)
-     {
-       JSeq jseq = jseqs.get(i);
-       String seqId = jseq.getId();
-       SequenceI tmpSeq = seqRefIds.get(seqId);
-       if (tmpSeq != null)
-       {
-         if (!incompleteSeqs.containsKey(seqId))
-         {
-           // may not need this check, but keep it for at least 2.9,1 release
-           if (tmpSeq.getStart() != jseq.getStart()
-                   || tmpSeq.getEnd() != jseq.getEnd())
-           {
-             System.err.println(
-                     String.format("Warning JAL-2154 regression: updating start/end for sequence %s from %d/%d to %d/%d",
-                             tmpSeq.getName(), tmpSeq.getStart(),
-                             tmpSeq.getEnd(), jseq.getStart(),
-                             jseq.getEnd()));
-           }
-         }
-         else
-         {
-           incompleteSeqs.remove(seqId);
-         }
-         if (vamsasSeqs.size() > vi
-                 && vamsasSeqs.get(vi).getId().equals(seqId))
-         {
-           // most likely we are reading a dataset XML document so
-           // update from vamsasSeq section of XML for this sequence
-           tmpSeq.setName(vamsasSeqs.get(vi).getName());
-           tmpSeq.setDescription(vamsasSeqs.get(vi).getDescription());
-           tmpSeq.setSequence(vamsasSeqs.get(vi).getSequence());
-           vi++;
-         }
-         else
-         {
-           // reading multiple views, so vamsasSeq set is a subset of JSeq
-           multipleView = true;
-         }
-         tmpSeq.setStart(jseq.getStart());
-         tmpSeq.setEnd(jseq.getEnd());
-         tmpseqs.add(tmpSeq);
-       }
-       else
-       {
-         Sequence vamsasSeq = vamsasSeqs.get(vi);
-         tmpSeq = new jalview.datamodel.Sequence(vamsasSeq.getName(),
-                 vamsasSeq.getSequence());
-         tmpSeq.setDescription(vamsasSeq.getDescription());
-         tmpSeq.setStart(jseq.getStart());
-         tmpSeq.setEnd(jseq.getEnd());
-         tmpSeq.setVamsasId(uniqueSetSuffix + seqId);
-         seqRefIds.put(vamsasSeq.getId(), tmpSeq);
-         tmpseqs.add(tmpSeq);
-         vi++;
-       }
-       if (safeBoolean(jseq.isViewreference()))
-       {
-         referenceseqForView = tmpseqs.get(tmpseqs.size() - 1);
-       }
-       if (jseq.isHidden() != null && jseq.isHidden().booleanValue())
-       {
-         if (hiddenSeqs == null)
-         {
-           hiddenSeqs = new ArrayList<>();
-         }
-         hiddenSeqs.add(tmpSeq);
-       }
-     }
-     // /
-     // Create the alignment object from the sequence set
-     // ///////////////////////////////
-     SequenceI[] orderedSeqs = tmpseqs
-             .toArray(new SequenceI[tmpseqs.size()]);
-     AlignmentI al = null;
-     // so we must create or recover the dataset alignment before going further
-     // ///////////////////////////////
-     if (vamsasSet.getDatasetId() == null || vamsasSet.getDatasetId() == "")
-     {
-       // older jalview projects do not have a dataset - so creat alignment and
-       // dataset
-       al = new Alignment(orderedSeqs);
-       al.setDataset(null);
-     }
-     else
-     {
-       boolean isdsal = jalviewModel.getViewport().isEmpty();
-       if (isdsal)
-       {
-         // we are importing a dataset record, so
-         // recover reference to an alignment already materialsed as dataset
-         al = getDatasetFor(vamsasSet.getDatasetId());
-       }
-       if (al == null)
-       {
-         // materialse the alignment
-         al = new Alignment(orderedSeqs);
-       }
-       if (isdsal)
-       {
-         addDatasetRef(vamsasSet.getDatasetId(), al);
-       }
-       // finally, verify all data in vamsasSet is actually present in al
-       // passing on flag indicating if it is actually a stored dataset
-       recoverDatasetFor(vamsasSet, al, isdsal, uniqueSeqSetId);
-     }
-     if (referenceseqForView != null)
-     {
-       al.setSeqrep(referenceseqForView);
-     }
-     // / Add the alignment properties
-     for (int i = 0; i < vamsasSet.getSequenceSetProperties().size(); i++)
-     {
-       SequenceSetProperties ssp = vamsasSet.getSequenceSetProperties()
-               .get(i);
-       al.setProperty(ssp.getKey(), ssp.getValue());
-     }
-     // ///////////////////////////////
-     Hashtable pdbloaded = new Hashtable(); // TODO nothing writes to this??
-     if (!multipleView)
-     {
-       // load sequence features, database references and any associated PDB
-       // structures for the alignment
-       //
-       // prior to 2.10, this part would only be executed the first time a
-       // sequence was encountered, but not afterwards.
-       // now, for 2.10 projects, this is also done if the xml doc includes
-       // dataset sequences not actually present in any particular view.
-       //
-       for (int i = 0; i < vamsasSeqs.size(); i++)
-       {
-         JSeq jseq = jseqs.get(i);
-         if (jseq.getFeatures().size() > 0)
-         {
-           List<Feature> features = jseq.getFeatures();
-           for (int f = 0; f < features.size(); f++)
-           {
-             Feature feat = features.get(f);
-             SequenceFeature sf = new SequenceFeature(feat.getType(),
-                     feat.getDescription(), feat.getBegin(), feat.getEnd(),
-                     safeFloat(feat.getScore()), feat.getFeatureGroup());
-             sf.setStatus(feat.getStatus());
-             /*
-              * load any feature attributes - include map-valued attributes
-              */
-             Map<String, Map<String, String>> mapAttributes = new HashMap<>();
-             for (int od = 0; od < feat.getOtherData().size(); od++)
-             {
-               OtherData keyValue = feat.getOtherData().get(od);
-               String attributeName = keyValue.getKey();
-               String attributeValue = keyValue.getValue();
-               if (attributeName.startsWith("LINK"))
-               {
-                 sf.addLink(attributeValue);
-               }
-               else
-               {
-                 String subAttribute = keyValue.getKey2();
-                 if (subAttribute == null)
-                 {
-                   // simple string-valued attribute
-                   sf.setValue(attributeName, attributeValue);
-                 }
-                 else
-                 {
-                   // attribute 'key' has sub-attribute 'key2'
-                   if (!mapAttributes.containsKey(attributeName))
-                   {
-                     mapAttributes.put(attributeName, new HashMap<>());
-                   }
-                   mapAttributes.get(attributeName).put(subAttribute,
-                           attributeValue);
-                 }
-               }
-             }
-             for (Entry<String, Map<String, String>> mapAttribute : mapAttributes
-                     .entrySet())
-             {
-               sf.setValue(mapAttribute.getKey(), mapAttribute.getValue());
-             }
-             // adds feature to datasequence's feature set (since Jalview 2.10)
-             al.getSequenceAt(i).addSequenceFeature(sf);
-           }
-         }
-         if (vamsasSeqs.get(i).getDBRef().size() > 0)
-         {
-           // adds dbrefs to datasequence's set (since Jalview 2.10)
-           addDBRefs(
-                   al.getSequenceAt(i).getDatasetSequence() == null
-                           ? al.getSequenceAt(i)
-                           : al.getSequenceAt(i).getDatasetSequence(),
-                   vamsasSeqs.get(i));
-         }
-         if (jseq.getPdbids().size() > 0)
-         {
-           List<Pdbids> ids = jseq.getPdbids();
-           for (int p = 0; p < ids.size(); p++)
-           {
-             Pdbids pdbid = ids.get(p);
-             jalview.datamodel.PDBEntry entry = new jalview.datamodel.PDBEntry();
-             entry.setId(pdbid.getId());
-             if (pdbid.getType() != null)
-             {
-               if (PDBEntry.Type.getType(pdbid.getType()) != null)
-               {
-                 entry.setType(PDBEntry.Type.getType(pdbid.getType()));
-               }
-               else
-               {
-                 entry.setType(PDBEntry.Type.FILE);
-               }
-             }
-             // jprovider is null when executing 'New View'
-             if (pdbid.getFile() != null && jprovider != null)
-             {
-               if (!pdbloaded.containsKey(pdbid.getFile()))
-               {
-                 entry.setFile(loadPDBFile(jprovider, pdbid.getId(),
-                         pdbid.getFile()));
-               }
-               else
-               {
-                 entry.setFile(pdbloaded.get(pdbid.getId()).toString());
-               }
-             }
-             /*
-             if (pdbid.getPdbentryItem() != null)
-             {
-               for (PdbentryItem item : pdbid.getPdbentryItem())
-               {
-                 for (Property pr : item.getProperty())
-                 {
-                   entry.setProperty(pr.getName(), pr.getValue());
-                 }
-               }
-             }
-             */
-             for (Property prop : pdbid.getProperty())
-             {
-               entry.setProperty(prop.getName(), prop.getValue());
-             }
-             Desktop.getStructureSelectionManager()
-                     .registerPDBEntry(entry);
-             // adds PDBEntry to datasequence's set (since Jalview 2.10)
-             if (al.getSequenceAt(i).getDatasetSequence() != null)
-             {
-               al.getSequenceAt(i).getDatasetSequence().addPDBId(entry);
-             }
-             else
-             {
-               al.getSequenceAt(i).addPDBId(entry);
-             }
-           }
-         }
-         /*
-          * load any HMMER profile
-          */
-         // TODO fix this
-         String hmmJarFile = jseqs.get(i).getHmmerProfile();
-         if (hmmJarFile != null && jprovider != null)
-         {
-           loadHmmerProfile(jprovider, hmmJarFile, al.getSequenceAt(i));
-         }
-       }
-     } // end !multipleview
-     // ///////////////////////////////
-     // LOAD SEQUENCE MAPPINGS
-     if (vamsasSet.getAlcodonFrame().size() > 0)
-     {
-       // TODO Potentially this should only be done once for all views of an
-       // alignment
-       List<AlcodonFrame> alc = vamsasSet.getAlcodonFrame();
-       for (int i = 0; i < alc.size(); i++)
-       {
-         AlignedCodonFrame cf = new AlignedCodonFrame();
-         if (alc.get(i).getAlcodMap().size() > 0)
-         {
-           List<AlcodMap> maps = alc.get(i).getAlcodMap();
-           for (int m = 0; m < maps.size(); m++)
-           {
-             AlcodMap map = maps.get(m);
-             SequenceI dnaseq = seqRefIds.get(map.getDnasq());
-             // Load Mapping
-             jalview.datamodel.Mapping mapping = null;
-             // attach to dna sequence reference.
-             if (map.getMapping() != null)
-             {
-               mapping = addMapping(map.getMapping());
-               if (dnaseq != null && mapping.getTo() != null)
-               {
-                 cf.addMap(dnaseq, mapping.getTo(), mapping.getMap());
-               }
-               else
-               {
-                 // defer to later
-                 frefedSequence.add(
-                         newAlcodMapRef(map.getDnasq(), cf, mapping));
-               }
-             }
-           }
-           al.addCodonFrame(cf);
-         }
-       }
-     }
-     // ////////////////////////////////
-     // LOAD ANNOTATIONS
-     List<JvAnnotRow> autoAlan = new ArrayList<>();
-     /*
-      * store any annotations which forward reference a group's ID
-      */
-     Map<String, List<AlignmentAnnotation>> groupAnnotRefs = new Hashtable<>();
-     if (vamsasSet.getAnnotation().size()/*Count()*/ > 0)
-     {
-       List<Annotation> an = vamsasSet.getAnnotation();
-       for (int i = 0; i < an.size(); i++)
-       {
-         Annotation annotation = an.get(i);
-         /**
-          * test if annotation is automatically calculated for this view only
-          */
-         boolean autoForView = false;
-         if (annotation.getLabel().equals("Quality")
-                 || annotation.getLabel().equals("Conservation")
-                 || annotation.getLabel().equals("Consensus"))
-         {
-           // Kludge for pre 2.5 projects which lacked the autocalculated flag
-           autoForView = true;
-           // JAXB has no has() test; schema defaults value to false
-           // if (!annotation.hasAutoCalculated())
-           // {
-           // annotation.setAutoCalculated(true);
-           // }
-         }
-         if (autoForView || annotation.isAutoCalculated())
-         {
-           // remove ID - we don't recover annotation from other views for
-           // view-specific annotation
-           annotation.setId(null);
-         }
-         // set visibility for other annotation in this view
-         String annotationId = annotation.getId();
-         if (annotationId != null && annotationIds.containsKey(annotationId))
-         {
-           AlignmentAnnotation jda = annotationIds.get(annotationId);
-           // in principle Visible should always be true for annotation displayed
-           // in multiple views
-           if (annotation.isVisible() != null)
-           {
-             jda.visible = annotation.isVisible();
-           }
-           al.addAnnotation(jda);
-           continue;
-         }
-         // Construct new annotation from model.
-         List<AnnotationElement> ae = annotation.getAnnotationElement();
-         jalview.datamodel.Annotation[] anot = null;
-         java.awt.Color firstColour = null;
-         int anpos;
-         if (!annotation.isScoreOnly())
-         {
-           anot = new jalview.datamodel.Annotation[al.getWidth()];
-           for (int aa = 0; aa < ae.size() && aa < anot.length; aa++)
-           {
-             AnnotationElement annElement = ae.get(aa);
-             anpos = annElement.getPosition();
-             if (anpos >= anot.length)
-             {
-               continue;
-             }
-             float value = safeFloat(annElement.getValue());
-             anot[anpos] = new jalview.datamodel.Annotation(
-                     annElement.getDisplayCharacter(),
-                     annElement.getDescription(),
-                     (annElement.getSecondaryStructure() == null
-                             || annElement.getSecondaryStructure()
-                                     .length() == 0)
-                                             ? ' '
-                                             : annElement
-                                                     .getSecondaryStructure()
-                                                     .charAt(0),
-                     value);
-             anot[anpos].colour = new Color(safeInt(annElement.getColour()));
-             if (firstColour == null)
-             {
-               firstColour = anot[anpos].colour;
-             }
-           }
-         }
-         // create the new AlignmentAnnotation
-         jalview.datamodel.AlignmentAnnotation jaa = null;
-         if (annotation.isGraph())
-         {
-           float llim = 0, hlim = 0;
-           // if (autoForView || an[i].isAutoCalculated()) {
-           // hlim=11f;
-           // }
-           jaa = new jalview.datamodel.AlignmentAnnotation(
-                   annotation.getLabel(), annotation.getDescription(), anot,
-                   llim, hlim, safeInt(annotation.getGraphType()));
-           jaa.graphGroup = safeInt(annotation.getGraphGroup());
-           jaa._linecolour = firstColour;
-           if (annotation.getThresholdLine() != null)
-           {
-             jaa.setThreshold(new jalview.datamodel.GraphLine(
-                     safeFloat(annotation.getThresholdLine().getValue()),
-                     annotation.getThresholdLine().getLabel(),
-                     new java.awt.Color(safeInt(
-                             annotation.getThresholdLine().getColour()))));
-           }
-           if (autoForView || annotation.isAutoCalculated())
-           {
-             // Hardwire the symbol display line to ensure that labels for
-             // histograms are displayed
-             jaa.hasText = true;
-           }
-         }
-         else
-         {
-           jaa = new jalview.datamodel.AlignmentAnnotation(
-                   annotation.getLabel(), annotation.getDescription(), anot);
-           jaa._linecolour = firstColour;
-         }
-         // register new annotation
-         // Annotation graphs such as Conservation will not have id.
-         if (annotation.getId() != null)
-         {
-           annotationIds.put(annotation.getId(), jaa);
-           jaa.annotationId = annotation.getId();
-         }
-         // recover sequence association
-         String sequenceRef = annotation.getSequenceRef();
-         if (sequenceRef != null)
-         {
-           // from 2.9 sequenceRef is to sequence id (JAL-1781)
-           SequenceI sequence = seqRefIds.get(sequenceRef);
-           if (sequence == null)
-           {
-             // in pre-2.9 projects sequence ref is to sequence name
-             sequence = al.findName(sequenceRef);
-           }
-           if (sequence != null)
-           {
-             jaa.createSequenceMapping(sequence, 1, true);
-             sequence.addAlignmentAnnotation(jaa);
-           }
-         }
-         // and make a note of any group association
-         if (annotation.getGroupRef() != null
-                 && annotation.getGroupRef().length() > 0)
-         {
-           List<jalview.datamodel.AlignmentAnnotation> aal = groupAnnotRefs
-                   .get(annotation.getGroupRef());
-           if (aal == null)
-           {
-             aal = new ArrayList<>();
-             groupAnnotRefs.put(annotation.getGroupRef(), aal);
-           }
-           aal.add(jaa);
-         }
-         if (annotation.getScore() != null)
-         {
-           jaa.setScore(annotation.getScore().doubleValue());
-         }
-         if (annotation.isVisible() != null)
-         {
-           jaa.visible = annotation.isVisible().booleanValue();
-         }
-         if (annotation.isCentreColLabels() != null)
-         {
-           jaa.centreColLabels = annotation.isCentreColLabels()
-                   .booleanValue();
-         }
-         if (annotation.isScaleColLabels() != null)
-         {
-           jaa.scaleColLabel = annotation.isScaleColLabels().booleanValue();
-         }
-         if (annotation.isAutoCalculated())
-         {
-           // newer files have an 'autoCalculated' flag and store calculation
-           // state in viewport properties
-           jaa.autoCalculated = true; // means annotation will be marked for
-           // update at end of load.
-         }
-         if (annotation.getGraphHeight() != null)
-         {
-           jaa.graphHeight = annotation.getGraphHeight().intValue();
-         }
-         jaa.belowAlignment = annotation.isBelowAlignment();
-         jaa.setCalcId(annotation.getCalcId());
-         if (annotation.getProperty().size() > 0)
-         {
-           for (Annotation.Property prop : annotation
-                   .getProperty())
-           {
-             jaa.setProperty(prop.getName(), prop.getValue());
-           }
-         }
-         if (jaa.autoCalculated)
-         {
-           autoAlan.add(new JvAnnotRow(i, jaa));
-         }
-         else
-         // if (!autoForView)
-         {
-           // add autocalculated group annotation and any user created annotation
-           // for the view
-           al.addAnnotation(jaa);
-         }
-       }
-     }
-     // ///////////////////////
-     // LOAD GROUPS
-     // Create alignment markup and styles for this view
-     if (jalviewModel.getJGroup().size() > 0)
-     {
-       List<JGroup> groups = jalviewModel.getJGroup();
-       boolean addAnnotSchemeGroup = false;
-       for (int i = 0; i < groups.size(); i++)
-       {
-         JGroup jGroup = groups.get(i);
-         ColourSchemeI cs = null;
-         if (jGroup.getColour() != null)
-         {
-           if (jGroup.getColour().startsWith("ucs"))
-           {
-             cs = getUserColourScheme(jalviewModel, jGroup.getColour());
-           }
-           else if (jGroup.getColour().equals("AnnotationColourGradient")
-                   && jGroup.getAnnotationColours() != null)
-           {
-             addAnnotSchemeGroup = true;
-           }
-           else
-           {
-             cs = ColourSchemeProperty.getColourScheme(null, al,
-                     jGroup.getColour());
-           }
-         }
-         int pidThreshold = safeInt(jGroup.getPidThreshold());
-         Vector<SequenceI> seqs = new Vector<>();
-         for (int s = 0; s < jGroup.getSeq().size(); s++)
-         {
-           String seqId = jGroup.getSeq().get(s);
-           SequenceI ts = seqRefIds.get(seqId);
-           if (ts != null)
-           {
-             seqs.addElement(ts);
-           }
-         }
-         if (seqs.size() < 1)
-         {
-           continue;
-         }
-         SequenceGroup sg = new SequenceGroup(seqs, jGroup.getName(), cs,
-                 safeBoolean(jGroup.isDisplayBoxes()),
-                 safeBoolean(jGroup.isDisplayText()),
-                 safeBoolean(jGroup.isColourText()),
-                 safeInt(jGroup.getStart()), safeInt(jGroup.getEnd()));
-         sg.getGroupColourScheme().setThreshold(pidThreshold, true);
-         sg.getGroupColourScheme()
-                 .setConservationInc(safeInt(jGroup.getConsThreshold()));
-         sg.setOutlineColour(new Color(safeInt(jGroup.getOutlineColour())));
-         sg.textColour = new Color(safeInt(jGroup.getTextCol1()));
-         sg.textColour2 = new Color(safeInt(jGroup.getTextCol2()));
-         sg.setShowNonconserved(safeBoolean(jGroup.isShowUnconserved()));
-         sg.thresholdTextColour = safeInt(jGroup.getTextColThreshold());
-         // attributes with a default in the schema are never null
-         sg.setShowConsensusHistogram(jGroup.isShowConsensusHistogram());
-         sg.setshowSequenceLogo(jGroup.isShowSequenceLogo());
-         sg.setNormaliseSequenceLogo(jGroup.isNormaliseSequenceLogo());
-         sg.setIgnoreGapsConsensus(jGroup.isIgnoreGapsinConsensus());
-         if (jGroup.getConsThreshold() != null
-                 && jGroup.getConsThreshold().intValue() != 0)
-         {
-           Conservation c = new Conservation("All", sg.getSequences(null), 0,
-                   sg.getWidth() - 1);
-           c.calculate();
-           c.verdict(false, 25);
-           sg.cs.setConservation(c);
-         }
-         if (jGroup.getId() != null && groupAnnotRefs.size() > 0)
-         {
-           // re-instate unique group/annotation row reference
-           List<AlignmentAnnotation> jaal = groupAnnotRefs
-                   .get(jGroup.getId());
-           if (jaal != null)
-           {
-             for (AlignmentAnnotation jaa : jaal)
-             {
-               jaa.groupRef = sg;
-               if (jaa.autoCalculated)
-               {
-                 // match up and try to set group autocalc alignment row for this
-                 // annotation
-                 if (jaa.label.startsWith("Consensus for "))
-                 {
-                   sg.setConsensus(jaa);
-                 }
-                 // match up and try to set group autocalc alignment row for this
-                 // annotation
-                 if (jaa.label.startsWith("Conservation for "))
-                 {
-                   sg.setConservationRow(jaa);
-                 }
-               }
-             }
-           }
-         }
-         al.addGroup(sg);
-         if (addAnnotSchemeGroup)
-         {
-           // reconstruct the annotation colourscheme
-           sg.setColourScheme(
-                   constructAnnotationColour(jGroup.getAnnotationColours(),
-                           null, al, jalviewModel, false));
-         }
-       }
-     }
-     if (view == null)
-     {
-       // only dataset in this model, so just return.
-       return null;
-     }
-     // ///////////////////////////////
-     // LOAD VIEWPORT
-     // now check to see if we really need to create a new viewport.
-     if (multipleView && viewportsAdded.size() == 0)
-     {
-       // We recovered an alignment for which a viewport already exists.
-       // TODO: fix up any settings necessary for overlaying stored state onto
-       // state recovered from another document. (may not be necessary).
-       // we may need a binding from a viewport in memory to one recovered from
-       // XML.
-       // and then recover its containing af to allow the settings to be applied.
-       // TODO: fix for vamsas demo
-       System.err.println(
-               "About to recover a viewport for existing alignment: Sequence set ID is "
-                       + uniqueSeqSetId);
-       Object seqsetobj = retrieveExistingObj(uniqueSeqSetId);
-       if (seqsetobj != null)
-       {
-         if (seqsetobj instanceof String)
-         {
-           uniqueSeqSetId = (String) seqsetobj;
-           System.err.println(
-                   "Recovered extant sequence set ID mapping for ID : New Sequence set ID is "
-                           + uniqueSeqSetId);
-         }
-         else
-         {
-           System.err.println(
-                   "Warning : Collision between sequence set ID string and existing jalview object mapping.");
-         }
-       }
-     }
-     /**
-      * indicate that annotation colours are applied across all groups (pre
-      * Jalview 2.8.1 behaviour)
-      */
-     boolean doGroupAnnColour = Jalview2XML.isVersionStringLaterThan("2.8.1",
-             jalviewModel.getVersion());
-     AlignFrame af = null;
-     AlignmentPanel ap = null;
-     AlignViewport av = null;
-     if (viewId != null)
-     {
-       // Check to see if this alignment already has a view id == viewId
-       jalview.gui.AlignmentPanel views[] = Desktop
-               .getAlignmentPanels(uniqueSeqSetId);
-       if (views != null && views.length > 0)
-       {
-         for (int v = 0; v < views.length; v++)
-         {
-           ap = views[v];
-           av = ap.av;
-           if (av.getViewId().equalsIgnoreCase(viewId))
-           {
-             // recover the existing alignpanel, alignframe, viewport
-             af = ap.alignFrame;
-             break;
-             // TODO: could even skip resetting view settings if we don't want to
-             // change the local settings from other jalview processes
-           }
-         }
-       }
-     }
-     if (af == null)
-     {
-       af = loadViewport(fileName, file, jseqs, hiddenSeqs, al, jalviewModel, view,
-               uniqueSeqSetId, viewId, autoAlan);
-       av = af.getViewport();
-       // note that this only retrieves the most recently accessed
-       // tab of an AlignFrame.
-       ap = af.alignPanel;
-     }
-     /*
-      * Load any trees, PDB structures and viewers
-      * 
-      * Not done if flag is false (when this method is used for New View)
-      */
-     final AlignFrame af0 = af;
-     final AlignViewport av0 = av;
-     final AlignmentPanel ap0 = ap;
- //    Platform.timeCheck("Jalview2XML.loadFromObject-beforetree",
- //            Platform.TIME_MARK);
-     if (loadTreesAndStructures)
-     {
-       if (!jalviewModel.getTree().isEmpty())
-       {
-         SwingUtilities.invokeLater(new Runnable()
-         {
-           @Override
-           public void run()
-           {
- //            Platform.timeCheck(null, Platform.TIME_MARK);
-             loadTrees(jalviewModel, view, af0, av0, ap0);
- //            Platform.timeCheck("Jalview2XML.loadTrees", Platform.TIME_MARK);
-           }
-         });
-       }
-       if (!jalviewModel.getPcaViewer().isEmpty())
-       {
-         SwingUtilities.invokeLater(new Runnable()
-         {
-           @Override
-           public void run()
-           {
- //            Platform.timeCheck(null, Platform.TIME_MARK);
-             loadPCAViewers(jalviewModel, ap0);
- //            Platform.timeCheck("Jalview2XML.loadPCA", Platform.TIME_MARK);
-           }
-         });
-       }
-       SwingUtilities.invokeLater(new Runnable()
-       {
-         @Override
-         public void run()
-         {
- //          Platform.timeCheck(null, Platform.TIME_MARK);
-           loadPDBStructures(jprovider, jseqs, af0, ap0);
- //          Platform.timeCheck("Jalview2XML.loadPDB", Platform.TIME_MARK);
-         }
-       });
-       SwingUtilities.invokeLater(new Runnable()
-       {
-         @Override
-         public void run()
-         {
-           loadRnaViewers(jprovider, jseqs, ap0);
-         }
-       });
-     }
-     // and finally return.
-     // but do not set holdRepaint true just yet, because this could be the
-     // initial frame with just its dataset.
-     return af;
-   }
-   /**
-    * Loads a HMMER profile from a file stored in the project, and associates it
-    * with the specified sequence
-    * 
-    * @param jprovider
-    * @param hmmJarFile
-    * @param seq
-    */
-   protected void loadHmmerProfile(jarInputStreamProvider jprovider,
-           String hmmJarFile, SequenceI seq)
-   {
-     try
-     {
-       String hmmFile = copyJarEntry(jprovider, hmmJarFile, "hmm", null);
-       HMMFile parser = new HMMFile(hmmFile, DataSourceType.FILE);
-       HiddenMarkovModel hmmModel = parser.getHMM();
-       hmmModel = new HiddenMarkovModel(hmmModel, seq);
-       seq.setHMM(hmmModel);
-     } catch (IOException e)
-     {
-       warn("Error loading HMM profile for " + seq.getName() + ": "
-               + e.getMessage());
-     }
-   }
-   /**
-    * Instantiate and link any saved RNA (Varna) viewers. The state of the Varna
-    * panel is restored from separate jar entries, two (gapped and trimmed) per
-    * sequence and secondary structure.
-    * 
-    * Currently each viewer shows just one sequence and structure (gapped and
-    * trimmed), however this method is designed to support multiple sequences or
-    * structures in viewers if wanted in future.
-    * 
-    * @param jprovider
-    * @param jseqs
-    * @param ap
-    */
-   protected void loadRnaViewers(jarInputStreamProvider jprovider,
-           List<JSeq> jseqs, AlignmentPanel ap)
-   {
-     /*
-      * scan the sequences for references to viewers; create each one the first
-      * time it is referenced, add Rna models to existing viewers
-      */
-     for (JSeq jseq : jseqs)
-     {
-       for (int i = 0; i < jseq.getRnaViewer().size(); i++)
-       {
-         RnaViewer viewer = jseq.getRnaViewer().get(i);
-         AppVarna appVarna = findOrCreateVarnaViewer(viewer, uniqueSetSuffix,
-                 ap);
-         for (int j = 0; j < viewer.getSecondaryStructure().size(); j++)
-         {
-           SecondaryStructure ss = viewer.getSecondaryStructure().get(j);
-           SequenceI seq = seqRefIds.get(jseq.getId());
-           AlignmentAnnotation ann = this.annotationIds
-                   .get(ss.getAnnotationId());
-           /*
-            * add the structure to the Varna display (with session state copied
-            * from the jar to a temporary file)
-            */
-           boolean gapped = safeBoolean(ss.isGapped());
-           String rnaTitle = ss.getTitle();
-           String sessionState = ss.getViewerState();
-           String tempStateFile = copyJarEntry(jprovider, sessionState,
-                   "varna", null);
-           RnaModel rna = new RnaModel(rnaTitle, ann, seq, null, gapped);
-           appVarna.addModelSession(rna, rnaTitle, tempStateFile);
-         }
-         appVarna.setInitialSelection(safeInt(viewer.getSelectedRna()));
-       }
-     }
-   }
-   /**
-    * Locate and return an already instantiated matching AppVarna, or create one
-    * if not found
-    * 
-    * @param viewer
-    * @param viewIdSuffix
-    * @param ap
-    * @return
-    */
-   protected AppVarna findOrCreateVarnaViewer(RnaViewer viewer,
-           String viewIdSuffix, AlignmentPanel ap)
-   {
-     /*
-      * on each load a suffix is appended to the saved viewId, to avoid conflicts
-      * if load is repeated
-      */
-     String postLoadId = viewer.getViewId() + viewIdSuffix;
-     for (JInternalFrame frame : getAllFrames())
-     {
-       if (frame instanceof AppVarna)
-       {
-         AppVarna varna = (AppVarna) frame;
-         if (postLoadId.equals(varna.getViewId()))
-         {
-           // this viewer is already instantiated
-           // could in future here add ap as another 'parent' of the
-           // AppVarna window; currently just 1-to-many
-           return varna;
-         }
-       }
-     }
-     /*
-      * viewer not found - make it
-      */
-     RnaViewerModel model = new RnaViewerModel(postLoadId, viewer.getTitle(),
-             safeInt(viewer.getXpos()), safeInt(viewer.getYpos()),
-             safeInt(viewer.getWidth()), safeInt(viewer.getHeight()),
-             safeInt(viewer.getDividerLocation()));
-     AppVarna varna = new AppVarna(model, ap);
-     return varna;
-   }
-   /**
-    * Load any saved trees
-    * 
-    * @param jm
-    * @param view
-    * @param af
-    * @param av
-    * @param ap
-    */
-   protected void loadTrees(JalviewModel jm, Viewport view,
-           AlignFrame af, AlignViewport av, AlignmentPanel ap)
-   {
-     // TODO result of automated refactoring - are all these parameters needed?
-     try
-     {
-       for (int t = 0; t < jm.getTree().size(); t++)
-       {
-         Tree tree = jm.getTree().get(t);
-         TreePanel tp = (TreePanel) retrieveExistingObj(tree.getId());
-         if (tp == null)
-         {
-           tp = af.showNewickTree(new NewickFile(tree.getNewick()),
-                   tree.getTitle(), safeInt(tree.getWidth()),
-                   safeInt(tree.getHeight()), safeInt(tree.getXpos()),
-                   safeInt(tree.getYpos()));
-           if (tp == null)
-           {
-             warn("There was a problem recovering stored Newick tree: \n"
-                     + tree.getNewick());
-             continue;
-           }
-           if (tree.getId() != null)
-           {
-             // perhaps bind the tree id to something ?
-           }
-         }
-         else
-         {
-           // update local tree attributes ?
-           // TODO: should check if tp has been manipulated by user - if so its
-           // settings shouldn't be modified
-           tp.setTitle(tree.getTitle());
-           tp.setBounds(new Rectangle(safeInt(tree.getXpos()),
-                   safeInt(tree.getYpos()), safeInt(tree.getWidth()),
-                   safeInt(tree.getHeight())));
-           tp.setViewport(av); // af.viewport;
-           // TODO: verify 'associate with all views' works still
-           tp.getTreeCanvas().setViewport(av); // af.viewport;
-           tp.getTreeCanvas().setAssociatedPanel(ap); // af.alignPanel;
-         }
-         tp.getTreeCanvas().setApplyToAllViews(tree.isLinkToAllViews());
-         tp.fitToWindow.setState(safeBoolean(tree.isFitToWindow()));
-         tp.fitToWindow_actionPerformed(null);
-         if (tree.getFontName() != null)
-         {
-           tp.setTreeFont(
-                   new Font(tree.getFontName(), safeInt(tree.getFontStyle()),
-                           safeInt(tree.getFontSize())));
-         }
-         else
-         {
-           tp.setTreeFont(
-                   new Font(view.getFontName(), safeInt(view.getFontStyle()),
-                           safeInt(view.getFontSize())));
-         }
-         tp.showPlaceholders(safeBoolean(tree.isMarkUnlinked()));
-         tp.showBootstrap(safeBoolean(tree.isShowBootstrap()));
-         tp.showDistances(safeBoolean(tree.isShowDistances()));
-         tp.getTreeCanvas().setThreshold(safeFloat(tree.getThreshold()));
-         if (safeBoolean(tree.isCurrentTree()))
-         {
-           af.getViewport().setCurrentTree(tp.getTree());
-         }
-       }
-     } catch (Exception ex)
-     {
-       ex.printStackTrace();
-     }
-   }
-   /**
-    * Load and link any saved structure viewers.
-    * 
-    * @param jprovider
-    * @param jseqs
-    * @param af
-    * @param ap
-    */
-   protected void loadPDBStructures(jarInputStreamProvider jprovider,
-           List<JSeq> jseqs, AlignFrame af, AlignmentPanel ap)
-   {
-     /*
-      * Run through all PDB ids on the alignment, and collect mappings between
-      * distinct view ids and all sequences referring to that view.
-      */
-     Map<String, StructureViewerModel> structureViewers = new LinkedHashMap<>();
-     for (int i = 0; i < jseqs.size(); i++)
-     {
-       JSeq jseq = jseqs.get(i);
-       if (jseq.getPdbids().size() > 0)
-       {
-         List<Pdbids> ids = jseq.getPdbids();
-         for (int p = 0; p < ids.size(); p++)
-         {
-           Pdbids pdbid = ids.get(p);
-           final int structureStateCount = pdbid.getStructureState().size();
-           for (int s = 0; s < structureStateCount; s++)
-           {
-             // check to see if we haven't already created this structure view
-             final StructureState structureState = pdbid
-                     .getStructureState().get(s);
-             String sviewid = (structureState.getViewId() == null) ? null
-                     : structureState.getViewId() + uniqueSetSuffix;
-             jalview.datamodel.PDBEntry jpdb = new jalview.datamodel.PDBEntry();
-             // Originally : pdbid.getFile()
-             // : TODO: verify external PDB file recovery still works in normal
-             // jalview project load
-             jpdb.setFile(
-                     loadPDBFile(jprovider, pdbid.getId(), pdbid.getFile()));
-             jpdb.setId(pdbid.getId());
-             int x = safeInt(structureState.getXpos());
-             int y = safeInt(structureState.getYpos());
-             int width = safeInt(structureState.getWidth());
-             int height = safeInt(structureState.getHeight());
-             // Probably don't need to do this anymore...
-             // Desktop.getDesktop().getComponentAt(x, y);
-             // TODO: NOW: check that this recovers the PDB file correctly.
-             String pdbFile = loadPDBFile(jprovider, pdbid.getId(),
-                     pdbid.getFile());
-             jalview.datamodel.SequenceI seq = seqRefIds
-                     .get(jseq.getId() + "");
-             if (sviewid == null)
-             {
-               sviewid = "_jalview_pre2_4_" + x + "," + y + "," + width + ","
-                       + height;
-             }
-             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(),
-                               viewerType));
-               // Legacy pre-2.7 conversion JAL-823 :
-               // do not assume any view has to be linked for colour by
-               // sequence
-             }
-             // assemble String[] { pdb files }, String[] { id for each
-             // file }, orig_fileloc, SequenceI[][] {{ seqs_file 1 }, {
-             // seqs_file 2}, boolean[] {
-             // linkAlignPanel,superposeWithAlignpanel}} from hash
-             StructureViewerModel jmoldat = structureViewers.get(sviewid);
-             jmoldat.setAlignWithPanel(jmoldat.isAlignWithPanel()
-                     || structureState.isAlignwithAlignPanel());
-             /*
-              * Default colour by linked panel to false if not specified (e.g.
-              * for pre-2.7 projects)
-              */
-             boolean colourWithAlignPanel = jmoldat.isColourWithAlignPanel();
-             colourWithAlignPanel |= structureState.isColourwithAlignPanel();
-             jmoldat.setColourWithAlignPanel(colourWithAlignPanel);
-             /*
-              * Default colour by viewer to true if not specified (e.g. for
-              * pre-2.7 projects)
-              */
-             boolean colourByViewer = jmoldat.isColourByViewer();
-             colourByViewer &= structureState.isColourByJmol();
-             jmoldat.setColourByViewer(colourByViewer);
-             if (jmoldat.getStateData().length() < structureState
-                     .getValue()/*Content()*/.length())
-             {
-               jmoldat.setStateData(structureState.getValue());// Content());
-             }
-             if (pdbid.getFile() != null)
-             {
-               File mapkey = new File(pdbid.getFile());
-               StructureData seqstrmaps = jmoldat.getFileData().get(mapkey);
-               if (seqstrmaps == null)
-               {
-                 jmoldat.getFileData().put(mapkey,
-                         seqstrmaps = jmoldat.new StructureData(pdbFile,
-                                 pdbid.getId()));
-               }
-               if (!seqstrmaps.getSeqList().contains(seq))
-               {
-                 seqstrmaps.getSeqList().add(seq);
-                 // TODO and chains?
-               }
-             }
-             else
-             {
-               errorMessage = ("The Jmol views in this project were imported\nfrom an older version of Jalview.\nPlease review the sequence colour associations\nin the Colour by section of the Jmol View menu.\n\nIn the case of problems, see note at\nhttp://issues.jalview.org/browse/JAL-747");
-               warn(errorMessage);
-             }
-           }
-         }
-       }
-     }
-     // Instantiate the associated structure views
-     for (Entry<String, StructureViewerModel> entry : structureViewers
-             .entrySet())
-     {
-       try
-       {
-         createOrLinkStructureViewer(entry, af, ap, jprovider);
-       } catch (Exception e)
-       {
-         System.err.println(
-                 "Error loading structure viewer: " + e.getMessage());
-         // failed - try the next one
-       }
-     }
-   }
-   /**
-    * 
-    * @param viewerData
-    * @param af
-    * @param ap
-    * @param jprovider
-    */
-   protected void createOrLinkStructureViewer(
-           Entry<String, StructureViewerModel> viewerData, AlignFrame af,
-           AlignmentPanel ap, jarInputStreamProvider jprovider)
-   {
-     final StructureViewerModel stateData = viewerData.getValue();
-     /*
-      * Search for any viewer windows already open from other alignment views
-      * that exactly match the stored structure state
-      */
-     StructureViewerBase comp = findMatchingViewer(viewerData);
-     if (comp != null)
-     {
-       linkStructureViewer(ap, comp, stateData);
-       return;
-     }
-     String type = stateData.getType();
-     try
-     {
-       ViewerType viewerType = ViewerType.valueOf(type);
-       createStructureViewer(viewerType, viewerData, af, jprovider);
-     } catch (IllegalArgumentException | NullPointerException e)
-     {
-       // TODO JAL-3619 show error dialog / offer an alternative viewer
-       Cache.log.error("Invalid structure viewer type: " + type);
-     }
-   }
-   /**
-    * 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();
- // BH again was invokeAndWait
-     // try
-     // {
-       javax.swing.SwingUtilities.invokeLater(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;
-   }
-   /**
-    * Generates a name for the entry in the project jar file to hold state
-    * information for a structure viewer
-    * 
-    * @param viewId
-    * @return
-    */
-   protected String getViewerJarEntryName(String viewId)
-   {
-     return VIEWER_PREFIX + viewId;
-   }
-   /**
-    * Returns any open frame that matches given structure viewer data. The match
-    * is based on the unique viewId, or (for older project versions) the frame's
-    * geometry.
-    * 
-    * @param viewerData
-    * @return
-    */
-   protected StructureViewerBase findMatchingViewer(
-           Entry<String, StructureViewerModel> viewerData)
-   {
-     final String sviewid = viewerData.getKey();
-     final StructureViewerModel svattrib = viewerData.getValue();
-     StructureViewerBase comp = null;
-     JInternalFrame[] frames = getAllFrames();
-     for (JInternalFrame frame : frames)
-     {
-       if (frame instanceof StructureViewerBase)
-       {
-         /*
-          * Post jalview 2.4 schema includes structure view id
-          */
-         if (sviewid != null && ((StructureViewerBase) frame).getViewId()
-                 .equals(sviewid))
-         {
-           comp = (StructureViewerBase) frame;
-           break; // break added in 2.9
-         }
-         /*
-          * Otherwise test for matching position and size of viewer frame
-          */
-         else if (frame.getX() == svattrib.getX()
-                 && frame.getY() == svattrib.getY()
-                 && frame.getHeight() == svattrib.getHeight()
-                 && frame.getWidth() == svattrib.getWidth())
-         {
-           comp = (StructureViewerBase) frame;
-           // no break in faint hope of an exact match on viewId
-         }
-       }
-     }
-     return comp;
-   }
-   /**
-    * Link an AlignmentPanel to an existing structure viewer.
-    * 
-    * @param ap
-    * @param viewer
-    * @param oldFiles
-    * @param useinViewerSuperpos
-    * @param usetoColourbyseq
-    * @param viewerColouring
-    */
-   protected void linkStructureViewer(AlignmentPanel ap,
-           StructureViewerBase viewer, StructureViewerModel stateData)
-   {
-     // NOTE: if the jalview project is part of a shared session then
-     // view synchronization should/could be done here.
-     final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
-     final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
-     final boolean viewerColouring = stateData.isColourByViewer();
-     Map<File, StructureData> oldFiles = stateData.getFileData();
-     /*
-      * Add mapping for sequences in this view to an already open viewer
-      */
-     final AAStructureBindingModel binding = viewer.getBinding();
-     for (File id : oldFiles.keySet())
-     {
-       // add this and any other pdb files that should be present in the
-       // viewer
-       StructureData filedat = oldFiles.get(id);
-       String pdbFile = filedat.getFilePath();
-       SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
-       binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
-               null);
-       binding.addSequenceForStructFile(pdbFile, seq);
-     }
-     // and add the AlignmentPanel's reference to the view panel
-     viewer.addAlignmentPanel(ap);
-     if (useinViewerSuperpos)
-     {
-       viewer.useAlignmentPanelForSuperposition(ap);
-     }
-     else
++     * Generates a name for the entry in the project jar file to hold state
++     * information for a structure viewer
++     * 
++     * @param viewId
++     * @return
++     */
++    protected String getViewerJarEntryName(String viewId)
      {
--      viewer.excludeAlignmentPanelForSuperposition(ap);
++      return VIEWER_PREFIX + viewId;
      }
--    if (usetoColourbyseq)
--    {
--      viewer.useAlignmentPanelForColourbyseq(ap, !viewerColouring);
++
++    /**
++     * Returns any open frame that matches given structure viewer data. The match
++     * is based on the unique viewId, or (for older project versions) the frame's
++     * geometry.
++     * 
++     * @param viewerData
++     * @return
++     */
++    protected StructureViewerBase findMatchingViewer(
++                                                   Entry<String, StructureViewerModel> viewerData)
++    {
++      final String sviewid = viewerData.getKey();
++      final StructureViewerModel svattrib = viewerData.getValue();
++      StructureViewerBase comp = null;
++      JInternalFrame[] frames = getAllFrames();
++      for (JInternalFrame frame : frames)
++          {
++              if (frame instanceof StructureViewerBase)
++                  {
++                      /*
++                       * Post jalview 2.4 schema includes structure view id
++                       */
++                      if (sviewid != null && ((StructureViewerBase) frame).getViewId()
++                          .equals(sviewid))
++                          {
++                              comp = (StructureViewerBase) frame;
++                              break; // break added in 2.9
++                          }
++                      /*
++                       * Otherwise test for matching position and size of viewer frame
++                       */
++                      else if (frame.getX() == svattrib.getX()
++                               && frame.getY() == svattrib.getY()
++                               && frame.getHeight() == svattrib.getHeight()
++                               && frame.getWidth() == svattrib.getWidth())
++                          {
++                              comp = (StructureViewerBase) frame;
++                              // no break in faint hope of an exact match on viewId
++                          }
++                  }
++          }
++      return comp;
      }
--    else
--    {
--      viewer.excludeAlignmentPanelForColourbyseq(ap);
++
++    /**
++     * Link an AlignmentPanel to an existing structure viewer.
++     * 
++     * @param ap
++     * @param viewer
++     * @param oldFiles
++     * @param useinViewerSuperpos
++     * @param usetoColourbyseq
++     * @param viewerColouring
++     */
++    protected void linkStructureViewer(AlignmentPanel ap,
++                                     StructureViewerBase viewer, StructureViewerModel stateData)
++    {
++      // NOTE: if the jalview project is part of a shared session then
++      // view synchronization should/could be done here.
++
++      final boolean useinViewerSuperpos = stateData.isAlignWithPanel();
++      final boolean usetoColourbyseq = stateData.isColourWithAlignPanel();
++      final boolean viewerColouring = stateData.isColourByViewer();
++      Map<File, StructureData> oldFiles = stateData.getFileData();
++
++      /*
++       * Add mapping for sequences in this view to an already open viewer
++       */
++      final AAStructureBindingModel binding = viewer.getBinding();
++      for (File id : oldFiles.keySet())
++          {
++              // add this and any other pdb files that should be present in the
++              // viewer
++              StructureData filedat = oldFiles.get(id);
++              String pdbFile = filedat.getFilePath();
++              SequenceI[] seq = filedat.getSeqList().toArray(new SequenceI[0]);
++              binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
++                                          null);
++              binding.addSequenceForStructFile(pdbFile, seq);
++          }
++      // and add the AlignmentPanel's reference to the view panel
++      viewer.addAlignmentPanel(ap);
++      if (useinViewerSuperpos)
++          {
++              viewer.useAlignmentPanelForSuperposition(ap);
++          }
++      else
++          {
++              viewer.excludeAlignmentPanelForSuperposition(ap);
++          }
++      if (usetoColourbyseq)
++          {
++              viewer.useAlignmentPanelForColourbyseq(ap, !viewerColouring);
++          }
++      else
++          {
++              viewer.excludeAlignmentPanelForColourbyseq(ap);
++          }
      }
--  }
  
--  /**
--   * Get all frames within the Desktop.
--   * 
--   * @return
--   */
--  protected JInternalFrame[] getAllFrames()
--  {
--    JInternalFrame[] frames = null;
--    // TODO is this necessary - is it safe - risk of hanging?
--    do
--    {
--      try
--      {
-         frames = Desktop.getDesktopPane().getAllFrames();
-       } catch (ArrayIndexOutOfBoundsException e)
-       {
-         // occasional No such child exceptions are thrown here...
-         try
-         {
-           Thread.sleep(10);
-         } catch (InterruptedException f)
-         {
-         }
-       }
-     } while (frames == null);
-     return frames;
-   }
 -        frames = Desktop.desktop.getAllFrames();
 -      } catch (ArrayIndexOutOfBoundsException e)
 -      {
 -        // occasional No such child exceptions are thrown here...
 -        try
 -        {
 -          Thread.sleep(10);
 -        } catch (InterruptedException f)
 -        {
 -        }
 -      }
 -    } while (frames == null);
 -    return frames;
 -  }
++    /**
++     * Get all frames within the Desktop.
++     * 
++     * @return
++     */
++    protected JInternalFrame[] getAllFrames()
++    {
++      JInternalFrame[] frames = null;
++      // TODO is this necessary - is it safe - risk of hanging?
++      do
++          {
++              try
++                  {
++                      frames = Desktop.getDesktopPane().getAllFrames();
++                  } catch (ArrayIndexOutOfBoundsException e)
++                  {
++                      // occasional No such child exceptions are thrown here...
++                      try
++                          {
++                              Thread.sleep(10);
++                          } catch (InterruptedException f)
++                          {
++                          }
++                  }
++          } while (frames == null);
++      return frames;
++    }
  
--  /**
--   * Answers true if 'version' is equal to or later than 'supported', where each
--   * is formatted as major/minor versions like "2.8.3" or "2.3.4b1" for bugfix
--   * changes. Development and test values for 'version' are leniently treated
--   * i.e. answer true.
--   * 
--   * @param supported
--   *          - minimum version we are comparing against
--   * @param version
--   *          - version of data being processsed
--   * @return
--   */
--  public static boolean isVersionStringLaterThan(String supported,
--          String version)
++    /**
++     * Answers true if 'version' is equal to or later than 'supported', where each
++     * is formatted as major/minor versions like "2.8.3" or "2.3.4b1" for bugfix
++     * changes. Development and test values for 'version' are leniently treated
++     * i.e. answer true.
++     * 
++     * @param supported
++     *          - minimum version we are comparing against
++     * @param version
++     *          - version of data being processsed
++     * @return
++     */
++    public static boolean isVersionStringLaterThan(String supported,
++                                              String version)
    {
      if (supported == null || version == null
              || version.equalsIgnoreCase("DEVELOPMENT BUILD")
        SequenceI[] dsseqs = new SequenceI[dseqs.size()];
        dseqs.copyInto(dsseqs);
        ds = new jalview.datamodel.Alignment(dsseqs);
- //      debug("Jalview2XML Created new dataset " + vamsasSet.getDatasetId()
 -      Console.debug("Created new dataset " + vamsasSet.getDatasetId()
 -              + " for alignment " + System.identityHashCode(al));
++//      Console.debug("Jalview2XML Created new dataset " + vamsasSet.getDatasetId()
 +//              + " for alignment " + System.identityHashCode(al));
        addDatasetRef(vamsasSet.getDatasetId(), ds);
      }
      // set the dataset for the newly imported alignment.
        }
      } catch (Exception ex)
      {
-       Cache.log.error("Error loading PCA: " + ex.toString());
+       Console.error("Error loading PCA: " + ex.toString());
+     }
+   }
+   /**
+    * 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()
++// BH again was invokeAndWait
++    // try
++    // {
++      javax.swing.SwingUtilities.invokeLater(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)
 -    {
 -      Console.warn("Unexpected error when opening " + viewerType
 -              + " structure viewer", ex);
 -    }
++//    } catch (InvocationTargetException | InterruptedException ex)
++//    {
++//      Console.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)
+     {
+       Console.error("Error restoring Jmol session: " + e.toString());
      }
+     return null;
    }
  
    /**
      return colour;
    }
  }
++
   */
  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.Locale;
 -import java.util.Map;
 -import java.util.Vector;
  import jalview.analysis.AlignSeq;
  import jalview.api.StructureSelectionManagerProvider;
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
- import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
  import jalview.commands.OrderCommand;
@@@ -48,18 -58,6 +49,19 @@@ 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.Locale;
 +import java.util.Map;
 +import java.util.Vector;
 +
  import mc_view.Atom;
  import mc_view.PDBChain;
  import mc_view.PDBfile;
@@@ -68,6 -66,8 +70,7 @@@ public class StructureSelectionManager 
  {
    public final static String NEWLINE = System.lineSeparator();
  
 -  static IdentityHashMap<StructureSelectionManagerProvider, StructureSelectionManager> instances;
    private List<StructureMapping> mappings = new ArrayList<>();
  
    private boolean processSecondaryStructure = false;
    private List<CommandListener> commandListeners = new ArrayList<>();
  
    private List<SelectionListener> sel_listeners = new ArrayList<>();
 +  /*
 +   * instances of this class scoped by some context class
 +   */
 +  private IdentityHashMap<StructureSelectionManagerProvider, StructureSelectionManager> selectionManagers;
 +
 +  /**
 +   * Answers an instance of this class for the current application (Java or JS
 +   * 'applet') scope
 +   * 
 +   * @return
 +   */
 +  private static StructureSelectionManager getInstance()
 +  {
 +    return (StructureSelectionManager) ApplicationSingletonProvider
 +            .getInstance(StructureSelectionManager.class);
 +  }
 +
 +  /**
 +   * Private constructor as all 'singleton' instances are managed here or by
 +   * ApplicationSingletonProvider
 +   */
 +  private StructureSelectionManager()
 +  {
 +    selectionManagers = new IdentityHashMap<>();
 +  }
 +
 +  /**
 +   * Answers an instance of this class for the current application (Java or JS
 +   * 'applet') scope, and scoped to the specified context
 +   * 
 +   * @param context
 +   * @return
 +   */
 +  public static StructureSelectionManager getStructureSelectionManager(
 +          StructureSelectionManagerProvider context)
 +  {
 +    return getInstance().getInstanceForContext(context);
 +  }
 +
 +  /**
 +   * Answers an instance of this class scoped to the given context. The instance
 +   * is created on the first request for the context, thereafter the same
 +   * instance is returned. Note that the context may be null (this is the case
 +   * when running headless without a Desktop).
 +   * 
 +   * @param context
 +   * @return
 +   */
 +  StructureSelectionManager getInstanceForContext(
 +          StructureSelectionManagerProvider context)
 +  {
 +    StructureSelectionManager instance = selectionManagers.get(context);
 +    if (instance == null)
 +    {
 +      instance = new StructureSelectionManager();
 +      selectionManagers.put(context, instance);
 +    }
 +    return instance;
 +  }
++/** Null provider in 2.11.2
++
++
++  private static StructureSelectionManager nullProvider = null;
 +
++  public static StructureSelectionManager getStructureSelectionManager(
++          StructureSelectionManagerProvider context)
++  {
++    if (context == null)
++    {
++      if (nullProvider == null)
++      {
++        if (instances != null)
++        {
++          throw new Error(MessageManager.getString(
++                  "error.implementation_error_structure_selection_manager_null"),
++                  new NullPointerException(MessageManager
++                          .getString("exception.ssm_context_is_null")));
++        }
++        else
++        {
++          nullProvider = new StructureSelectionManager();
++        }
++        return nullProvider;
++      }
++    }
++    if (instances == null)
++    {
++      instances = new java.util.IdentityHashMap<>();
++    }
++    StructureSelectionManager instance = instances.get(context);
++    if (instance == null)
++    {
++      if (nullProvider != null)
++      {
++        instance = nullProvider;
++      }
++      else
++      {
++        instance = new StructureSelectionManager();
++      }
++      instances.put(context, instance);
++    }
++    return instance;
++  }
++*/
  
    /**
     * @return true if will try to use external services for processing secondary
              StructureMapping siftsMapping = null;
              try
              {
 -              siftsMapping = getStructureMapping(seq, pdbFile, chain.id,
 -                      pdb, chain, sqmpping, maxAlignseq, siftsClient);
 +              siftsMapping = getStructureMapping(seq,
 +                      pdbFile, chain.id, pdb, chain, sqmpping, maxAlignseq,
 +                      siftsClient);
                foundSiftsMappings.add(siftsMapping);
                chain.makeExactMapping(siftsMapping, seq);
-               chain.transferRESNUMFeatures(seq, "IEA: SIFTS");// FIXME: is this
+               chain.transferRESNUMFeatures(seq, "IEA: SIFTS",
+                       pdb.getId().toLowerCase(Locale.ROOT));// FIXME: is this
                // "IEA:SIFTS" ?
                chain.transferResidueAnnotation(siftsMapping, null);
              } catch (SiftsException e)
      }
    }
  
 +  
    /**
 -   * release all references associated with this manager provider
 +   * Removes the instance associated with this provider
     * 
 -   * @param jalviewLite
 +   * @param provider
     */
 -  public static void release(StructureSelectionManagerProvider jalviewLite)
 +  public static void release(StructureSelectionManagerProvider provider)
    {
 -    // synchronized (instances)
 -    {
 -      if (instances == null)
 -      {
 -        return;
 -      }
 -      StructureSelectionManager mnger = (instances.get(jalviewLite));
 -      if (mnger != null)
 -      {
 -        instances.remove(jalviewLite);
 -        try
 -        {
 -          /* bsoares 2019-03-20 finalize deprecated, no apparent external
 -           * resources to close
 -           */
 -          // mnger.finalize();
 -        } catch (Throwable x)
 -        {
 -        }
 -      }
 -    }
 +    getInstance().selectionManagers.remove(provider);
    }
-   
    public void registerPDBEntry(PDBEntry pdbentry)
    {
      if (pdbentry.getFile() != null
@@@ -83,11 -83,12 +83,11 @@@ public class IdentifiersUrlProvider ext
      String errorMessage = null;
      try
      {
-       // NOTE: THIS WILL FAIL IN SWINGJS BECAUSE IT INVOLVES A FILE READER
-     
+       // NOTE: THIS WILL FAIL IN SWINGJS BECAUSE IT INVOLVES A FILE READER
 -
        FileReader reader = new FileReader(idFileName);
        String key = "";
-       Map<String, Object> obj = (Map<String, Object>) JSONUtils.parse(reader);
+       Map<String, Object> obj = (Map<String, Object>) JSONUtils
+               .parse(reader);
        if (obj.containsKey(ID_ORG_KEY))
        {
          key = ID_ORG_KEY;
index 8119daa,f9fa80d..3e4f72b
mode 100755,100644..100644
   */
  package jalview.util;
  
- import java.io.File;
+ import java.awt.Desktop;
  import java.io.IOException;
- import java.lang.reflect.Constructor;
- import java.lang.reflect.Field;
- import java.lang.reflect.InvocationTargetException;
- import java.lang.reflect.Method;
+ import java.net.URI;
+ import java.net.URISyntaxException;
+ import java.util.ArrayList;
+ import java.util.List;
+ import jalview.bin.Cache;
+ import jalview.bin.Console;
  
- /**
-  * BrowserLauncher is a class that provides one static method, openURL, which
-  * opens the default web browser for the current user of the system to the given
-  * URL. It may support other protocols depending on the system -- mailto, ftp,
-  * etc. -- but that has not been rigorously tested and is not guaranteed to
-  * work.
-  * <p>
-  * Yes, this is platform-specific code, and yes, it may rely on classes on
-  * certain platforms that are not part of the standard JDK. What we're trying to
-  * do, though, is to take something that's frequently desirable but inherently
-  * platform-specific -- opening a default browser -- and allow programmers (you,
-  * for example) to do so without worrying about dropping into native code or
-  * doing anything else similarly evil.
-  * <p>
-  * Anyway, this code is completely in Java and will run on all JDK 1.1-compliant
-  * systems without modification or a need for additional libraries. All classes
-  * that are required on certain platforms to allow this to run are dynamically
-  * loaded at runtime via reflection and, if not found, will not cause this to do
-  * anything other than returning an error when opening the browser.
-  * <p>
-  * There are certain system requirements for this class, as it's running through
-  * Runtime.exec(), which is Java's way of making a native system call.
-  * Currently, this requires that a Macintosh have a Finder which supports the
-  * GURL event, which is true for Mac OS 8.0 and 8.1 systems that have the
-  * Internet Scripting AppleScript dictionary installed in the Scripting
-  * Additions folder in the Extensions folder (which is installed by default as
-  * far as I know under Mac OS 8.0 and 8.1), and for all Mac OS 8.5 and later
-  * systems. On Windows, it only runs under Win32 systems (Windows 95, 98, and NT
-  * 4.0, as well as later versions of all). On other systems, this drops back
-  * from the inherently platform-sensitive concept of a default browser and
-  * simply attempts to launch Netscape via a shell command.
-  * <p>
-  * This code is Copyright 1999-2001 by Eric Albert (ejalbert\@cs.stanford.edu)
-  * and may be redistributed or modified in any form without restrictions as long
-  * as the portion of this comment from this paragraph through the end of the
-  * comment is not removed. The author requests that he be notified of any
-  * application, applet, or other binary that makes use of this code, but that's
-  * more out of curiosity than anything and is not required. This software
-  * includes no warranty. The author is not repsonsible for any loss of data or
-  * functionality or any adverse or unexpected effects of using this software.
-  * <p>
-  * Credits: <br>
-  * Steven Spencer, JavaWorld magazine
-  * (<a href="http://www.javaworld.com/javaworld/javatips/jw-javatip66.html">Java
-  * Tip 66</a>) <br>
-  * Thanks also to Ron B. Yeh, Eric Shapiro, Ben Engber, Paul Teitlebaum, Andrea
-  * Cantatore, Larry Barowski, Trevor Bedzek, Frank Miedrich, and Ron Rabakukk
-  * 
-  * @author Eric Albert (<a href=
-  *         "mailto:ejalbert@cs.stanford.edu">ejalbert@cs.stanford.edu</a>)
-  * @version 1.4b1 (Released June 20, 2001)
-  */
  public class BrowserLauncher
  {
-   /**
-    * The Java virtual machine that we are running on. Actually, in most cases we
-    * only care about the operating system, but some operating systems require us
-    * to switch on the VM.
-    */
-   private static int jvm;
-   /** The browser for the system */
-   private static Object browser;
-   /**
-    * Caches whether any classes, methods, and fields that are not part of the
-    * JDK and need to be dynamically loaded at runtime loaded successfully.
-    * <p>
-    * Note that if this is <code>false</code>, <code>openURL()</code> will always
-    * return an IOException.
-    */
-   private static boolean loadedWithoutErrors;
-   /** The com.apple.mrj.MRJFileUtils class */
-   private static Class mrjFileUtilsClass;
-   /** The com.apple.mrj.MRJOSType class */
-   private static Class mrjOSTypeClass;
-   /** The com.apple.MacOS.AEDesc class */
-   private static Class aeDescClass;
-   /** The &lt;init&gt;(int) method of com.apple.MacOS.AETarget */
-   private static Constructor aeTargetConstructor;
-   /** The &lt;init&gt;(int, int, int) method of com.apple.MacOS.AppleEvent */
-   private static Constructor appleEventConstructor;
-   /** The &lt;init&gt;(String) method of com.apple.MacOS.AEDesc */
-   private static Constructor aeDescConstructor;
-   /** The findFolder method of com.apple.mrj.MRJFileUtils */
-   private static Method findFolder;
-   /** The getFileCreator method of com.apple.mrj.MRJFileUtils */
-   private static Method getFileCreator;
-   /** The getFileType method of com.apple.mrj.MRJFileUtils */
-   private static Method getFileType;
-   /** The openURL method of com.apple.mrj.MRJFileUtils */
-   private static Method openURL;
-   /** The makeOSType method of com.apple.MacOS.OSUtils */
-   private static Method makeOSType;
-   /** The putParameter method of com.apple.MacOS.AppleEvent */
-   private static Method putParameter;
-   /** The sendNoReply method of com.apple.MacOS.AppleEvent */
-   private static Method sendNoReply;
-   /** Actually an MRJOSType pointing to the System Folder on a Macintosh */
-   private static Object kSystemFolderType;
-   /** The keyDirectObject AppleEvent parameter type */
-   private static Integer keyDirectObject;
-   /** The kAutoGenerateReturnID AppleEvent code */
-   private static Integer kAutoGenerateReturnID;
-   /** The kAnyTransactionID AppleEvent code */
-   private static Integer kAnyTransactionID;
-   /** The linkage object required for JDirect 3 on Mac OS X. */
-   private static Object linkage;
-   /** The framework to reference on Mac OS X */
-   private static final String JDirect_MacOSX = "/System/Library/Frameworks/Carbon.framework/Frameworks/HIToolbox.framework/HIToolbox";
-   /** JVM constant for MRJ 2.0 */
-   private static final int MRJ_2_0 = 0;
-   /** JVM constant for MRJ 2.1 or later */
-   private static final int MRJ_2_1 = 1;
-   /** JVM constant for Java on Mac OS X 10.0 (MRJ 3.0) */
-   private static final int MRJ_3_0 = 3;
-   /** JVM constant for MRJ 3.1 */
-   private static final int MRJ_3_1 = 4;
-   /** JVM constant for any Windows NT JVM */
-   private static final int WINDOWS_NT = 5;
-   /** JVM constant for any Windows 9x JVM */
-   private static final int WINDOWS_9x = 6;
-   /** JVM constant for any other platform */
-   private static final int OTHER = -1;
-   /**
-    * The file type of the Finder on a Macintosh. Hardcoding "Finder" would keep
-    * non-U.S. English systems from working properly.
-    */
-   private static final String FINDER_TYPE = "FNDR";
-   /**
-    * The creator code of the Finder on a Macintosh, which is needed to send
-    * AppleEvents to the application.
-    */
-   private static final String FINDER_CREATOR = "MACS";
-   /** The name for the AppleEvent type corresponding to a GetURL event. */
-   private static final String GURL_EVENT = "GURL";
-   /**
-    * The first parameter that needs to be passed into Runtime.exec() to open the
-    * default web browser on Windows.
-    */
-   private static final String FIRST_WINDOWS_PARAMETER = "/c";
-   /** The second parameter for Runtime.exec() on Windows. */
-   private static final String SECOND_WINDOWS_PARAMETER = "start";
-   /**
-    * The third parameter for Runtime.exec() on Windows. This is a "title"
-    * parameter that the command line expects. Setting this parameter allows URLs
-    * containing spaces to work.
-    */
-   private static final String THIRD_WINDOWS_PARAMETER = "\"\"";
+   private static BrowserLauncher INSTANCE = null;
  
-   /**
-    * The shell parameters for Netscape that opens a given URL in an already-open
-    * copy of Netscape on many command-line systems.
-    */
-   private static final String NETSCAPE_REMOTE_PARAMETER = "-remote";
+   private static String preferredBrowser = null;
  
-   private static final String NETSCAPE_OPEN_PARAMETER_START = "openURL(";
-   private static final String NETSCAPE_OPEN_NEW_WINDOW = ", new-window";
-   private static final String NETSCAPE_OPEN_PARAMETER_END = ")";
-   /**
-    * The message from any exception thrown throughout the initialization
-    * process.
-    */
-   private static String errorMessage;
-   /**
-    * An initialization block that determines the operating system and loads the
-    * necessary runtime data.
-    */
-   static
+   public static BrowserLauncher getInstance()
    {
-     loadedWithoutErrors = true;
-     if (!Platform.isJS())
-     /**
-      * Java only
-      * 
-      * @j2sIgnore
-      * 
-      */
+     if (INSTANCE != null)
      {
-     String osName = System.getProperty("os.name");
-     if (osName.startsWith("Mac OS"))
-     {
-       String mrjVersion = System.getProperty("mrj.version");
-       String majorMRJVersion;
-       if (mrjVersion == null)
-       {
-         // must be on some later build with mrj support
-         majorMRJVersion = "3.1";
-       }
-       else
-       {
-         majorMRJVersion = mrjVersion.substring(0, 3);
-       }
-       try
-       {
-         double version = Double.valueOf(majorMRJVersion).doubleValue();
-         if (version == 2)
-         {
-           jvm = MRJ_2_0;
-         }
-         else if ((version >= 2.1) && (version < 3))
-         {
-           // Assume that all 2.x versions of MRJ work the same. MRJ 2.1 actually
-           // works via Runtime.exec() and 2.2 supports that but has an openURL()
-           // method
-           // as well that we currently ignore.
-           jvm = MRJ_2_1;
-         }
-         else if (version == 3.0)
-         {
-           jvm = MRJ_3_0;
-         }
-         else if (version >= 3.1)
-         {
-           // Assume that all 3.1 and later versions of MRJ work the same.
-           jvm = MRJ_3_1;
-         }
-         else
-         {
-           loadedWithoutErrors = false;
-           errorMessage = "Unsupported MRJ version: " + version;
-         }
-       } catch (NumberFormatException nfe)
-       {
-         loadedWithoutErrors = false;
-         errorMessage = "Invalid MRJ version: " + mrjVersion;
-       }
-     }
-     else if (osName.startsWith("Windows"))
-     {
-       if (osName.indexOf("9") != -1)
-       {
-         jvm = WINDOWS_9x;
-       }
-       else
-       {
-         jvm = WINDOWS_NT;
-       }
-     }
-     else
-     {
-       jvm = OTHER;
-     }
-     if (loadedWithoutErrors)
-     { // if we haven't hit any errors yet
-       loadedWithoutErrors = loadClasses();
-     }
+       return INSTANCE;
      }
+     INSTANCE = new BrowserLauncher();
+     return INSTANCE;
    }
  
-   /**
-    * This class should be never be instantiated; this just ensures so.
-    */
-   private BrowserLauncher()
+   public static void openURL(String url)
    {
-   }
-   /**
-    * Called by a static initializer to load any classes, fields, and methods
-    * required at runtime to locate the user's web browser.
-    * 
-    * @return <code>true</code> if all intialization succeeded <code>false</code>
-    *         if any portion of the initialization failed
-    */
-   private static boolean loadClasses()
-   {
-     if (!Platform.isJS())
-     /**
-      * Java only
-      * 
-      * @j2sIgnore
-      * 
-      */
-     {
-     switch (jvm)
+     if (Platform.isJS())
      {
-     case MRJ_2_0:
-       try
-       {
-         Class aeTargetClass = Class.forName("com.apple.MacOS.AETarget");
-         Class osUtilsClass = Class.forName("com.apple.MacOS.OSUtils");
-         Class appleEventClass = Class.forName("com.apple.MacOS.AppleEvent");
-         Class aeClass = Class.forName("com.apple.MacOS.ae");
-         aeDescClass = Class.forName("com.apple.MacOS.AEDesc");
-         aeTargetConstructor = aeTargetClass
-                 .getDeclaredConstructor(new Class[]
-                 { int.class });
-         appleEventConstructor = appleEventClass
-                 .getDeclaredConstructor(new Class[]
-                 { int.class, int.class, aeTargetClass, int.class,
-                     int.class });
-         aeDescConstructor = aeDescClass
-                 .getDeclaredConstructor(new Class[]
-                 { String.class });
-         makeOSType = osUtilsClass.getDeclaredMethod("makeOSType",
-                 new Class[]
-                 { String.class });
-         putParameter = appleEventClass.getDeclaredMethod("putParameter",
-                 new Class[]
-                 { int.class, aeDescClass });
-         sendNoReply = appleEventClass.getDeclaredMethod("sendNoReply",
-                 new Class[] {});
-         Field keyDirectObjectField = aeClass
-                 .getDeclaredField("keyDirectObject");
-         keyDirectObject = (Integer) keyDirectObjectField.get(null);
-         Field autoGenerateReturnIDField = appleEventClass
-                 .getDeclaredField("kAutoGenerateReturnID");
-         kAutoGenerateReturnID = (Integer) autoGenerateReturnIDField
-                 .get(null);
-         Field anyTransactionIDField = appleEventClass
-                 .getDeclaredField("kAnyTransactionID");
-         kAnyTransactionID = (Integer) anyTransactionIDField.get(null);
-       } catch (ClassNotFoundException cnfe)
-       {
-         errorMessage = cnfe.getMessage();
-         return false;
-       } catch (NoSuchMethodException nsme)
-       {
-         errorMessage = nsme.getMessage();
-         return false;
-       } catch (NoSuchFieldException nsfe)
-       {
-         errorMessage = nsfe.getMessage();
-         return false;
-       } catch (IllegalAccessException iae)
-       {
-         errorMessage = iae.getMessage();
-         return false;
-       }
-       break;
-     case MRJ_2_1:
-       try
-       {
-         mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils");
-         mrjOSTypeClass = Class.forName("com.apple.mrj.MRJOSType");
-         Field systemFolderField = mrjFileUtilsClass
-                 .getDeclaredField("kSystemFolderType");
-         kSystemFolderType = systemFolderField.get(null);
-         findFolder = mrjFileUtilsClass.getDeclaredMethod("findFolder",
-                 new Class[]
-                 { mrjOSTypeClass });
-         getFileCreator = mrjFileUtilsClass
-                 .getDeclaredMethod("getFileCreator", new Class[]
-                 { File.class });
-         getFileType = mrjFileUtilsClass.getDeclaredMethod("getFileType",
-                 new Class[]
-                 { File.class });
-       } catch (ClassNotFoundException cnfe)
-       {
-         errorMessage = cnfe.getMessage();
-         return false;
-       } catch (NoSuchFieldException nsfe)
-       {
-         errorMessage = nsfe.getMessage();
-         return false;
-       } catch (NoSuchMethodException nsme)
-       {
-         errorMessage = nsme.getMessage();
-         return false;
-       } catch (SecurityException se)
-       {
-         errorMessage = se.getMessage();
-         return false;
-       } catch (IllegalAccessException iae)
-       {
-         errorMessage = iae.getMessage();
-         return false;
-       }
-       break;
-     case MRJ_3_0:
-       try
-       {
-         Class linker = Class.forName("com.apple.mrj.jdirect.Linker");
-         Constructor constructor = linker
-                 .getConstructor(new Class[]
-                 { Class.class });
-         linkage = constructor
-                 .newInstance(new Object[]
-                 { BrowserLauncher.class });
-       } catch (ClassNotFoundException cnfe)
-       {
-         errorMessage = cnfe.getMessage();
-         return false;
-       } catch (NoSuchMethodException nsme)
-       {
-         errorMessage = nsme.getMessage();
-         return false;
-       } catch (InvocationTargetException ite)
-       {
-         errorMessage = ite.getMessage();
-         return false;
-       } catch (InstantiationException ie)
-       {
-         errorMessage = ie.getMessage();
-         return false;
-       } catch (IllegalAccessException iae)
-       {
-         errorMessage = iae.getMessage();
-         return false;
-       }
-       break;
-     case MRJ_3_1:
-       try
-       {
-         mrjFileUtilsClass = Class.forName("com.apple.mrj.MRJFileUtils");
-         openURL = mrjFileUtilsClass.getDeclaredMethod("openURL",
-                 new Class[]
-                 { String.class });
-       } catch (ClassNotFoundException cnfe)
-       {
-         errorMessage = cnfe.getMessage();
-         return false;
-       } catch (NoSuchMethodException nsme)
-       {
-         errorMessage = nsme.getMessage();
-         return false;
-       }
-       break;
-     default:
-       break;
-     }
 -      Platform.openURL(url);
++      try {
++            Platform.openURL(url);
++      } catch (Throwable t) {
++          System.err.println("Couldn't open "+url);
++          System.err.print(t.getStackTrace());
++      }
+       return;
      }
-     return true;
-   }
-   /**
-    * Attempts to locate the default web browser on the local system. s results
-    * so it only locates the browser once for each use of this class per JVM
-    * instance.
-    * 
-    * @return The browser for the system. Note that this may not be what you
-    *         would consider to be a standard web browser; instead, it's the
-    *         application that gets called to open the default web browser. In
-    *         some cases, this will be a non-String object that provides the
-    *         means of calling the default browser.
-    */
-   private static Object locateBrowser()
-   {
-     if (!Platform.isJS())
+     else
      /**
       * Java only
       * 
        {
          try
          {
-           File file = new File(systemFolder, systemFolderFiles[i]);
-           if (!file.isFile())
-           {
-             continue;
-           }
-           // We're looking for a file with a creator code of 'MACS' and
-           // a type of 'FNDR'. Only requiring the type results in non-Finder
-           // applications being picked up on certain Mac OS 9 systems,
-           // especially German ones, and sending a GURL event to those
-           // applications results in a logout under Multiple Users.
-           Object fileType = getFileType.invoke(null, new Object[] { file });
-           if (FINDER_TYPE.equals(fileType.toString()))
-           {
-             Object fileCreator = getFileCreator.invoke(null,
-                     new Object[]
-                     { file });
-             if (FINDER_CREATOR.equals(fileCreator.toString()))
-             {
-               browser = file.toString(); // Actually the Finder, but that's OK
-               return browser;
-             }
-           }
-         } catch (IllegalArgumentException iare)
-         {
-           errorMessage = iare.getMessage();
-           return null;
-         } catch (IllegalAccessException iae)
+           d.browse(new URI(url));
+         } catch (IOException e)
          {
-           browser = null;
-           errorMessage = iae.getMessage();
-           return browser;
-         } catch (InvocationTargetException ite)
+           Console.warn(MessageManager.formatMessage(
+                   "exception.browser_unable_to_launch", url));
+           Console.warn(e.getMessage());
+           Console.debug(Cache.getStackTraceString(e));
+         } catch (URISyntaxException e1)
          {
-           browser = null;
-           errorMessage = ite.getTargetException().getClass() + ": "
-                   + ite.getTargetException().getMessage();
-           return browser;
+           Console.warn(MessageManager.formatMessage(
+                   "exception.browser_unable_to_launch", url));
+           Console.warn(e1.getMessage());
+           Console.debug(Cache.getStackTraceString(e1));
          }
        }
-       browser = null;
-       break;
-     case MRJ_3_0:
-     case MRJ_3_1:
-       browser = ""; // Return something non-null
-       break;
-     case WINDOWS_NT:
-       browser = "cmd.exe";
-       break;
-     case WINDOWS_9x:
-       browser = "command.com";
-       break;
-     case OTHER:
-     default:
-       browser = jalview.bin.Cache.getDefault("DEFAULT_BROWSER", "firefox");
-       break;
-     }
+       else
+       {
+         Console.warn(MessageManager
+                 .formatMessage("exception.browser_os_not_supported", url));
+       }
      }
-     return browser;
    }
  
-   /**
-    * used to ensure that browser is up-to-date after a configuration change
-    * (Unix DEFAULT_BROWSER property change).
-    */
    public static void resetBrowser()
    {
-     browser = null;
+     resetBrowser(false);
    }
  
-   /**
-    * Attempts to open the default web browser to the given URL.
-    * 
-    * @param url
-    *          The URL to open
-    * @throws IOException
-    *           If the web browser could not be located or does not run
-    */
-   public static void openURL(String url) throws IOException
+   public static void resetBrowser(boolean removeIfNull)
    {
-     if (Platform.isJS())
-     {
-       Platform.openURL(url);
-       return;
-     }
-     else
-     /**
-      * Java only
-      * 
-      * @j2sIgnore
-      */
-     {
-     if (!loadedWithoutErrors)
+     String defaultBrowser = Cache.getProperty("DEFAULT_BROWSER");
+     preferredBrowser = defaultBrowser;
+     // System.setProperty(getBrowserSystemProperty(),
+     // Cache.getProperty("DEFAULT_BROWSER"));
+     if (defaultBrowser == null && removeIfNull)
      {
-       throw new IOException(MessageManager
-               .formatMessage("exception.browser_not_found", new String[]
-               { errorMessage }));
+       // System.clearProperty(getBrowserSystemProperty());
      }
  
-     Object browser = locateBrowser();
-     if (browser == null)
-     {
-       throw new IOException(MessageManager.formatMessage(
-               "exception.browser_unable_to_locate", new String[]
-               { errorMessage }));
-     }
-     switch (jvm)
-     {
-     case MRJ_2_0:
-       Object aeDesc = null;
-       try
-       {
-         aeDesc = aeDescConstructor.newInstance(new Object[] { url });
-         putParameter.invoke(browser,
-                 new Object[]
-                 { keyDirectObject, aeDesc });
-         sendNoReply.invoke(browser, new Object[] {});
-       } catch (InvocationTargetException ite)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.invocation_target_exception_creating_aedesc",
-                 new String[]
-                 { ite.getMessage() }));
-       } catch (IllegalAccessException iae)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.illegal_access_building_apple_evt", new String[]
-                 { iae.getMessage() }));
-       } catch (InstantiationException ie)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.illegal_access_building_apple_evt", new String[]
-                 { ie.getMessage() }));
-       } finally
-       {
-         aeDesc = null; // Encourage it to get disposed if it was created
-         browser = null; // Ditto
-       }
-       break;
-     case MRJ_2_1:
-       Runtime.getRuntime().exec(new String[] { (String) browser, url });
-       break;
-     case MRJ_3_0:
-       int[] instance = new int[1];
-       int result = ICStart(instance, 0);
-       if (result == 0)
-       {
-         int[] selectionStart = new int[] { 0 };
-         byte[] urlBytes = url.getBytes();
-         int[] selectionEnd = new int[] { urlBytes.length };
-         result = ICLaunchURL(instance[0], new byte[] { 0 }, urlBytes,
-                 urlBytes.length, selectionStart, selectionEnd);
-         if (result == 0)
-         {
-           // Ignore the return value; the URL was launched successfully
-           // regardless of what happens here.
-           ICStop(instance);
-         }
-         else
-         {
-           throw new IOException(MessageManager.formatMessage(
-                   "exception.unable_to_launch_url", new String[]
-                   { Integer.valueOf(result).toString() }));
-         }
-       }
-       else
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.unable_to_create_internet_config", new String[]
-                 { Integer.valueOf(result).toString() }));
-       }
-       break;
-     case MRJ_3_1:
-       try
-       {
-         openURL.invoke(null, new Object[] { url });
-       } catch (InvocationTargetException ite)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.invocation_target_calling_url", new String[]
-                 { ite.getMessage() }));
-       } catch (IllegalAccessException iae)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.illegal_access_calling_url", new String[]
-                 { iae.getMessage() }));
-       }
-       break;
-     case WINDOWS_NT:
-     case WINDOWS_9x:
-       // Add quotes around the URL to allow ampersands and other special
-       // characters to work.
-       Process process = Runtime.getRuntime()
-               .exec(new String[]
-               { (String) browser, FIRST_WINDOWS_PARAMETER,
-                   SECOND_WINDOWS_PARAMETER, THIRD_WINDOWS_PARAMETER,
-                   '"' + url + '"' });
-       // This avoids a memory leak on some versions of Java on Windows.
-       // That's hinted at in
-       // <http://developer.java.sun.com/developer/qow/archive/68/>.
-       try
-       {
-         process.waitFor();
-         process.exitValue();
-       } catch (InterruptedException ie)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.interrupted_launching_browser", new String[]
-                 { ie.getMessage() }));
-       }
-       break;
-     case OTHER:
-       // Assume that we're on Unix and that Netscape (actually Firefox) is
-       // installed
-       // First, attempt to open the URL in a currently running session of
-       // Netscape
-       // JBPNote log debug
-       /*
-        * System.out.println("Executing : "+browser+" "+
-        * NETSCAPE_REMOTE_PARAMETER+" "+ NETSCAPE_OPEN_PARAMETER_START + url +
-        * NETSCAPE_OPEN_NEW_WINDOW + NETSCAPE_OPEN_PARAMETER_END);
-        */
-       process = Runtime.getRuntime()
-               .exec(new String[]
-               { (String) browser, NETSCAPE_REMOTE_PARAMETER,
-                   NETSCAPE_OPEN_PARAMETER_START + url
-                           + NETSCAPE_OPEN_NEW_WINDOW
-                           + NETSCAPE_OPEN_PARAMETER_END });
-       try
-       {
-         int exitCode = process.waitFor();
-         if (exitCode != 0)
-         { // if Netscape was not open
-           Runtime.getRuntime().exec(new String[] { (String) browser, url });
-         }
-       } catch (InterruptedException ie)
-       {
-         throw new IOException(MessageManager.formatMessage(
-                 "exception.interrupted_launching_browser", new String[]
-                 { ie.getMessage() }));
-       }
-       break;
-     default:
-       // This should never occur, but if it does, we'll try the simplest thing
-       // possible
-       Runtime.getRuntime().exec(new String[] { (String) browser, url });
-       break;
-     }
-     }
    }
  
+   public static List<String> getBrowserList()
+   {
+     return new ArrayList<String>();
+   }
  
-   /**
-    * Methods required for Mac OS X. The presence of native methods does not
-    * cause any problems on other platforms.
-    */
-   private native static int ICStart(int[] instance, int signature);
-   private native static int ICStop(int[] instance);
+   public static String getBrowserSystemProperty()
+   {
+     // return IBrowserLaunching.BROWSER_SYSTEM_PROPERTY;
+     return "jalview.default.browser";
+   }
  
-   private native static int ICLaunchURL(int instance, byte[] hint,
-           byte[] data, int len, int[] selectionStart, int[] selectionEnd);
 -}
 +}
Simple merge
Simple merge
@@@ -168,156 -178,181 +178,190 @@@ public class DBRefUtil
      return rfs;
    }
  
-       /**
-        * look up source in an internal list of database reference sources and return
-        * the canonical jalview name for the source, or the original string if it has
-        * no canonical form.
-        * 
-        * @param source
-        * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*) or
-        *         original source
-        */
-       public static String getCanonicalName(String source) 
-       {
-         if (source == null) 
-         {
-               return null;
-         }
-         String canonical = canonicalSourceNameLookup.get(source.toLowerCase(Locale.ROOT));
-         return canonical == null ? source : canonical;
-       }
-       /**
-        * Returns a (possibly empty) list of those references that match the given
-        * entry. Currently uses a comparator which matches if
-        * <ul>
-        * <li>database sources are the same</li>
-        * <li>accession ids are the same</li>
-        * <li>both have no mapping, or the mappings are the same</li>
-        * </ul>
-        * 
-        * @param ref   Set of references to search
-        * @param entry pattern to match
-        * @param mode  SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION optional
-        * @return
-        */
-       public static List<DBRefEntry> searchRefs(List<DBRefEntry> ref, DBRefEntry entry, int mode) {
-               return searchRefs(ref, entry, matchDbAndIdAndEitherMapOrEquivalentMapList, mode);
-       }
-       /**
-        * Returns a list of those references that match the given accession id
-        * <ul>
-        * <li>database sources are the same</li>
-        * <li>accession ids are the same</li>
-        * <li>both have no mapping, or the mappings are the same</li>
-        * </ul>
-        * 
-        * @param refs  Set of references to search
-        * @param accId accession id to match
-        * @return
-        */
-       public static List<DBRefEntry> searchRefs(List<DBRefEntry> refs, String accId) {
-               List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
-               if (refs == null || accId == null) {
-                       return rfs;
-               }
-               for (int i = 0, n = refs.size(); i < n; i++) {
-                       DBRefEntry e = refs.get(i);
-                       if (accId.equals(e.getAccessionId())) {
-                               rfs.add(e);
-                       }
-               }
-               return rfs;
- //    return searchRefs(refs, new DBRefEntry("", "", accId), matchId, SEARCH_MODE_FULL);
-       }
-       /**
-        * Returns a (possibly empty) list of those references that match the given
-        * entry, according to the given comparator.
-        * 
-        * @param refs       an array of database references to search
-        * @param entry      an entry to compare against
-        * @param comparator
-        * @param mode       SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION
-        *                   optional
-        * @return
-        */
-       static List<DBRefEntry> searchRefs(List<DBRefEntry> refs, DBRefEntry entry, DbRefComp comparator, int mode) {
-               List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
-               if (refs == null || entry == null) {
-                       return rfs;
-               }
-               for (int i = 0, n = refs.size(); i < n; i++) {
-                       DBRefEntry e = refs.get(i);
-                       if (comparator.matches(entry, e, SEARCH_MODE_FULL)) {
-                               rfs.add(e);
-                       }
-               }
-               return rfs;
-       }
-       interface DbRefComp {
-               default public boolean matches(DBRefEntry refa, DBRefEntry refb) {
-                       return matches(refa, refb, SEARCH_MODE_FULL);
-               };
-               public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode);
-       }
-       /**
-        * match on all non-null fields in refa
-        */
-       // TODO unused - remove? would be broken by equating "" with null
-       public static DbRefComp matchNonNullonA = new DbRefComp() {
-               @Override
-               public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
-                       if ((mode & DB_SOURCE) != 0 && 
-                                       (refa.getSource() == null || DBRefUtils.getCanonicalName(refb.getSource())
-                                       .equals(DBRefUtils.getCanonicalName(refa.getSource())))) {
-                               if ((mode & DB_VERSION) != 0 && 
-                                               (refa.getVersion() == null || refb.getVersion().equals(refa.getVersion()))) {
-                                       if ((mode & DB_ID) != 0 && 
-                                                       (refa.getAccessionId() == null || refb.getAccessionId().equals(refa.getAccessionId()))) {
-                                               if ((mode & DB_MAP) != 0 && 
-                                                               (refa.getMap() == null || (refb.getMap() != null && refb.getMap().equals(refa.getMap())))) {
-                                                       return true;
-                                               }
-                                       }
-                               }
-                       }
-                       return false;
-               }
-       };
-       /**
-        * either field is null or field matches for all of source, version, accession
-        * id and map.
-        */
-       // TODO unused - remove?
-       public static DbRefComp matchEitherNonNull = new DbRefComp() {
-               @Override
-               public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode) {
-                       if (nullOrEqualSource(refa.getSource(), refb.getSource())
-                                       && nullOrEqual(refa.getVersion(), refb.getVersion())
-                                       && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
-                                       && nullOrEqual(refa.getMap(), refb.getMap())) {
-                               return true;
-                       }
-                       return false;
-               }
-       };
+   /**
+    * look up source in an internal list of database reference sources and return
+    * the canonical jalview name for the source, or the original string if it has
+    * no canonical form.
+    * 
+    * @param source
+    * @return canonical jalview source (one of jalview.datamodel.DBRefSource.*)
+    *         or original source
+    */
+   public static String getCanonicalName(String source)
+   {
+     if (source == null)
+     {
+       return null;
+     }
+     String canonical = canonicalSourceNameLookup
+             .get(source.toLowerCase(Locale.ROOT));
+     return canonical == null ? source : canonical;
+   }
+   /**
+    * Returns a (possibly empty) list of those references that match the given
+    * entry. Currently uses a comparator which matches if
+    * <ul>
+    * <li>database sources are the same</li>
+    * <li>accession ids are the same</li>
+    * <li>both have no mapping, or the mappings are the same</li>
+    * </ul>
+    * 
+    * @param ref
+    *          Set of references to search
+    * @param entry
+    *          pattern to match
+    * @param mode
+    *          SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION optional
+    * @return
+    */
+   public static List<DBRefEntry> searchRefs(List<DBRefEntry> ref,
+           DBRefEntry entry, int mode)
+   {
+     return searchRefs(ref, entry,
+             matchDbAndIdAndEitherMapOrEquivalentMapList, mode);
+   }
+   /**
+    * Returns a list of those references that match the given accession id
+    * <ul>
+    * <li>database sources are the same</li>
+    * <li>accession ids are the same</li>
+    * <li>both have no mapping, or the mappings are the same</li>
+    * </ul>
+    * 
+    * @param refs
+    *          Set of references to search
+    * @param accId
+    *          accession id to match
+    * @return
+    */
+   public static List<DBRefEntry> searchRefs(List<DBRefEntry> refs,
+           String accId)
+   {
+     List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
+     if (refs == null || accId == null)
+     {
+       return rfs;
+     }
+     for (int i = 0, n = refs.size(); i < n; i++)
+     {
+       DBRefEntry e = refs.get(i);
+       if (accId.equals(e.getAccessionId()))
+       {
+         rfs.add(e);
+       }
+     }
+     return rfs;
+     // return searchRefs(refs, new DBRefEntry("", "", accId), matchId,
+     // SEARCH_MODE_FULL);
+   }
+   /**
+    * Returns a (possibly empty) list of those references that match the given
+    * entry, according to the given comparator.
+    * 
+    * @param refs
+    *          an array of database references to search
+    * @param entry
+    *          an entry to compare against
+    * @param comparator
+    * @param mode
+    *          SEARCH_MODE_FULL for all; SEARCH_MODE_NO_MAP_NO_VERSION optional
+    * @return
+    */
+   static List<DBRefEntry> searchRefs(List<DBRefEntry> refs,
+           DBRefEntry entry, DbRefComp comparator, int mode)
+   {
+     List<DBRefEntry> rfs = new ArrayList<DBRefEntry>();
+     if (refs == null || entry == null)
+     {
+       return rfs;
+     }
+     for (int i = 0, n = refs.size(); i < n; i++)
+     {
+       DBRefEntry e = refs.get(i);
+       if (comparator.matches(entry, e, SEARCH_MODE_FULL))
+       {
+         rfs.add(e);
+       }
+     }
+     return rfs;
+   }
+   interface DbRefComp
+   {
+     default public boolean matches(DBRefEntry refa, DBRefEntry refb)
+     {
+       return matches(refa, refb, SEARCH_MODE_FULL);
+     };
+     public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode);
+   }
+   /**
+    * match on all non-null fields in refa
+    */
+   // TODO unused - remove? would be broken by equating "" with null
+   public static DbRefComp matchNonNullonA = new DbRefComp()
+   {
+     @Override
+     public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
+     {
+       if ((mode & DB_SOURCE) != 0 && (refa.getSource() == null
+               || DBRefUtils.getCanonicalName(refb.getSource()).equals(
+                       DBRefUtils.getCanonicalName(refa.getSource()))))
+       {
+         if ((mode & DB_VERSION) != 0 && (refa.getVersion() == null
+                 || refb.getVersion().equals(refa.getVersion())))
+         {
+           if ((mode & DB_ID) != 0 && (refa.getAccessionId() == null
+                   || refb.getAccessionId().equals(refa.getAccessionId())))
+           {
+             if ((mode & DB_MAP) != 0
+                     && (refa.getMap() == null || (refb.getMap() != null
+                             && refb.getMap().equals(refa.getMap()))))
+             {
+               return true;
+             }
+           }
+         }
+       }
+       return false;
+     }
+   };
+   /**
+    * either field is null or field matches for all of source, version, accession
+    * id and map.
+    */
+   // TODO unused - remove?
+   public static DbRefComp matchEitherNonNull = new DbRefComp()
+   {
+     @Override
+     public boolean matches(DBRefEntry refa, DBRefEntry refb, int mode)
+     {
+       if (nullOrEqualSource(refa.getSource(), refb.getSource())
+               && nullOrEqual(refa.getVersion(), refb.getVersion())
+               && nullOrEqual(refa.getAccessionId(), refb.getAccessionId())
+               && nullOrEqual(refa.getMap(), refb.getMap()))
+       {
+         return true;
+       }
+       return false;
+     }
+   };
  
 +  private static Regex PARSE_REGEX;
 +
 +  private static Regex getParseRegex()
 +  {
 +    return (PARSE_REGEX == null ? PARSE_REGEX = Platform.newRegex(
 +            "([0-9][0-9A-Za-z]{3})\\s*(.?)\\s*;\\s*([0-9]+)-([0-9]+)")
 +            : PARSE_REGEX);
 +  }
 +
    /**
     * Parses a DBRefEntry and adds it to the sequence, also a PDBEntry if the
     * database is PDB.
@@@ -75,9 -73,10 +76,10 @@@ public class HttpUtil
    {
      return file.startsWith("http://") || file.startsWith("https://");
    }
 +  
    /**
     * wrapper to get/post to a URL or check headers
 -   * 
     * @param url
     * @param ids
     * @param readTimeout
Simple merge
   */
  package jalview.util;
  
 -import java.util.ArrayList;
 -import java.util.Arrays;
 -import java.util.HashMap;
 -import java.util.Iterator;
 -import java.util.List;
 -import java.util.Map;
  import jalview.analysis.AlignmentSorter;
  import jalview.api.AlignViewportI;
+ import jalview.bin.Console;
  import jalview.commands.CommandI;
  import jalview.commands.EditCommand;
  import jalview.commands.EditCommand.Action;
@@@ -39,13 -49,6 +43,12 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceGroup;
  import jalview.datamodel.SequenceI;
  
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.HashMap;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
  /**
   * Helper methods for manipulations involving sequence mappings.
   * 
@@@ -361,19 -364,17 +364,18 @@@ public final class MappingUtil
         */
        int startResiduePos = selected.findPosition(firstUngappedPos);
        int endResiduePos = selected.findPosition(lastUngappedPos);
 +
-       for (AlignedCodonFrame acf : codonFrames)
+       for (SequenceI seq : mapTo.getAlignment().getSequences())
        {
-         SequenceI mappedSequence = targetIsNucleotide
-                 ? acf.getDnaForAaSeq(selected)
-                 : acf.getAaForDnaSeq(selected);
-         if (mappedSequence != null)
+         int mappedStartResidue = 0;
+         int mappedEndResidue = 0;
+         for (AlignedCodonFrame acf : codonFrames)
          {
-           for (SequenceI seq : mapTo.getAlignment().getSequences())
+           // rather than use acf.getCoveringMapping() we iterate through all
+           // mappings to make sure all CDS are selected for a protein
+           for (SequenceToSequenceMapping map : acf.getMappings())
            {
-             int mappedStartResidue = 0;
-             int mappedEndResidue = 0;
-             if (seq.getDatasetSequence() == mappedSequence)
+             if (map.covers(selected) && map.covers(seq))
              {
                /*
                 * Found a sequence mapping. Locate the start/end mapped residues.
@@@ -57,7 -56,8 +56,8 @@@ public class MessageManage
        // Locale.setDefault(loc);
        /* Getting messages for GV */
        log.info("Getting messages for lang: " + loc);
-       rb = ResourceBundle.getBundle("lang.Messages", Platform.getLocaleOrNone(loc), Control.getControl(Control.FORMAT_PROPERTIES));
+       Control control = Control.getControl(Control.FORMAT_PROPERTIES);
 -      rb = ResourceBundle.getBundle("lang.Messages", loc, control);
++      rb = ResourceBundle.getBundle("lang.Messages", Platform.getLocaleOrNone(loc), control);
        // if (log.isLoggable(Level.FINEST))
        // {
        // // this might take a while, so we only do it if it will be shown
@@@ -76,29 -54,8 +76,27 @@@ public class Platfor
    private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
            isWin = null, isLinux = null;
  
    private static Boolean isHeadless = null;
  
 +  private static swingjs.api.JSUtilI jsutil;
 +
 +  static
 +  {
 +    if (isJS)
 +    {
 +      try
 +      {
 +        // this is ok - it's a highly embedded method in Java; the deprecation
 +        // is
 +        // really a recommended best practice.
 +        jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
 +      } catch (InstantiationException | IllegalAccessException
 +              | ClassNotFoundException e)
 +      {
 +        e.printStackTrace();
 +      }
 +    }
 +  }
    /**
     * added to group mouse events into Windows and nonWindows (mac, unix, linux)
     * 
    public static void addJ2SDirectDatabaseCall(String domain)
    {
  
 -    if (isJS())
 +    if (isJS)
      {
 +      jsutil.addDirectDatabaseCall(domain);
        System.out.println(
--              "Platform adding known access-control-allow-origin * for domain "
--                      + domain);
++            "Platform adding known access-control-allow-origin * for domain "
++                    + domain);
+       /**
+        * @j2sNative
+        * 
+        *            J2S.addDirectDatabaseCall(domain);
+        */
      }
  
    }
  
 +  /**
 +   * Allow for URL-line command arguments. Untested.
 +   * 
 +   */
    public static void getURLCommandArguments()
    {
 -    try
 -    {
 +      try {
        /**
         * Retrieve the first query field as command arguments to Jalview. Include
         * only if prior to "?j2s" or "&j2s" or "#". Assign the applet's
@@@ -548,28 -572,17 +572,42 @@@ public class StringUtil
      return enc;
    }
  
 +  /**
 +   * Answers true if the string is not empty and consists only of digits, or
 +   * characters 'a'-'f' or 'A'-'F', else false
 +   * 
 +   * @param s
 +   * @return
 +   */
 +  public static boolean isHexString(String s)
 +  {
 +    int j = s.length();
 +    if (j == 0)
 +    {
 +      return false;
 +    }
 +    for (int i = 0; i < j; i++)
 +    {
 +      int c = s.charAt(i);
 +      if (!(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'))
 +      {
 +        return false;
 +      }
 +    }
 +    return true;
 +  }
++
+   public static int firstCharPosIgnoreCase(String text, String chars)
+   {
+     int min = text.length() + 1;
+     for (char c : chars.toLowerCase(Locale.ROOT).toCharArray())
+     {
+       int i = text.toLowerCase(Locale.ROOT).indexOf(c);
+       if (0 <= i && i < min)
+       {
+         min = i;
+       }
+     }
+     return min < text.length() + 1 ? min : -1;
+   }
  }
index 8d4796d,0000000..eba241a
mode 100644,000000..100644
--- /dev/null
@@@ -1,547 -1,0 +1,548 @@@
 +package jalview.workers;
 +
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.NoSuchElementException;
 +import java.util.Objects;
 +import java.util.WeakHashMap;
 +import java.util.concurrent.CopyOnWriteArrayList;
 +import java.util.concurrent.Executors;
 +import java.util.concurrent.Future;
 +import java.util.concurrent.ScheduledExecutorService;
 +import java.util.concurrent.TimeUnit;
 +
 +import static java.lang.String.format;
 +import static java.util.Collections.synchronizedMap;
 +import static java.util.Collections.unmodifiableList;
 +
 +import java.util.ArrayList;
 +
 +import jalview.api.AlignCalcListener;
 +import jalview.api.AlignCalcManagerI2;
 +import jalview.api.AlignCalcWorkerI;
 +import jalview.api.PollableAlignCalcWorkerI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.AlignmentAnnotation;
 +
 +public class AlignCalcManager2 implements AlignCalcManagerI2
 +{
 +  private abstract class WorkerManager
 +  {
 +    protected volatile boolean enabled = true;
 +
 +    protected AlignCalcWorkerI worker;
 +
 +    WorkerManager(AlignCalcWorkerI worker)
 +    {
 +      this.worker = worker;
 +    }
 +
 +    protected AlignCalcWorkerI getWorker()
 +    {
 +      return worker;
 +    }
 +
 +    boolean isEnabled()
 +    {
 +      return enabled;
 +    }
 +
 +    void setEnabled(boolean enabled)
 +    {
 +      this.enabled = enabled;
 +    }
 +
 +    synchronized void restart()
 +    {
 +      if (!isEnabled())
 +      {
 +        return;
 +      }
 +      if (!isRegistered())
 +      {
 +        setEnabled(false);
 +      }
 +      if (isWorking())
 +      {
 +        cancel();
 +      }
 +      submit();
 +    }
 +
 +    protected boolean isRegistered()
 +    {
 +      return registered.containsKey(getWorker());
 +    }
 +
 +    abstract boolean isWorking();
 +
 +    protected abstract void submit();
 +
 +    abstract void cancel();
 +  }
 +
 +  private class SimpleWorkerManager extends WorkerManager
 +  {
 +    private Future<?> task = null;
 +
 +    SimpleWorkerManager(AlignCalcWorkerI worker)
 +    {
 +      super(worker);
 +    }
 +
 +    @Override
 +    boolean isWorking()
 +    {
 +      return task != null && !task.isDone();
 +    }
 +
 +    @Override
 +    protected void submit()
 +    {
 +      if (task != null && !(task.isDone() || task.isCancelled()))
 +      {
 +        throw new IllegalStateException(
 +                "Cannot submit new task if the prevoius one is still running");
 +      }
-       Cache.log.debug(
++      Console.debug(
 +              format("Worker %s queued", getWorker().getClass().getName()));
 +      task = executor.submit(() -> {
 +        try
 +        {
-           Cache.log.debug(format("Worker %s started",
++          Console.debug(format("Worker %s started",
 +                  getWorker().getClass().getName()));
 +          getWorker().run();
-           Cache.log.debug(format("Worker %s finished",
++          Console.debug(format("Worker %s finished",
 +                  getWorker().getClass().getName()));
 +        } catch (InterruptedException e)
 +        {
-           Cache.log.debug(format("Worker %s interrupted",
++          Console.debug(format("Worker %s interrupted",
 +                  getWorker().getClass().getName()));
 +        } catch (Throwable th)
 +        {
-           Cache.log.debug(format("Worker %s failed",
++          Console.debug(format("Worker %s failed",
 +                  getWorker().getClass().getName()), th);
 +        } finally
 +        {
 +          if (!isRegistered())
 +          {
 +            // delete worker reference so garbage collector can remove it
 +            worker = null;
 +          }
 +        }
 +      });
 +    }
 +
 +    @Override
 +    synchronized void cancel()
 +    {
 +      if (!isWorking())
 +      {
 +        return;
 +      }
-       Cache.log.debug(format("Cancelling worker %s",
++      Console.debug(format("Cancelling worker %s",
 +              getWorker().getClass().getName()));
 +      task.cancel(true);
 +    }
 +  }
 +
 +  private class PollableWorkerManager extends WorkerManager
 +  {
 +    private Future<?> task = null;
 +
 +    PollableWorkerManager(PollableAlignCalcWorkerI worker)
 +    {
 +      super(worker);
 +    }
 +
 +    @Override
 +    protected PollableAlignCalcWorkerI getWorker()
 +    {
 +      return (PollableAlignCalcWorkerI) super.getWorker();
 +    }
 +
 +    @Override
 +    boolean isWorking()
 +    {
 +      return task != null && !task.isDone();
 +    }
 +
 +    protected void submit()
 +    {
 +      if (task != null && !(task.isDone() || task.isCancelled()))
 +      {
 +        throw new IllegalStateException(
 +                "Cannot submit new task if the prevoius one is still running");
 +      }
-       Cache.log.debug(
++      Console.debug(
 +              format("Worker %s queued", getWorker().getClass().getName()));
 +      final var runnable = new Runnable()
 +      {
 +        private boolean started = false;
 +
 +        private boolean completed = false;
 +
 +        Future<?> future = null;
 +
 +        @Override
 +        public void run()
 +        {
 +          try
 +          {
 +            if (!started)
 +            {
-               Cache.log.debug(format("Worker %s started",
++              Console.debug(format("Worker %s started",
 +                      getWorker().getClass().getName()));
 +              getWorker().startUp();
 +              started = true;
 +            }
 +            else if (!completed)
 +            {
-               Cache.log.debug(format("Polling worker %s",
++              Console.debug(format("Polling worker %s",
 +                      getWorker().getClass().getName()));
 +              if (getWorker().poll())
 +              {
-                 Cache.log.debug(format("Worker %s finished",
++                Console.debug(format("Worker %s finished",
 +                        getWorker().getClass().getName()));
 +                completed = true;
 +              }
 +            }
 +          } catch (Throwable th)
 +          {
-             Cache.log.debug(format("Worker %s failed",
++            Console.debug(format("Worker %s failed",
 +                    getWorker().getClass().getName()), th);
 +            completed = true;
 +          }
 +          if (completed)
 +          {
 +            final var worker = getWorker();
 +            if (!isRegistered())
 +              PollableWorkerManager.super.worker = null;
-             Cache.log.debug(format("Finalizing completed worker %s",
++            Console.debug(format("Finalizing completed worker %s",
 +                    worker.getClass().getName()));
 +            worker.done();
 +            // almost impossible, but the future may be null at this point
 +            // let it throw NPE to cancel forcefully
 +            future.cancel(false);
 +          }
 +        }
 +      };
 +      runnable.future = task = executor.scheduleWithFixedDelay(runnable, 10,
 +              1000, TimeUnit.MILLISECONDS);
 +    }
 +
 +    synchronized protected void cancel()
 +    {
 +      if (!isWorking())
 +      {
 +        return;
 +      }
-       Cache.log.debug(format("Cancelling worker %s",
++      Console.debug(format("Cancelling worker %s",
 +              getWorker().getClass().getName()));
 +      task.cancel(false);
 +      executor.submit(() -> {
 +        final var worker = getWorker();
 +        if (!isRegistered())
 +          PollableWorkerManager.super.worker = null;
 +        if (worker != null)
 +        {
 +          worker.cancel();
-           Cache.log.debug(format("Finalizing cancelled worker %s",
++          Console.debug(format("Finalizing cancelled worker %s",
 +                  worker.getClass().getName()));
 +          worker.done();
 +        }
 +      });
 +    }
 +  }
 +
 +  private final ScheduledExecutorService executor = Executors
 +          .newSingleThreadScheduledExecutor();
 +
 +  private final Map<AlignCalcWorkerI, WorkerManager> registered = synchronizedMap(
 +          new HashMap<>());
 +
 +  private final Map<AlignCalcWorkerI, WorkerManager> oneshot = synchronizedMap(
 +          new WeakHashMap<>());
 +
 +  private final List<AlignCalcListener> listeners = new CopyOnWriteArrayList<>();
 +
 +  private WorkerManager createManager(AlignCalcWorkerI worker)
 +  {
 +    if (worker instanceof PollableAlignCalcWorkerI)
 +    {
 +      return new PollableWorkerManager((PollableAlignCalcWorkerI) worker);
 +    }
 +    else
 +    {
 +      return new SimpleWorkerManager(worker);
 +    }
 +  }
 +
 +  @Override
 +  public void registerWorker(AlignCalcWorkerI worker)
 +  {
 +    Objects.requireNonNull(worker);
 +    WorkerManager manager = createManager(worker);
 +    registered.putIfAbsent(worker, manager);
 +    startWorker(worker);
 +  }
 +
 +  @Override
 +  public List<AlignCalcWorkerI> getWorkers()
 +  {
 +    List<AlignCalcWorkerI> result = new ArrayList<>(registered.size());
 +    result.addAll(registered.keySet());
 +    return result;
 +  }
 +
 +  @Override
 +  public List<AlignCalcWorkerI> getWorkersOfClass(
 +          Class<? extends AlignCalcWorkerI> cls)
 +  {
 +    List<AlignCalcWorkerI> collected = new ArrayList<>();
 +    for (var worker : getWorkers())
 +    {
 +      if (worker.getClass().equals(cls))
 +      {
 +        collected.add(worker);
 +      }
 +    }
 +    return unmodifiableList(collected);
 +  }
 +
 +  @Override
 +  public void removeWorker(AlignCalcWorkerI worker)
 +  {
 +    if (worker.isDeletable())
 +    {
 +      registered.remove(worker);
 +    }
 +  }
 +
 +  @Override
 +  public void removeWorkerForAnnotation(AlignmentAnnotation annot)
 +  {
 +    synchronized (registered)
 +    {
 +      for (var worker : getWorkers())
 +      {
 +        if (worker.involves(annot))
 +        {
 +          removeWorker(worker);
 +        }
 +      }
 +    }
 +  }
 +
 +  @Override
 +  public void removeWorkersOfClass(Class<? extends AlignCalcWorkerI> cls)
 +  {
 +    synchronized (registered)
 +    {
 +      for (var worker : getWorkers())
 +      {
 +        if (worker.getClass().equals(cls))
 +        {
 +          removeWorker(worker);
 +        }
 +      }
 +    }
 +  }
 +
 +  @Override
 +  public void disableWorker(AlignCalcWorkerI worker)
 +  {
 +    // Null pointer check might be needed
 +    registered.get(worker).setEnabled(false);
 +  }
 +
 +  @Override
 +  public void enableWorker(AlignCalcWorkerI worker)
 +  {
 +    // Null pointer check might be needed
 +    registered.get(worker).setEnabled(true);
 +  }
 +
 +  @Override
 +  public boolean isDisabled(AlignCalcWorkerI worker)
 +  {
 +    if (registered.containsKey(worker))
 +    {
 +      return !registered.get(worker).isEnabled();
 +    }
 +    else
 +    {
 +      return false;
 +    }
 +  }
 +
 +  @Override
 +  public boolean isWorking(AlignCalcWorkerI worker)
 +  {
 +    var manager = registered.get(worker);
 +    if (manager == null)
 +      manager = oneshot.get(worker);
 +    if (manager == null)
 +      return false;
 +    else
 +      return manager.isWorking();
 +  }
 +
 +  @Override
 +  public boolean isWorkingWithAnnotation(AlignmentAnnotation annot)
 +  {
 +    synchronized (registered)
 +    {
 +      for (var entry : registered.entrySet())
 +        if (entry.getKey().involves(annot) && entry.getValue().isWorking())
 +          return true;
 +    }
 +    synchronized (oneshot)
 +    {
 +      for (var entry : registered.entrySet())
 +        if (entry.getKey().involves(annot) && entry.getValue().isWorking())
 +          return true;
 +    }
 +    return false;
 +  }
 +
 +  @Override
 +  public boolean isWorking()
 +  {
 +    synchronized (registered)
 +    {
 +      for (var manager : registered.values())
 +        if (manager.isWorking())
 +          return true;
 +    }
 +    synchronized (oneshot)
 +    {
 +      for (var manager : oneshot.values())
 +        if (manager.isWorking())
 +          return true;
 +    }
 +    return false;
 +  }
 +
 +  @Override
 +  public void startWorker(AlignCalcWorkerI worker)
 +  {
 +    Objects.requireNonNull(worker);
 +    var manager = registered.get(worker);
 +    if (manager == null)
 +    {
-       Cache.log.warn("Starting unregistered worker " + worker);
++      Console.warn("Starting unregistered worker " + worker);
 +      manager = createManager(worker);
 +      oneshot.put(worker, manager);
 +    }
 +    manager.restart();
 +  }
 +
 +  @Override
 +  public void restartWorkers()
 +  {
 +    synchronized (registered)
 +    {
 +      for (var manager : registered.values())
 +      {
 +        manager.restart();
 +      }
 +    }
 +  }
 +
 +  @Override
 +  public void cancelWorker(AlignCalcWorkerI worker)
 +  {
 +    Objects.requireNonNull(worker);
 +    var manager = registered.get(worker);
 +    if (manager == null)
 +      manager = oneshot.get(worker);
 +    if (manager == null)
 +    {
 +      throw new NoSuchElementException();
 +    }
 +    manager.cancel();
 +  }
 +
 +  private void notifyQueued(AlignCalcWorkerI worker)
 +  {
 +    for (AlignCalcListener listener : listeners)
 +    {
 +      listener.workerQueued(worker);
 +    }
 +  }
 +
 +  private void notifyStarted(AlignCalcWorkerI worker)
 +  {
 +    for (AlignCalcListener listener : listeners)
 +    {
 +      listener.workerStarted(worker);
 +    }
 +  }
 +
 +  private void notifyCompleted(AlignCalcWorkerI worker)
 +  {
 +    for (AlignCalcListener listener : listeners)
 +    {
 +      try
 +      {
 +        listener.workerCompleted(worker);
 +      } catch (RuntimeException e)
 +      {
 +        e.printStackTrace();
 +      }
 +    }
 +  }
 +
 +  private void notifyCancelled(AlignCalcWorkerI worker)
 +  {
 +    for (AlignCalcListener listener : listeners)
 +    {
 +      try
 +      {
 +        listener.workerCancelled(worker);
 +      } catch (RuntimeException e)
 +      {
 +        e.printStackTrace();
 +      }
 +    }
 +  }
 +
 +  private void notifyExceptional(AlignCalcWorkerI worker,
 +          Throwable throwable)
 +  {
 +    for (AlignCalcListener listener : listeners)
 +    {
 +      try
 +      {
 +        listener.workerExceptional(worker, throwable);
 +      } catch (RuntimeException e)
 +      {
 +        e.printStackTrace();
 +      }
 +    }
 +  }
 +
 +  @Override
 +  public void addAlignCalcListener(AlignCalcListener listener)
 +  {
 +    listeners.add(listener);
 +  }
 +
 +  @Override
 +  public void removeAlignCalcListener(AlignCalcListener listener)
 +  {
 +    listeners.remove(listener);
 +  }
 +
 +  @Override
 +  public void shutdown()
 +  {
 +    executor.shutdownNow();
 +    listeners.clear();
 +    registered.clear();
 +  }
 +
 +}
@@@ -110,90 -104,104 +110,91 @@@ public abstract class AWSThrea
      if (jobs == null)
      {
        jobComplete = true;
-       Cache.log.debug(
++      Console.debug(
 +          "WebServiceJob poll loop finished with no jobs created.");
 +      wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
 +      wsInfo.appendProgressText(
 +          MessageManager.getString("info.no_jobs_ran"));
 +      wsInfo.setFinishedNoResults();
 +      return;
      }
 -    while (!jobComplete)
 +    timer = new Timer(5000, new ActionListener()
      {
 -      jstate = new JobStateSummary();
 -      for (int j = 0; j < jobs.length; j++)
 +      
 +      @Override
 +      public void actionPerformed(ActionEvent e)
        {
 -        if (!jobs[j].submitted && jobs[j].hasValidInput())
 -        {
 -          StartJob(jobs[j]);
 -        }
 -
 -        if (jobs[j].submitted && !jobs[j].subjobComplete)
 +        JobStateSummary jstate = new JobStateSummary();
 +        for (final AWsJob job : jobs)
          {
 -          try
 +          if (!job.submitted && job.hasValidInput())
            {
 -            pollJob(jobs[j]);
 -            if (!jobs[j].hasResponse())
 -            {
 -              throw (new Exception(
 -                      "Timed out when communicating with server\nTry again later.\n"));
 -            }
 -            Console.debug("Job " + j + " Result state " + jobs[j].getState()
 -                    + "(ServerError=" + jobs[j].isServerError() + ")");
 -          } catch (Exception ex)
 +            StartJob(job);
 +          }
-           Cache.log.debug(format(
++          Console.debug(format(
 +                  "Job %s is %ssubmitted", job, job.submitted ? "" : "not "));
 +          if (job.submitted && !job.subjobComplete)
            {
-             Cache.log.debug(format(
 -            // Deal with Transaction exceptions
 -            wsInfo.appendProgressText(jobs[j].jobnum, MessageManager
 -                    .formatMessage("info.server_exception", new Object[]
 -                    { WebServiceName, ex.getMessage() }));
 -            // always output the exception's stack trace to the log
 -            Console.warn(WebServiceName + " job(" + jobs[j].jobnum
 -                    + ") Server exception.");
 -            // todo: could limit trace to cause if this is a SOAPFaultException.
 -            ex.printStackTrace();
 -
 -            if (jobs[j].allowedServerExceptions > 0)
++            Console.debug(format(
 +                    "Polling Job %s Result state was:%s(ServerError=%b)",
 +                    job, job.getState(), job.isServerError()));
 +            try
 +            {
 +              pollJob(job);
 +              if (!job.hasResponse())
 +                throw new Exception("Timed out when communicating with server. Try again later.");
 +              else
-                 Cache.log.debug(format("Job %s Result state:%s(ServerError=%b)",
++                Console.debug(format("Job %s Result state:%s(ServerError=%b)",
 +                        job, job.getState(), job.isServerError()));
 +            } catch (Exception exc)
              {
 -              jobs[j].allowedServerExceptions--;
 -              Console.debug("Sleeping after a server exception.");
 -              try
 +              // Deal with Transaction exceptions
 +              wsInfo.appendProgressText(job.jobnum, MessageManager
 +                      .formatMessage("info.server_exception", WebServiceName,
 +                          exc.getMessage()));
 +              // always output the exception's stack trace to the log
-               Cache.log.warn(format("%s job(%s) Server exception.",
++              Console.warn(format("%s job(%s) Server exception.",
 +                      WebServiceName, job.jobnum));
 +              exc.printStackTrace();
 +              
 +              if (job.allowedServerExceptions > 0)
                {
 -                Thread.sleep(5000);
 -              } catch (InterruptedException ex1)
 +                job.allowedServerExceptions--;
 +              }
 +              else
                {
-                 Cache.log.warn(format("Dropping job %s %s", job, job.jobId));
++                Console.warn(format("Dropping job %s %s", job, job.jobId));
 +                job.subjobComplete = true;
 +                wsInfo.setStatus(job.jobnum, WebserviceInfo.STATE_STOPPED_SERVERERROR);
                }
 -            }
 -            else
 +            } catch (OutOfMemoryError oomerror)
              {
 -              Console.warn("Dropping job " + j + " " + jobs[j].jobId);
 -              jobs[j].subjobComplete = true;
 -              wsInfo.setStatus(jobs[j].jobnum,
 -                      WebserviceInfo.STATE_STOPPED_SERVERERROR);
 +              jobComplete = true;
 +              job.subjobComplete = true;
 +              job.clearResponse();
 +              wsInfo.setStatus(job.jobnum, WebserviceInfo.STATE_STOPPED_ERROR);
-               Cache.log.error(format("Out of memory when retrieving Job %s id:%s/%s",
++              Console.error(format("Out of memory when retrieving Job %s id:%s/%s",
 +                      job, WsUrl, job.jobId), oomerror);
 +              new jalview.gui.OOMWarning("retrieving result for " + WebServiceName, oomerror);
 +              System.gc();
              }
 -          } catch (OutOfMemoryError er)
 -          {
 -            jobComplete = true;
 -            jobs[j].subjobComplete = true;
 -            jobs[j].clearResponse(); // may contain out of date result data
 -            wsInfo.setStatus(jobs[j].jobnum,
 -                    WebserviceInfo.STATE_STOPPED_ERROR);
 -            Console.error("Out of memory when retrieving Job " + j + " id:"
 -                    + WsUrl + "/" + jobs[j].jobId, er);
 -            new jalview.gui.OOMWarning(
 -                    "retrieving result for " + WebServiceName, er);
 -            System.gc();
            }
 +          jstate.updateJobPanelState(wsInfo, OutputHeader, job);
          }
 -        jstate.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
 -      }
 -      // Decide on overall state based on collected jobs[] states
 -      updateGlobalStatus(jstate);
 -      if (!jobComplete)
 -      {
 -        try
 +        // Decide on overall state based on collected jobs[] states
 +        updateGlobalStatus(jstate);
 +        if (jobComplete)
          {
 -          Thread.sleep(5000);
 -        } catch (InterruptedException e)
 -        {
 -          Console.debug("Interrupted sleep waiting for next job poll.", e);
 +          timer.stop();
 +          // jobs should never be null at this point
 +          parseResult(); // tidy up and make results available to user
 +          
          }
 -        // System.out.println("I'm alive "+alTitle);
        }
 -    }
 -    if (jobComplete && jobs != null)
 -    {
 -      parseResult(); // tidy up and make results available to user
 -    }
 -    else
 -    {
 -      Console.debug(
 -              "WebServiceJob poll loop finished with no jobs created.");
 -      wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
 -      wsInfo.appendProgressText(
 -              MessageManager.getString("info.no_jobs_ran"));
 -      wsInfo.setFinishedNoResults();
 -    }
 +    });
 +    timer.setInitialDelay(0);
 +    timer.start();
    }
  
    protected void updateGlobalStatus(JobStateSummary jstate)
        }
      }
    }
 -  public AWSThread()
 -  {
 -    super();
 -  }
 -
 -  public AWSThread(Runnable target)
 -  {
 -    super(target);
 -  }
 -
 -  public AWSThread(String name)
 -  {
 -    super(name);
 -  }
 -
 -  public AWSThread(ThreadGroup group, Runnable target)
 +  
 +  public void interrupt()
    {
 -    super(group, target);
 -  }
 -
 -  public AWSThread(ThreadGroup group, String name)
 -  {
 -    super(group, name);
 -  }
 -
 -  public AWSThread(Runnable target, String name)
 -  {
 -    super(target, name);
 -  }
 -
 -  public AWSThread(ThreadGroup group, Runnable target, String name)
 -  {
 -    super(group, target, name);
 +    timer.stop();
    }
  
    /**
      }
    }
  
 -  public AWSThread(ThreadGroup group, Runnable target, String name,
 -          long stackSize)
 -  {
 -    super(group, target, name, stackSize);
 -  }
    /**
     * 
     * @return gap character to use for any alignment generation
Simple merge
index 645ef34,0000000..be24716
mode 100644,000000..100644
--- /dev/null
@@@ -1,111 -1,0 +1,112 @@@
 +package jalview.ws.api;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.gui.AlignFrame;
 +import jalview.ws.jws2.MsaWSClient;
 +import jalview.ws.jws2.SequenceAnnotationWSClient;
 +import jalview.ws.params.ParamManager;
 +
 +import javax.swing.JMenu;
 +
 +public abstract class ServiceWithParameters extends UIinfo
 +{
 +
 +  protected jalview.ws.uimodel.AlignAnalysisUIText aaui;
 +
 +  public ServiceWithParameters(String serviceType, String action,
 +          String name, String description, String hosturl)
 +  {
 +    super(serviceType, action, name, description, hosturl);
 +  }
 +
 +  public abstract void initParamStore(ParamManager userParameterStore);
 +
 +  public jalview.ws.uimodel.AlignAnalysisUIText getAlignAnalysisUI()
 +  {
 +    return aaui;
 +  }
 +
 +  public void setAlignAnalysisUI(
 +          jalview.ws.uimodel.AlignAnalysisUIText aaui)
 +  {
 +    this.aaui = aaui;
 +  }
 +
 +  public boolean isInteractiveUpdate()
 +  {
 +    return aaui != null && aaui.isAA();
 +  }
 +  // config flags for SeqAnnotationServiceCalcWorker
 +
 +  public boolean isProteinService()
 +  {
 +    return aaui == null ? true : aaui.isPr();
 +  }
 +
 +  public boolean isNucleotideService()
 +  {
 +    return aaui == null ? false : aaui.isNa();
 +  }
 +
 +  public boolean isNeedsAlignedSequences()
 +  {
 +    return aaui == null ? false : aaui.isNeedsAlignedSeqs();
 +  }
 +
 +  public boolean isAlignmentAnalysis()
 +  {
 +    return aaui == null ? false : aaui.isAA();
 +  }
 +
 +  public boolean isFilterSymbols()
 +  {
 +    return aaui != null ? aaui.isFilterSymbols() : true;
 +  }
 +
 +  public int getMinimumInputSequences()
 +  {
 +    return aaui != null ? aaui.getMinimumSequences() : 1;
 +  }
 +
 +  public String getNameURI()
 +  {
 +    return "java:" + getName();
 +  }
 +
 +  public String getUri()
 +  {
 +    // TODO verify that service parameter sets in projects are consistent with
 +    // Jalview 2.10.4
 +    // this is only valid for Jaba 1.0 - this formula might have to change!
 +    return getHostURL()
 +            + (getHostURL().lastIndexOf("/") == (getHostURL().length() - 1)
 +                    ? ""
 +                    : "/")
 +            + getName();
 +  }
 +
 +  protected enum ServiceClient
 +  {
 +    MSAWSCLIENT, SEQUENCEANNOTATIONWSCLIENT;
 +  };
 +
 +  protected ServiceClient style = null;
 +
 +  public void attachWSMenuEntry(JMenu atpoint, AlignFrame alignFrame)
 +  {
 +    switch (style)
 +    {
 +    case MSAWSCLIENT:
 +        new MsaWSClient().attachWSMenuEntry(atpoint, this, alignFrame);
 +      break;
 +    case SEQUENCEANNOTATIONWSCLIENT:
 +        new SequenceAnnotationWSClient().attachWSMenuEntry(atpoint, this,
 +                alignFrame);
 +      break;
 +    default:
-       Cache.log.warn("Implementation error ? Service " + getName()
++      Console.warn("Implementation error ? Service " + getName()
 +              + " has Unknown service style " + style);
 +    }
 +  }
 +}
@@@ -14,6 -34,6 +34,7 @@@ import jalview.datamodel.SequenceI
  import jalview.io.DataSourceType;
  import jalview.io.EmblFlatFile;
  import jalview.io.FileParse;
++import jalview.util.Platform;
  import jalview.ws.ebi.EBIFetchClient;
  
  /**
@@@ -24,7 -44,7 +45,7 @@@
   */
  public abstract class EmblFlatfileSource extends EbiFileRetrievedProxy
  {
-   private static final Regex ACCESSION_REGEX = null;
 -  private static final Regex ACCESSION_REGEX = new Regex("^[A-Z]+[0-9]+");
++  private static Regex ACCESSION_REGEX = null;
  
    @Override
    public String getDbVersion()
@@@ -41,8 -40,10 +41,9 @@@ import javax.xml.stream.XMLInputFactory
  import javax.xml.stream.XMLStreamException;
  import javax.xml.stream.XMLStreamReader;
  
 -import com.stevesoft.pat.Regex;
  import jalview.analysis.SequenceIdMatcher;
- import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.DBRefEntry;
@@@ -56,7 -57,6 +57,8 @@@ import jalview.util.DBRefUtils
  import jalview.util.DnaUtils;
  import jalview.util.MapList;
  import jalview.util.MappingUtils;
 +import jalview.util.MessageManager;
++import jalview.util.Platform;
  import jalview.ws.ebi.EBIFetchClient;
  import jalview.xml.binding.embl.EntryType;
  import jalview.xml.binding.embl.EntryType.Feature;
@@@ -64,17 -64,10 +66,20 @@@ import jalview.xml.binding.embl.EntryTy
  import jalview.xml.binding.embl.ROOT;
  import jalview.xml.binding.embl.XrefType;
  
++import com.stevesoft.pat.Regex;
++
 +/**
 + * Provides XML binding and parsing of EMBL or EMBLCDS records retrieved from
 + * (e.g.) {@code https://www.ebi.ac.uk/ena/data/view/x53828&display=xml}.
 + * 
 + * @deprecated endpoint withdrawn August 2020 (JAL-3692), use EmblFlatfileSource
 + */
++@Deprecated
  public abstract class EmblXmlSource extends EbiFileRetrievedProxy
  {
 -  private static final Regex ACCESSION_REGEX = new Regex("^[A-Z]+[0-9]+");
 +  // TODO: delete class or update tyhis validator for 2.12 style Platform.regex
-   private static final Regex ACCESSION_REGEX = new Regex("^[A-Z]+[0-9]+");
++    private static final Regex ACCESSION_REGEX = Platform.newRegex("^[A-Z]+[0-9]+");
    /*
     * JAL-1856 Embl returns this text for query not found
     */
Simple merge
@@@ -31,6 -32,6 +31,7 @@@ import jalview.datamodel.Sequence
  import jalview.datamodel.SequenceFeature;
  import jalview.datamodel.SequenceI;
  import jalview.schemes.ResidueProperties;
++import jalview.util.Platform;
  import jalview.util.StringUtils;
  import jalview.ws.seqfetcher.DbSourceProxyImpl;
  import jalview.xml.binding.uniprot.DbReferenceType;
@@@ -159,17 -153,21 +159,22 @@@ public class Uniprot extends DbSourcePr
        // use/backoff logic to retry when the server tells us to go away
        if (urlconn.getResponseCode() == 200)
        {
-         List<SequenceI> seqs = new ArrayList<>();
-         for (Entry entry : entries)
+         InputStream istr = urlconn.getInputStream();
+         List<Entry> entries = getUniprotEntries(istr);
+         if (entries != null)
          {
-           seqs.add(uniprotEntryToSequence(entry));
+           List<SequenceI> seqs = new ArrayList<>();
+           for (Entry entry : entries)
+           {
+             seqs.add(uniprotEntryToSequence(entry));
+           }
+           al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
          }
-         al = new Alignment(seqs.toArray(new SequenceI[seqs.size()]));
        }
 +
        stopQuery();
        return al;
-       
      } catch (Exception e)
      {
        throw (e);
            dbRefs.add(dbr);
          }
        }
--      if ("Ensembl".equals(type))
++      // from 2.11.2.6 - probably see a conflict here
++      if (type != null
++              && type.toLowerCase(Locale.ROOT).startsWith("ensembl"))
        {
++        // remove version
++        String[] vrs = dbref.getId().split("\\.");
++        String version = vrs.length > 1 ? vrs[1]
++                : DBRefSource.UNIPROT + ":" + dbVersion;
++        dbr.setAccessionId(vrs[0]);
++        dbr.setVersion(version);
          /*
           * e.g. Uniprot accession Q9BXM7 has
           * <dbReference type="Ensembl" id="ENST00000321556">
                  "protein sequence ID");
          if (cdsId != null && cdsId.trim().length() > 0)
          {
++          // remove version
++          String[] cdsVrs = cdsId.split("\\.");
++          String cdsVersion = cdsVrs.length > 1 ? cdsVrs[1]
++                  : DBRefSource.UNIPROT + ":" + dbVersion;
            dbr = new DBRefEntry(DBRefSource.ENSEMBL,
--                  DBRefSource.UNIPROT + ":" + dbVersion, cdsId.trim());
++                  DBRefSource.UNIPROT + ":" + cdsVersion, cdsVrs[0]);
            dbRefs.add(dbr);
          }
        }
   * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
   * The Jalview Authors are detailed in the 'AUTHORS' file.
   */
 -package jalview.ws.jws2;
 +package jalview.ws.gui;
  
 -import jalview.analysis.AlignSeq;
 +import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentI;
  import jalview.datamodel.AlignmentOrder;
@@@ -58,6 -64,376 +59,7 @@@ public class MsaWSThread extends AWSThr
  
    // order
  
 -  class MsaWSJob extends JWs2Job
 -  {
 -    long lastChunk = 0;
 -
 -    WsParamSetI preset = null;
 -
 -    List<Argument> arguments = null;
 -
 -    /**
 -     * input
 -     */
 -    ArrayList<compbio.data.sequence.FastaSequence> seqs = new ArrayList<compbio.data.sequence.FastaSequence>();
 -
 -    /**
 -     * output
 -     */
 -    compbio.data.sequence.Alignment alignment;
 -
 -    // set if the job didn't get run - then the input is simply returned to the
 -    // user
 -    private boolean returnInput = false;
 -
 -    /**
 -     * MsaWSJob
 -     * 
 -     * @param jobNum
 -     *          int
 -     * @param jobId
 -     *          String
 -     */
 -    public MsaWSJob(int jobNum, SequenceI[] inSeqs)
 -    {
 -      this.jobnum = jobNum;
 -      if (!prepareInput(inSeqs, 2))
 -      {
 -        submitted = true;
 -        subjobComplete = true;
 -        returnInput = true;
 -      }
 -
 -    }
 -
 -    Hashtable<String, Map> SeqNames = new Hashtable();
 -
 -    Vector<String[]> emptySeqs = new Vector();
 -
 -    /**
 -     * prepare input sequences for MsaWS service
 -     * 
 -     * @param seqs
 -     *          jalview sequences to be prepared
 -     * @param minlen
 -     *          minimum number of residues required for this MsaWS service
 -     * @return true if seqs contains sequences to be submitted to service.
 -     */
 -    // TODO: return compbio.seqs list or nothing to indicate validity.
 -    private boolean prepareInput(SequenceI[] seqs, int minlen)
 -    {
 -      int nseqs = 0;
 -      if (minlen < 0)
 -      {
 -        throw new Error(MessageManager.getString(
 -                "error.implementation_error_minlen_must_be_greater_zero"));
 -      }
 -      for (int i = 0; i < seqs.length; i++)
 -      {
 -        if (seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
 -        {
 -          nseqs++;
 -        }
 -      }
 -      boolean valid = nseqs > 1; // need at least two seqs
 -      compbio.data.sequence.FastaSequence seq;
 -      for (int i = 0, n = 0; i < seqs.length; i++)
 -      {
 -
 -        String newname = jalview.analysis.SeqsetUtils.unique_name(i); // same
 -        // for
 -        // any
 -        // subjob
 -        SeqNames.put(newname,
 -                jalview.analysis.SeqsetUtils.SeqCharacterHash(seqs[i]));
 -        if (valid && seqs[i].getEnd() - seqs[i].getStart() > minlen - 1)
 -        {
 -          // make new input sequence with or without gaps
 -          seq = new compbio.data.sequence.FastaSequence(newname,
 -                  (submitGaps) ? seqs[i].getSequenceAsString()
 -                          : AlignSeq.extractGaps(
 -                                  jalview.util.Comparison.GapChars,
 -                                  seqs[i].getSequenceAsString()));
 -          this.seqs.add(seq);
 -        }
 -        else
 -        {
 -          String empty = null;
 -          if (seqs[i].getEnd() >= seqs[i].getStart())
 -          {
 -            empty = (submitGaps) ? seqs[i].getSequenceAsString()
 -                    : AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
 -                            seqs[i].getSequenceAsString());
 -          }
 -          emptySeqs.add(new String[] { newname, empty });
 -        }
 -      }
 -      return valid;
 -    }
 -
 -    /**
 -     * 
 -     * @return true if getAlignment will return a valid alignment result.
 -     */
 -    @Override
 -    public boolean hasResults()
 -    {
 -      if (subjobComplete && isFinished() && (alignment != null
 -              || (emptySeqs != null && emptySeqs.size() > 0)))
 -      {
 -        return true;
 -      }
 -      return false;
 -    }
 -
 -    /**
 -     * 
 -     * get the alignment including any empty sequences in the original order
 -     * with original ids. Caller must access the alignment.getMetadata() object
 -     * to annotate the final result passsed to the user.
 -     * 
 -     * @return { SequenceI[], AlignmentOrder }
 -     */
 -    public Object[] getAlignment()
 -    {
 -      // is this a generic subjob or a Jws2 specific Object[] return signature
 -      if (hasResults())
 -      {
 -        SequenceI[] alseqs = null;
 -        char alseq_gapchar = '-';
 -        int alseq_l = 0;
 -        if (alignment.getSequences().size() > 0)
 -        {
 -          alseqs = new SequenceI[alignment.getSequences().size()];
 -          for (compbio.data.sequence.FastaSequence seq : alignment
 -                  .getSequences())
 -          {
 -            alseqs[alseq_l++] = new Sequence(seq.getId(),
 -                    seq.getSequence());
 -          }
 -          alseq_gapchar = alignment.getMetadata().getGapchar();
 -
 -        }
 -        // add in the empty seqs.
 -        if (emptySeqs.size() > 0)
 -        {
 -          SequenceI[] t_alseqs = new SequenceI[alseq_l + emptySeqs.size()];
 -          // get width
 -          int i, w = 0;
 -          if (alseq_l > 0)
 -          {
 -            for (i = 0, w = alseqs[0].getLength(); i < alseq_l; i++)
 -            {
 -              if (w < alseqs[i].getLength())
 -              {
 -                w = alseqs[i].getLength();
 -              }
 -              t_alseqs[i] = alseqs[i];
 -              alseqs[i] = null;
 -            }
 -          }
 -          // check that aligned width is at least as wide as emptySeqs width.
 -          int ow = w, nw = w;
 -          for (i = 0, w = emptySeqs.size(); i < w; i++)
 -          {
 -            String[] es = emptySeqs.get(i);
 -            if (es != null && es[1] != null)
 -            {
 -              int sw = es[1].length();
 -              if (nw < sw)
 -              {
 -                nw = sw;
 -              }
 -            }
 -          }
 -          // make a gapped string.
 -          StringBuffer insbuff = new StringBuffer(w);
 -          for (i = 0; i < nw; i++)
 -          {
 -            insbuff.append(alseq_gapchar);
 -          }
 -          if (ow < nw)
 -          {
 -            for (i = 0; i < alseq_l; i++)
 -            {
 -              int sw = t_alseqs[i].getLength();
 -              if (nw > sw)
 -              {
 -                // pad at end
 -                alseqs[i].setSequence(t_alseqs[i].getSequenceAsString()
 -                        + insbuff.substring(0, sw - nw));
 -              }
 -            }
 -          }
 -          for (i = 0, w = emptySeqs.size(); i < w; i++)
 -          {
 -            String[] es = emptySeqs.get(i);
 -            if (es[1] == null)
 -            {
 -              t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(es[0],
 -                      insbuff.toString(), 1, 0);
 -            }
 -            else
 -            {
 -              if (es[1].length() < nw)
 -              {
 -                t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
 -                        es[0],
 -                        es[1] + insbuff.substring(0, nw - es[1].length()),
 -                        1, 1 + es[1].length());
 -              }
 -              else
 -              {
 -                t_alseqs[i + alseq_l] = new jalview.datamodel.Sequence(
 -                        es[0], es[1]);
 -              }
 -            }
 -          }
 -          alseqs = t_alseqs;
 -        }
 -        AlignmentOrder msaorder = new AlignmentOrder(alseqs);
 -        // always recover the order - makes parseResult()'s life easier.
 -        jalview.analysis.AlignmentSorter.recoverOrder(alseqs);
 -        // account for any missing sequences
 -        jalview.analysis.SeqsetUtils.deuniquify(SeqNames, alseqs);
 -        return new Object[] { alseqs, msaorder };
 -      }
 -      return null;
 -    }
 -
 -    /**
 -     * mark subjob as cancelled and set result object appropriatly
 -     */
 -    void cancel()
 -    {
 -      cancelled = true;
 -      subjobComplete = true;
 -      alignment = null;
 -    }
 -
 -    /**
 -     * 
 -     * @return boolean true if job can be submitted.
 -     */
 -    @Override
 -    public boolean hasValidInput()
 -    {
 -      // TODO: get attributes for this MsaWS instance to check if it can do two
 -      // sequence alignment.
 -      if (seqs != null && seqs.size() >= 2) // two or more sequences is valid ?
 -      {
 -        return true;
 -      }
 -      return false;
 -    }
 -
 -    StringBuffer jobProgress = new StringBuffer();
 -
 -    public void setStatus(String string)
 -    {
 -      jobProgress.setLength(0);
 -      jobProgress.append(string);
 -    }
 -
 -    @Override
 -    public String getStatus()
 -    {
 -      return jobProgress.toString();
 -    }
 -
 -    @Override
 -    public boolean hasStatus()
 -    {
 -      return jobProgress != null;
 -    }
 -
 -    /**
 -     * @return the lastChunk
 -     */
 -    public long getLastChunk()
 -    {
 -      return lastChunk;
 -    }
 -
 -    /**
 -     * @param lastChunk
 -     *          the lastChunk to set
 -     */
 -    public void setLastChunk(long lastChunk)
 -    {
 -      this.lastChunk = lastChunk;
 -    }
 -
 -    String alignmentProgram = null;
 -
 -    public String getAlignmentProgram()
 -    {
 -      return alignmentProgram;
 -    }
 -
 -    public boolean hasArguments()
 -    {
 -      return (arguments != null && arguments.size() > 0)
 -              || (preset != null && preset instanceof JabaWsParamSet);
 -    }
 -
 -    public List<Argument> getJabaArguments()
 -    {
 -      List<Argument> newargs = new ArrayList<Argument>();
 -      if (preset != null && preset instanceof JabaWsParamSet)
 -      {
 -        newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
 -      }
 -      if (arguments != null && arguments.size() > 0)
 -      {
 -        newargs.addAll(arguments);
 -      }
 -      return newargs;
 -    }
 -
 -    /**
 -     * add a progess header to status string containing presets/args used
 -     */
 -    public void addInitialStatus()
 -    {
 -      if (preset != null)
 -      {
 -        jobProgress.append("Using "
 -                + (preset instanceof JabaPreset ? "Server" : "User")
 -                + "Preset: " + preset.getName());
 -        if (preset instanceof JabaWsParamSet)
 -        {
 -          for (Argument opt : ((JabaWsParamSet) preset).getjabaArguments())
 -          {
 -            jobProgress.append(
 -                    opt.getName() + " " + opt.getDefaultValue() + "\n");
 -          }
 -        }
 -      }
 -      if (arguments != null && arguments.size() > 0)
 -      {
 -        jobProgress.append("With custom parameters : \n");
 -        // merge arguments with preset's own arguments.
 -        for (Argument opt : arguments)
 -        {
 -          jobProgress.append(
 -                  opt.getName() + " " + opt.getDefaultValue() + "\n");
 -        }
 -      }
 -      jobProgress.append("\nJob Output:\n");
 -    }
 -
 -    public boolean isPresetJob()
 -    {
 -      return preset != null && preset instanceof JabaPreset;
 -    }
 -
 -    public Preset getServerPreset()
 -    {
 -      return (isPresetJob()) ? ((JabaPreset) preset).p : null;
 -    }
 -  }
    String alTitle; // name which will be used to form new alignment window.
  
    AlignmentI dataset; // dataset to which the new alignment will be
      {
        j.addInitialStatus(); // list the presets/parameters used for the job in
                              // status
 -      if (j.isPresetJob())
 -      {
 -        j.setJobId(server.presetAlign(j.seqs, j.getServerPreset()));
 -      }
 -      else if (j.hasArguments())
 +      try
        {
 -        j.setJobId(server.customAlign(j.seqs, j.getJabaArguments()));
 -      }
 -      else
 +        JobId jobHandle = server.align(j.seqs, j.getPreset(),
 +                j.getArguments());
 +        if (jobHandle != null)
 +        {
 +          j.setJobHandle(jobHandle);
 +        }
 +
 +      } catch (Throwable throwable)
        {
-         Cache.log.error("failed to send the job to the alignment server", throwable);
 -        j.setJobId(server.align(j.seqs));
++        Console.error("failed to send the job to the alignment server", throwable);
 +        if (!server.handleSubmitError(throwable, j, wsInfo))
 +        {
 +          if (throwable instanceof Exception)
 +          {
 +            throw ((Exception) throwable);
 +          }
 +          if (throwable instanceof Error)
 +          {
 +            throw ((Error) throwable);
 +          }
 +        }
        }
 +      ///// generic
        if (j.getJobId() != null)
        {
          j.setSubmitted(true);
              System.out.println("*** End of status");
  
            }
 +          ///// jabaws specific(ish) Get Result from Server when available
            try
            {
 -            msjob.alignment = server.getResult(msjob.getJobId());
 -          } catch (compbio.metadata.ResultNotAvailableException e)
 -          {
 -            // job has failed for some reason - probably due to invalid
 -            // parameters
 -            Console.debug(
 -                    "Results not available for finished job - marking as broken job.",
 -                    e);
 -            msjob.jobProgress.append(
 -                    "\nResult not available. Probably due to invalid input or parameter settings. Server error message below:\n\n"
 -                            + e.getLocalizedMessage());
 -            msjob.setjobStatus(JobStatus.FAILED);
 +            msjob.alignment = server.getAlignmentFor(msjob.getJobHandle());
            } catch (Exception e)
            {
 -            Console.error("Couldn't get Alignment for job.", e);
 -            // TODO: Increment count and retry ?
 -            msjob.setjobStatus(JobStatus.UNDEFINED);
 +            if (!server.handleCollectionException(e, msjob, wsInfo))
 +            {
-               Cache.log.error("Couldn't get Alignment for job.", e);
++              Console.error("Couldn't get Alignment for job.", e);
 +              // TODO: Increment count and retry ?
 +              msjob.setState(JobState.SERVERERROR);
 +            }
            }
          }
          finalState.updateJobPanelState(wsInfo, OutputHeader, jobs[j]);
   */
  package jalview.ws.jws1;
  
 -import jalview.bin.Cache;
 -import jalview.bin.Console;
 -import jalview.gui.JvOptionPane;
 -import jalview.util.MessageManager;
  import java.net.URL;
  import java.util.Hashtable;
  import java.util.StringTokenizer;
@@@ -30,25 -35,9 +31,24 @@@ import ext.vamsas.IRegistryServiceLocat
  import ext.vamsas.RegistryServiceSoapBindingStub;
  import ext.vamsas.ServiceHandle;
  import ext.vamsas.ServiceHandles;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
 +import jalview.gui.JvOptionPane;
 +import jalview.util.MessageManager;
  
 -public class Discoverer implements Runnable
 +public class Discoverer implements Runnable, ApplicationSingletonI
  {
 +  public static Discoverer getInstance()
 +  {
 +    return (Discoverer) ApplicationSingletonProvider.getInstance(Discoverer.class);
 +  }
 +
 +  private Discoverer()
 +  {
 +    // use getInstance()
 +  }
    ext.vamsas.IRegistry registry; // the root registry service.
  
    private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
    // abstractServiceType
    // string
  
 -  public static java.util.Vector<ServiceHandle> serviceList = null;
 +  public java.util.Vector<ServiceHandle> serviceList = null;
  
 -  static private Vector<URL> getDiscoveryURLS()
 +  private Vector<URL> getDiscoveryURLS()
    {
      Vector<URL> urls = new Vector<>();
-     String RootServiceURLs = jalview.bin.Cache.getDefault("DISCOVERY_URLS",
+     String RootServiceURLs = Cache.getDefault("DISCOVERY_URLS",
              "http://www.compbio.dundee.ac.uk/JalviewWS/services/ServiceRegistry");
  
      try
     */
    static public void doDiscovery()
    {
 -    Console.debug("(Re)-Initialising the discovery URL list.");
 +    getInstance().discovery();
 +  }
 +
 +  private void discovery()
 +  {
-     jalview.bin.Cache.log
++    Console
 +            .debug("(Re)-Initialising the discovery URL list.");
      try
      {
 -      reallyDiscoverServices = Cache.getDefault("DISCOVERY_START", false);
 +      Discoverer d = getInstance();
-       reallyDiscoverServices = jalview.bin.Cache
++      reallyDiscoverServices = Cache
 +              .getDefault("DISCOVERY_START", false);
        if (reallyDiscoverServices)
        {
          ServiceURLList = getDiscoveryURLS();
    @Override
    public void run()
    {
-     Cache.log.info("Discovering jws1 services");
 -    final Discoverer discoverer = this;
 -    Thread discoverThread = new Thread()
 -    {
 -      @Override
 -      public void run()
 -      {
 -        Discoverer.doDiscovery();
 -        discoverer.discoverServices();
 -      }
 -    };
 -    discoverThread.start();
++    Console.info("Discovering jws1 services");
 +    Discoverer.doDiscovery();
 +    discoverServices();
    }
  
    /**
      }
      return instance;
    }
 -  /**
 -   * notes on discovery service 1. need to allow multiple discovery source urls.
 -   * 2. user interface to add/control list of urls in preferences notes on
 -   * wsclient discovery 1. need a classpath property with list of additional
 -   * plugin directories 2. optional config to cite specific bindings between
 -   * class name and Abstract service name. 3. precedence for automatic discovery
 -   * by using getAbstractName for WSClient - user added plugins override default
 -   * plugins ? notes on wsclient gui code for gui attachment now moved to
 -   * wsclient implementation. Needs more abstraction but approach seems to work.
 -   * is it possible to 'generalise' the data retrieval calls further ? current
 -   * methods are very specific (gatherForMSA or gatherForSeqOrMsaSecStrPred),
 -   * new methods for conservation (group or alignment), treecalc (aligned
 -   * profile), seqannot (sequences selected from dataset, annotation back to
 -   * dataset).
 -   * 
 -   */
 +  public static Hashtable<String, Vector<ServiceHandle>> getServices()
 +  {
 +    return getInstance().services;
 +  }
  }
  package jalview.ws.jws1;
  
  import java.util.Locale;
--
  import jalview.analysis.AlignSeq;
 +import jalview.analysis.SeqsetUtils.SequenceInfo;
- import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.datamodel.AlignmentView;
  import jalview.datamodel.SeqCigar;
  import jalview.datamodel.SequenceI;
@@@ -22,8 -22,7 +22,8 @@@ package jalview.ws.jws1
  
  import jalview.analysis.AlignSeq;
  import jalview.analysis.SeqsetUtils;
 +import jalview.analysis.SeqsetUtils.SequenceInfo;
- import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.datamodel.Alignment;
  import jalview.datamodel.AlignmentAnnotation;
  import jalview.datamodel.AlignmentI;
Simple merge
   */
  package jalview.ws.jws2;
  
 -import java.awt.event.ActionEvent;
 -import java.awt.event.ActionListener;
 +import jalview.gui.AlignFrame;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.WsParamSetI;
  import java.util.List;
  
 -import javax.swing.JCheckBoxMenuItem;
  import javax.swing.JMenu;
 -import javax.swing.JMenuItem;
 -import javax.swing.event.MenuEvent;
 -import javax.swing.event.MenuListener;
 -
 -import compbio.metadata.Argument;
 -import jalview.api.AlignCalcWorkerI;
 -import jalview.bin.Console;
 -import jalview.gui.AlignFrame;
 -import jalview.gui.Desktop;
 -import jalview.gui.JvSwingUtils;
 -import jalview.gui.WebserviceInfo;
 -import jalview.gui.WsJobParameters;
 -import jalview.util.MessageManager;
 -import jalview.ws.jws2.dm.AAConSettings;
 -import jalview.ws.jws2.dm.JabaWsParamSet;
 -import jalview.ws.jws2.jabaws2.Jws2Instance;
 -import jalview.ws.params.WsParamSetI;
 -import jalview.ws.uimodel.AlignAnalysisUIText;
  
  /**
   * provides metadata for a jabaws2 service instance - resolves names, etc.
index 2456528,0000000..4d78bd7
mode 100644,000000..100644
--- /dev/null
@@@ -1,292 -1,0 +1,293 @@@
 +package jalview.ws.jws2;
 +
 +import jalview.api.AlignCalcWorkerI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.JvSwingUtils;
 +import jalview.util.MessageManager;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.jws2.dm.AAConSettings;
 +import jalview.ws.jws2.jabaws2.Jws2Instance;
 +import jalview.ws.params.AutoCalcSetting;
 +import jalview.ws.uimodel.AlignAnalysisUIText;
 +
 +import java.awt.event.ActionEvent;
 +import java.awt.event.ActionListener;
 +import java.util.List;
 +
 +import javax.swing.JCheckBoxMenuItem;
 +import javax.swing.JMenu;
 +import javax.swing.JMenuItem;
 +import javax.swing.event.MenuEvent;
 +import javax.swing.event.MenuListener;
 +
 +public class Jws2ClientFactory
 +{
 +  static boolean registerAAConWSInstance(final JMenu wsmenu,
 +          final ServiceWithParameters service, final AlignFrame alignFrame)
 +  {
 +    final AlignAnalysisUIText aaui = service.getAlignAnalysisUI(); // null
 +                                                                        // ; //
 +    // AlignAnalysisUIText.aaConGUI.get(service.serviceType.toString());
 +    if (aaui == null)
 +    {
 +      // not an instantaneous calculation GUI type service
 +      return false;
 +    }
 +    // create the instaneous calculation GUI bits and update state if existing
 +    // GUI elements already present
 +
 +    JCheckBoxMenuItem _aaConEnabled = null;
 +    for (int i = 0; i < wsmenu.getItemCount(); i++)
 +    {
 +      JMenuItem item = wsmenu.getItem(i);
 +      if (item instanceof JCheckBoxMenuItem
 +              && item.getText().equals(aaui.getAAconToggle()))
 +      {
 +        _aaConEnabled = (JCheckBoxMenuItem) item;
 +      }
 +    }
 +    // is there an aaCon worker already present - if so, set it to use the
 +    // given service handle
 +    {
 +      List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 +              .getCalcManager()
 +              .getWorkersOfClass(aaui.getClient());
 +      if (aaconClient != null && aaconClient.size() > 0)
 +      {
 +        SeqAnnotationServiceCalcWorker worker = (SeqAnnotationServiceCalcWorker) aaconClient
 +                .get(0);
 +        if (!worker.service.getHostURL().equals(service.getHostURL()))
 +        {
 +          // javax.swing.SwingUtilities.invokeLater(new Runnable()
 +          {
 +            // @Override
 +            // public void run()
 +            {
 +              removeCurrentAAConWorkerFor(aaui, alignFrame);
 +              buildCurrentAAConWorkerFor(aaui, alignFrame, service);
 +            }
 +          } // );
 +        }
 +      }
 +    }
 +
 +    // is there a service already registered ? there shouldn't be if we are
 +    // being called correctly
 +    if (_aaConEnabled == null)
 +    {
 +      final JCheckBoxMenuItem aaConEnabled = new JCheckBoxMenuItem(
 +              aaui.getAAconToggle());
 +
 +      aaConEnabled.setToolTipText(
 +              JvSwingUtils.wrapTooltip(true, aaui.getAAconToggleTooltip()));
 +      aaConEnabled.addActionListener(new ActionListener()
 +      {
 +        @Override
 +        public void actionPerformed(ActionEvent arg0)
 +        {
 +
 +          List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 +                  .getCalcManager()
 +                  .getWorkersOfClass(SeqAnnotationServiceCalcWorker.class);
 +          if (aaconClient != null)
 +          {
 +            for (AlignCalcWorkerI worker : aaconClient)
 +            {
 +              if (((SeqAnnotationServiceCalcWorker) worker).getService()
 +                      .getClass().equals(aaui.getClient()))
 +              {
 +                removeCurrentAAConWorkerFor(aaui, alignFrame);
 +                return;
 +              }
 +            }
 +          }
 +          buildCurrentAAConWorkerFor(aaui, alignFrame);
 +        }
 +
 +      });
 +      wsmenu.add(aaConEnabled);
 +      final JMenuItem modifyParams = new JMenuItem(
 +              aaui.getAAeditSettings());
 +      modifyParams.setToolTipText(JvSwingUtils.wrapTooltip(true,
 +              aaui.getAAeditSettingsTooltip()));
 +      modifyParams.addActionListener(new ActionListener()
 +      {
 +
 +        @Override
 +        public void actionPerformed(ActionEvent arg0)
 +        {
 +          showAAConAnnotationSettingsFor(aaui, alignFrame);
 +        }
 +      });
 +      wsmenu.add(modifyParams);
 +      wsmenu.addMenuListener(new MenuListener()
 +      {
 +
 +        @Override
 +        public void menuSelected(MenuEvent arg0)
 +        {
 +          // TODO: refactor to the implementing class.
 +          if (alignFrame.getViewport().getAlignment().isNucleotide()
 +                  ? aaui.isNa()
 +                  : aaui.isPr())
 +          {
 +            aaConEnabled.setEnabled(true);
 +            modifyParams.setEnabled(true);
 +          }
 +          else
 +          {
 +            aaConEnabled.setEnabled(false);
 +            modifyParams.setEnabled(false);
 +            return;
 +          }
 +          List<AlignCalcWorkerI> aaconClient = alignFrame.getViewport()
 +                  .getCalcManager()
 +                  .getWorkersOfClass(SeqAnnotationServiceCalcWorker.class);
 +
 +          boolean serviceEnabled = false;
 +          if (aaconClient != null)
 +          {
 +            // NB code duplicatino again!
 +            for (AlignCalcWorkerI _worker : aaconClient)
 +            {
 +              SeqAnnotationServiceCalcWorker worker = (SeqAnnotationServiceCalcWorker) _worker;
 +              // this could be cleaner ?
 +              if (worker.hasService()
 +                      && aaui.getClient()
 +                              .equals(worker.getService().getClass()))
 +              {
 +                serviceEnabled = true;
 +              }
 +            }
 +          }
 +          aaConEnabled.setSelected(serviceEnabled);
 +        }
 +
 +        @Override
 +        public void menuDeselected(MenuEvent arg0)
 +        {
 +          // TODO Auto-generated method stub
 +
 +        }
 +
 +        @Override
 +        public void menuCanceled(MenuEvent arg0)
 +        {
 +          // TODO Auto-generated method stub
 +
 +        }
 +      });
 +
 +    }
 +    return true;
 +  }
 +
 +  private static void showAAConAnnotationSettingsFor(
 +          final AlignAnalysisUIText aaui, AlignFrame alignFrame)
 +  {
 +    /*
 +     * preferred settings Whether AACon is automatically recalculated Which
 +     * AACon server to use What parameters to use
 +     */
 +    // could actually do a class search for this too
 +    AutoCalcSetting fave = alignFrame.getViewport()
 +            .getCalcIdSettingsFor(aaui.getCalcId());
 +    if (fave == null)
 +    {
 +      fave = createDefaultAAConSettings(aaui);
 +    }
 +    new SequenceAnnotationWSClient(fave, alignFrame, true);
 +
 +  }
 +
 +  private static void buildCurrentAAConWorkerFor(
 +          final AlignAnalysisUIText aaui, AlignFrame alignFrame)
 +  {
 +    buildCurrentAAConWorkerFor(aaui, alignFrame, null);
 +  }
 +
 +  private static void buildCurrentAAConWorkerFor(
 +          final AlignAnalysisUIText aaui, AlignFrame alignFrame,
 +          ServiceWithParameters service)
 +  {
 +    /*
 +     * preferred settings Whether AACon is automatically recalculated Which
 +     * AACon server to use What parameters to use
 +     */
 +    AutoCalcSetting fave = alignFrame.getViewport()
 +            .getCalcIdSettingsFor(aaui.getCalcId());
 +    if (fave == null)
 +    {
 +      fave = createDefaultAAConSettings(aaui, service);
 +    }
 +    else
 +    {
 +      if (service != null && !fave.getService().getHostURL()
 +              .equals(service.getHostURL()))
 +      {
-         Cache.log.debug("Changing AACon service to " + service.getHostURL()
++        Console.debug("Changing AACon service to " + service.getHostURL()
 +                + " from " + fave.getService().getHostURL());
 +        fave.setService(service);
 +      }
 +    }
 +    new SequenceAnnotationWSClient(fave, alignFrame, false);
 +  }
 +
 +  private static AutoCalcSetting createDefaultAAConSettings(
 +          AlignAnalysisUIText aaui)
 +  {
 +    return createDefaultAAConSettings(aaui, null);
 +  }
 +
 +  private static AutoCalcSetting createDefaultAAConSettings(
 +          AlignAnalysisUIText aaui, ServiceWithParameters service)
 +  {
 +    if (service != null)
 +    {
 +      // if (!service.getServiceType()
 +      // .equals(compbio.ws.client.Services.AAConWS.toString()))
 +      // {
-       // Cache.log.warn(
++      // Console.warn(
 +      // "Ignoring invalid preferred service for AACon calculations (service
 +      // type was "
 +      // + service.getServiceType() + ")");
 +      // service = null;
 +      // }
 +      // else
 +      {
 +        // check service is actually in the list of currently avaialable
 +        // services
 +        if (!PreferredServiceRegistry.getRegistry().contains(service))
 +        {
 +          // it isn't ..
 +          service = null;
 +        }
 +      }
 +    }
 +    if (service == null)
 +    {
 +      // get the default service for AACon
 +      service = PreferredServiceRegistry.getRegistry().getPreferredServiceFor(null,
 +              aaui.getServiceType());
 +    }
 +    if (service == null)
 +    {
 +      // TODO raise dialog box explaining error, and/or open the JABA
 +      // preferences menu.
 +      throw new Error(
 +              MessageManager.getString("error.no_aacon_service_found"));
 +    }
 +    return service instanceof Jws2Instance
 +            ? new AAConSettings(true, service, null, null)
 +            : new AutoCalcSetting(service, null, null, true);
 +  }
 +
 +  private static void removeCurrentAAConWorkerFor(AlignAnalysisUIText aaui,
 +          AlignFrame alignFrame)
 +  {
 +    alignFrame.getViewport().getCalcManager()
 +            .removeWorkersOfClass(aaui.getClient());
 +  }
- }
++}
  package jalview.ws.jws2;
  
  import jalview.bin.Cache;
+ import jalview.bin.Console;
 +import jalview.bin.ApplicationSingletonProvider;
 +import jalview.bin.ApplicationSingletonProvider.ApplicationSingletonI;
  import jalview.gui.AlignFrame;
 -import jalview.gui.Desktop;
 -import jalview.gui.JvSwingUtils;
  import jalview.util.MessageManager;
 -import jalview.ws.WSMenuEntryProviderI;
 +import jalview.ws.ServiceChangeListener;
 +import jalview.ws.WSDiscovererI;
 +import jalview.ws.api.ServiceWithParameters;
  import jalview.ws.jws2.jabaws2.Jws2Instance;
  import jalview.ws.params.ParamDatastoreI;
  
@@@ -60,20 -61,8 +61,18 @@@ import compbio.ws.client.Services
   * @author JimP
   * 
   */
 -public class Jws2Discoverer implements Runnable, WSMenuEntryProviderI
 +public class Jws2Discoverer implements WSDiscovererI, Runnable, ApplicationSingletonI
  {
 +  /**
 +   * Returns the singleton instance of this class.
 +   * 
 +   * @return
 +   */
 +  public static Jws2Discoverer getInstance()
 +  {
 +    return (Jws2Discoverer) ApplicationSingletonProvider
 +            .getInstance(Jws2Discoverer.class);
 +  }
    public static final String COMPBIO_JABAWS = "http://www.compbio.dundee.ac.uk/jabaws";
  
    /*
  
    // preferred url has precedence over others
    private String preferredUrl;
 -  private PropertyChangeSupport changeSupport = new PropertyChangeSupport(
 -          this);
 +  
 +  private Set<ServiceChangeListener> serviceListeners = new CopyOnWriteArraySet<>();
    private Vector<String> invalidServiceUrls = null;
  
    private Vector<String> urlsWithoutServices = null;
    private volatile boolean running = false;
  
    private volatile boolean aborted = false;
 -  private Thread oldthread = null;
 +  
 +  private volatile Thread oldthread = null;
  
    /**
     * holds list of services.
    {
    }
  
 -  /**
 -   * change listeners are notified of "services" property changes
 -   * 
 -   * @param listener
 -   *          to be added that consumes new services Hashtable object.
 -   */
 -  public void addPropertyChangeListener(
 -          java.beans.PropertyChangeListener listener)
 +  @Override
 +  public void addServiceChangeListener(ServiceChangeListener listener)
    {
 -    changeSupport.addPropertyChangeListener(listener);
 +    serviceListeners.add(listener);
    }
  
 -  /**
 -   * 
 -   * 
 -   * @param listener
 -   *          to be removed
 -   */
 -  public void removePropertyChangeListener(
 -          java.beans.PropertyChangeListener listener)
 +  @Override
 +  public void removeServiceChangeListener(ServiceChangeListener listener)
 +  {
 +    serviceListeners.remove(listener);
 +  }
 +
 +  private void notifyServiceListeners(List<? extends ServiceWithParameters> services) 
    {
 -    changeSupport.removePropertyChangeListener(listener);
 +    if (services == null) services = this.services;
 +    for (var listener : serviceListeners) {
 +      listener.servicesChanged(this, services);
 +    }
    }
  
    /**
@@@ -231,27 -215,16 +231,28 @@@ public class MsaWSClient extends Jws2Cl
  
    @Override
    public void attachWSMenuEntry(JMenu rmsawsmenu,
-           final Jws2Instance service, final AlignFrame alignFrame)
 -          final Jws2Instance service, final AlignFrame af)
++          final ServiceWithParameters service, final AlignFrame alignFrame)
    {
-     if (registerAAConWSInstance(rmsawsmenu, service, alignFrame))
 -    if (registerAAConWSInstance(rmsawsmenu, service, af))
++    if (Jws2ClientFactory.registerAAConWSInstance(rmsawsmenu,
++                    service, alignFrame))
      {
--      // Alignment dependent analysis calculation WS gui
++          // Alignment dependent analysis calculation WS gui
        return;
      }
 +    serviceHandle = service;
      setWebService(service, true); // headless
 +    attachWSMenuEntry(rmsawsmenu, alignFrame);
 +  }
 +
 +  @Override
 +  public void attachWSMenuEntry(JMenu wsmenu, AlignFrame alignFrame)
 +  {
      boolean finished = true, submitGaps = false;
 -    JMenu msawsmenu = rmsawsmenu;
 +    /**
 +     * temp variables holding msa service submenu or root service menu
 +     */
 +    JMenu msawsmenu = wsmenu;
 +    JMenu rmsawsmenu = wsmenu;
      String svcname = WebServiceName;
      if (svcname.endsWith("WS"))
      {
index aafbbab,0000000..3253f43
mode 100644,000000..100644
--- /dev/null
@@@ -1,842 -1,0 +1,843 @@@
 +/*
 + * 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.ws.jws2;
 +
 +import jalview.analysis.AlignSeq;
 +import jalview.analysis.AlignmentAnnotationUtils;
 +import jalview.analysis.SeqsetUtils;
 +import jalview.api.AlignViewportI;
 +import jalview.api.AlignmentViewPanel;
 +import jalview.api.FeatureColourI;
 +import jalview.api.PollableAlignCalcWorkerI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.AnnotatedCollectionI;
 +import jalview.datamodel.Annotation;
 +import jalview.datamodel.ContiguousI;
 +import jalview.datamodel.Mapping;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherSetI;
 +import jalview.gui.AlignFrame;
 +import jalview.gui.Desktop;
 +import jalview.gui.IProgressIndicator;
 +import jalview.gui.IProgressIndicatorHandler;
 +import jalview.gui.JvOptionPane;
 +import jalview.gui.WebserviceInfo;
 +import jalview.schemes.FeatureSettingsAdapter;
 +import jalview.schemes.ResidueProperties;
 +import jalview.util.MapList;
 +import jalview.util.MessageManager;
 +import jalview.workers.AlignCalcWorker;
 +import jalview.ws.JobStateSummary;
 +import jalview.ws.api.CancellableI;
 +import jalview.ws.api.JalviewServiceEndpointProviderI;
 +import jalview.ws.api.JobId;
 +import jalview.ws.api.SequenceAnnotationServiceI;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.api.WSAnnotationCalcManagerI;
 +import jalview.ws.gui.AnnotationWsJob;
 +import jalview.ws.jws2.dm.AAConSettings;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.WsParamSetI;
 +
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +public class SeqAnnotationServiceCalcWorker extends AlignCalcWorker
 +        implements WSAnnotationCalcManagerI, PollableAlignCalcWorkerI
 +{
 +
 +  protected ServiceWithParameters service;
 +
 +  protected WsParamSetI preset;
 +
 +  protected List<ArgumentI> arguments;
 +
 +  protected IProgressIndicator guiProgress;
 +
 +  protected boolean submitGaps = true;
 +
 +  /**
 +   * by default, we filter out non-standard residues before submission
 +   */
 +  protected boolean filterNonStandardResidues = true;
 +
 +  /**
 +   * Recover any existing parameters for this service
 +   */
 +  protected void initViewportParams()
 +  {
 +    if (getCalcId() != null)
 +    {
 +      ((jalview.gui.AlignViewport) alignViewport).setCalcIdSettingsFor(
 +              getCalcId(),
 +              new AAConSettings(true, service, this.preset, arguments),
 +              true);
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @return null or a string used to recover all annotation generated by this
 +   *         worker
 +   */
 +  public String getCalcId()
 +  {
 +    return service.getAlignAnalysisUI() == null ? null
 +            : service.getAlignAnalysisUI().getCalcId();
 +  }
 +
 +  public WsParamSetI getPreset()
 +  {
 +    return preset;
 +  }
 +
 +  public List<ArgumentI> getArguments()
 +  {
 +    return arguments;
 +  }
 +
 +  /**
 +   * reconfigure and restart the AAConClient. This method will spawn a new
 +   * thread that will wait until any current jobs are finished, modify the
 +   * parameters and restart the conservation calculation with the new values.
 +   * 
 +   * @param newpreset
 +   * @param newarguments
 +   */
 +  public void updateParameters(final WsParamSetI newpreset,
 +          final List<ArgumentI> newarguments)
 +  {
 +    preset = newpreset;
 +    arguments = newarguments;
 +    calcMan.startWorker(this);
 +    initViewportParams();
 +  }
 +  protected boolean alignedSeqs = true;
 +
 +  protected boolean nucleotidesAllowed = false;
 +
 +  protected boolean proteinAllowed = false;
 +
 +  /**
 +   * record sequences for mapping result back to afterwards
 +   */
 +  protected boolean bySequence = false;
 +
 +  protected Map<String, SequenceI> seqNames;
 +
 +  // TODO: convert to bitset
 +  protected boolean[] gapMap;
 +
 +  int realw;
 +
 +  protected int start;
 +
 +  int end;
 +
 +  private AlignFrame alignFrame;
 +
 +  public boolean[] getGapMap()
 +  {
 +    return gapMap;
 +  }
 +
 +  public SeqAnnotationServiceCalcWorker(ServiceWithParameters service,
 +          AlignFrame alignFrame,
 +          WsParamSetI preset, List<ArgumentI> paramset)
 +  {
 +    super(alignFrame.getCurrentView(), alignFrame.alignPanel);
 +    // TODO: both these fields needed ?
 +    this.alignFrame = alignFrame;
 +    this.guiProgress = alignFrame;
 +    this.preset = preset;
 +    this.arguments = paramset;
 +    this.service = service;
 +    try
 +    {
 +      annotService = (jalview.ws.api.SequenceAnnotationServiceI) ((JalviewServiceEndpointProviderI) service)
 +              .getEndpoint();
 +    } catch (ClassCastException cce)
 +    {
 +      annotService = null;
 +      JvOptionPane.showMessageDialog(Desktop.getInstance(),
 +              MessageManager.formatMessage(
 +                      "label.service_called_is_not_an_annotation_service",
 +                      new String[]
 +                      { service.getName() }),
 +              MessageManager.getString("label.internal_jalview_error"),
 +              JvOptionPane.WARNING_MESSAGE);
 +
 +    }
 +    cancellable = CancellableI.class.isInstance(annotService);
 +    // configure submission flags
 +    proteinAllowed = service.isProteinService();
 +    nucleotidesAllowed = service.isNucleotideService();
 +    alignedSeqs = service.isNeedsAlignedSequences();
 +    bySequence = !service.isAlignmentAnalysis();
 +    filterNonStandardResidues = service.isFilterSymbols();
 +    min_valid_seqs = service.getMinimumInputSequences();
 +    submitGaps = service.isAlignmentAnalysis();
 +
 +    if (service.isInteractiveUpdate())
 +    {
 +      initViewportParams();
 +    }
 +  }
 +
 +  /**
 +   * 
 +   * @return true if the submission thread should attempt to submit data
 +   */
 +  public boolean hasService()
 +  {
 +    return annotService != null;
 +  }
 +
 +  protected SequenceAnnotationServiceI annotService;
 +  protected final boolean cancellable;
 +
 +  volatile JobId rslt = null;
 +
 +  AnnotationWsJob running = null;
 +
 +  private int min_valid_seqs;
 +
 +
 +  private long progressId = -1;
 +  JobStateSummary job = null;
 +  WebserviceInfo info = null;
 +  List<SequenceI> seqs = null;
 +  
 +  @Override public void startUp() throws Throwable
 +  {
 +    if (alignViewport.isClosed())
 +    {
 +      abortAndDestroy();
 +      return;
 +    }
 +    if (!hasService())
 +    {
 +      return;
 +    }
 +
 +    StringBuffer msg = new StringBuffer();
 +    job = new JobStateSummary();
 +    info = new WebserviceInfo("foo", "bar", false);
 +
 +    seqs = getInputSequences(
 +            alignViewport.getAlignment(),
 +            bySequence ? alignViewport.getSelectionGroup() : null);
 +
 +    if (seqs == null || !checkValidInputSeqs(seqs))
 +    {
-       jalview.bin.Cache.log.debug(
++      jalview.bin.Console.debug(
 +              "Sequences for analysis service were null or not valid");
 +      return;
 +    }
 +
 +    if (guiProgress != null)
 +    {
 +      guiProgress.setProgressBar(service.getActionText(),
 +              progressId = System.currentTimeMillis());
 +    }
-     jalview.bin.Cache.log.debug("submitted " + seqs.size()
++    jalview.bin.Console.debug("submitted " + seqs.size()
 +            + " sequences to " + service.getActionText());
 +
 +    rslt = annotService.submitToService(seqs, getPreset(),
 +            getArguments());
 +    if (rslt == null)
 +    {
 +      return;
 +    }
 +    // TODO: handle job submission error reporting here.
-     Cache.log.debug("Service " + service.getUri() + "\nSubmitted job ID: "
++    Console.debug("Service " + service.getUri() + "\nSubmitted job ID: "
 +            + rslt);
 +    // ///
 +    // otherwise, construct WsJob and any UI handlers
 +    running = new AnnotationWsJob();
 +    running.setJobHandle(rslt);
 +    running.setSeqNames(seqNames);
 +    running.setStartPos(start);
 +    running.setSeqs(seqs);
 +    job.updateJobPanelState(info, "", running);
 +    if (guiProgress != null)
 +    {
 +      guiProgress.registerHandler(progressId,
 +              new IProgressIndicatorHandler()
 +              {
 +
 +                @Override
 +                public boolean cancelActivity(long id)
 +                {
 +                  calcMan.cancelWorker(SeqAnnotationServiceCalcWorker.this);
 +                  return true;
 +                }
 +
 +                @Override
 +                public boolean canCancel()
 +                {
 +                  return cancellable;
 +                }
 +              });
 +    }
 +  }
 +  
 +  @Override public boolean poll() throws Throwable
 +  {
 +    boolean finished = false;
 +    
-     Cache.log.debug("Updating status for annotation service.");
++    Console.debug("Updating status for annotation service.");
 +    annotService.updateStatus(running);
 +    job.updateJobPanelState(info, "", running);
 +    if (running.isSubjobComplete())
 +    {
-       Cache.log.debug(
++      Console.debug(
 +              "Finished polling analysis service job: status reported is "
 +                      + running.getState());
 +      finished = true;
 +    }
 +    else
 +    {
-       Cache.log.debug("Status now " + running.getState());
++      Console.debug("Status now " + running.getState());
 +    }
 +
 +    // pull any stats - some services need to flush log output before
 +    // results are available
-     Cache.log.debug("Updating progress log for annotation service.");
++    Console.debug("Updating progress log for annotation service.");
 +
 +    try
 +    {
 +      annotService.updateJobProgress(running);
 +    } catch (Throwable thr)
 +    {
-       Cache.log.debug("Ignoring exception during progress update.",
++      Console.debug("Ignoring exception during progress update.",
 +              thr);
 +    }
-     Cache.log.debug("Result of poll: " + running.getStatus());
++    Console.debug("Result of poll: " + running.getStatus());
 +    
 +    
 +    if (finished)
 +    {
-       Cache.log.debug("Job poll loop exited. Job is " + running.getState());
++      Console.debug("Job poll loop exited. Job is " + running.getState());
 +      if (running.isFinished())
 +      {
 +        // expect there to be results to collect
 +        // configure job with the associated view's feature renderer, if one
 +        // exists.
 +        // TODO: here one would also grab the 'master feature renderer' in order
 +        // to enable/disable
 +        // features automatically according to user preferences
 +        running.setFeatureRenderer(
 +                ((jalview.gui.AlignmentPanel) ap).cloneFeatureRenderer());
-         Cache.log.debug("retrieving job results.");
++        Console.debug("retrieving job results.");
 +        final Map<String, FeatureColourI> featureColours = new HashMap<>();
 +        final Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
 +        List<AlignmentAnnotation> returnedAnnot = annotService
 +                .getAnnotationResult(running.getJobHandle(), seqs,
 +                        featureColours, featureFilters);
 +
-         Cache.log.debug("Obtained " + (returnedAnnot == null ? "no rows"
++        Console.debug("Obtained " + (returnedAnnot == null ? "no rows"
 +                : ("" + returnedAnnot.size())));
-         Cache.log.debug("There were " + featureColours.size()
++        Console.debug("There were " + featureColours.size()
 +                + " feature colours and " + featureFilters.size()
 +                + " filters defined.");
 +
 +        // TODO
 +        // copy over each annotation row reurned and also defined on each
 +        // sequence, excluding regions not annotated due to gapMap/column
 +        // visibility
 +
 +        // update calcId if it is not already set on returned annotation
 +        if (returnedAnnot != null)
 +        {
 +          for (AlignmentAnnotation aa : returnedAnnot)
 +          {
 +            // assume that any CalcIds already set
 +            if (getCalcId() != null && aa.getCalcId() == null
 +                    || "".equals(aa.getCalcId()))
 +            {
 +              aa.setCalcId(getCalcId());
 +            }
 +            // autocalculated annotation are created by interactive alignment
 +            // analysis services
 +            aa.autoCalculated = service.isAlignmentAnalysis()
 +                    && service.isInteractiveUpdate();
 +          }
 +        }
 +
 +        running.setAnnotation(returnedAnnot);
 +
 +        if (running.hasResults())
 +        {
-           jalview.bin.Cache.log.debug("Updating result annotation from Job "
++          jalview.bin.Console.debug("Updating result annotation from Job "
 +                  + rslt + " at " + service.getUri());
 +          updateResultAnnotation(true);
 +          if (running.isTransferSequenceFeatures())
 +          {
 +            // TODO
 +            // look at each sequence and lift over any features, excluding
 +            // regions
 +            // not annotated due to gapMap/column visibility
 +
-             jalview.bin.Cache.log.debug(
++            jalview.bin.Console.debug(
 +                    "Updating feature display settings and transferring features from Job "
 +                            + rslt + " at " + service.getUri());
 +            // TODO: consider merge rather than apply here
 +            alignViewport.applyFeaturesStyle(new FeatureSettingsAdapter()
 +            {
 +              @Override
 +              public FeatureColourI getFeatureColour(String type)
 +              {
 +                return featureColours.get(type);
 +              }
 +
 +              @Override
 +              public FeatureMatcherSetI getFeatureFilters(String type)
 +              {
 +                return featureFilters.get(type);
 +              }
 +
 +              @Override
 +              public boolean isFeatureDisplayed(String type)
 +              {
 +                return featureColours.containsKey(type);
 +              }
 +
 +            });
 +            // TODO: JAL-1150 - create sequence feature settings API for
 +            // defining
 +            // styles and enabling/disabling feature overlay on alignment panel
 +
 +            if (alignFrame.alignPanel == ap)
 +            {
 +              alignViewport.setShowSequenceFeatures(true);
 +              alignFrame.setMenusForViewport();
 +            }
 +          }
 +          ap.adjustAnnotationHeight();
 +        }
 +      }
-       Cache.log.debug("Annotation Service Worker thread finished.");
++      Console.debug("Annotation Service Worker thread finished.");
 +
 +    }
 +    
 +    return finished;
 +  }
 +  
 +  @Override public void cancel()
 +  {
 +    cancelCurrentJob();
 +  }
 +  
 +  @Override public void done()
 +  {
 +    if (ap != null)
 +    {
 +      if (guiProgress != null && progressId != -1)
 +      {
 +        guiProgress.removeProgressBar(progressId);
 +      }
 +      // TODO: may not need to paintAlignment again !
 +      ap.paintAlignment(false, false);
 +    }
 +  }
 +
 +  /**
 +   * validate input for dynamic/non-dynamic update context TODO: move to
 +   * analysis interface ?
 +   * @param seqs
 +   * 
 +   * @return true if input is valid
 +   */
 +  boolean checkValidInputSeqs(List<SequenceI> seqs)
 +  {
 +    int nvalid = 0;
 +    for (SequenceI sq : seqs)
 +    {
 +      if (sq.getStart() <= sq.getEnd()
 +              && (sq.isProtein() ? proteinAllowed : nucleotidesAllowed))
 +      {
 +        if (submitGaps
 +                || sq.getLength() == (sq.getEnd() - sq.getStart() + 1))
 +        {
 +          nvalid++;
 +        }
 +      }
 +    }
 +    return nvalid >= min_valid_seqs;
 +  }
 +
 +  public void cancelCurrentJob()
 +  {
 +    try
 +    {
 +      String id = running.getJobId();
 +      if (cancellable && ((CancellableI) annotService).cancel(running))
 +      {
 +        System.err.println("Cancelled job " + id);
 +      }
 +      else
 +      {
 +        System.err.println("Job " + id + " couldn't be cancelled.");
 +      }
 +    } catch (Exception q)
 +    {
 +      q.printStackTrace();
 +    }
 +  }
 +
 +  /**
 +   * Interactive updating. Analysis calculations that work on the currently
 +   * displayed alignment data should cancel existing jobs when the input data
 +   * has changed.
 +   * 
 +   * @return true if a running job should be cancelled because new input data is
 +   *         available for analysis
 +   */
 +  boolean isInteractiveUpdate()
 +  {
 +    return service.isInteractiveUpdate();
 +  }
 +
 +  /**
 +   * decide what sequences will be analysed TODO: refactor to generate
 +   * List<SequenceI> for submission to service interface
 +   * 
 +   * @param alignment
 +   * @param inputSeqs
 +   * @return
 +   */
 +  public List<SequenceI> getInputSequences(AlignmentI alignment,
 +          AnnotatedCollectionI inputSeqs)
 +  {
 +    if (alignment == null || alignment.getWidth() <= 0
 +            || alignment.getSequences() == null || alignment.isNucleotide()
 +                    ? !nucleotidesAllowed
 +                    : !proteinAllowed)
 +    {
 +      return null;
 +    }
 +    if (inputSeqs == null || inputSeqs.getWidth() <= 0
 +            || inputSeqs.getSequences() == null
 +            || inputSeqs.getSequences().size() < 1)
 +    {
 +      inputSeqs = alignment;
 +    }
 +
 +    List<SequenceI> seqs = new ArrayList<>();
 +
 +    int minlen = 10;
 +    int ln = -1;
 +    if (bySequence)
 +    {
 +      seqNames = new HashMap<>();
 +    }
 +    gapMap = new boolean[0];
 +    start = inputSeqs.getStartRes();
 +    end = inputSeqs.getEndRes();
 +    // TODO: URGENT! unify with JPred / MSA code to handle hidden regions
 +    // correctly
 +    // TODO: push attributes into WsJob instance (so they can be safely
 +    // persisted/restored
 +    for (SequenceI sq : (inputSeqs.getSequences()))
 +    {
 +      if (bySequence
 +              ? sq.findPosition(end + 1)
 +                      - sq.findPosition(start + 1) > minlen - 1
 +              : sq.getEnd() - sq.getStart() > minlen - 1)
 +      {
 +        String newname = SeqsetUtils.unique_name(seqs.size() + 1);
 +        // make new input sequence with or without gaps
 +        if (seqNames != null)
 +        {
 +          seqNames.put(newname, sq);
 +        }
 +        SequenceI seq;
 +        if (submitGaps)
 +        {
 +          seqs.add(seq = new jalview.datamodel.Sequence(newname,
 +                  sq.getSequenceAsString()));
 +          if (gapMap == null || gapMap.length < seq.getLength())
 +          {
 +            boolean[] tg = gapMap;
 +            gapMap = new boolean[seq.getLength()];
 +            System.arraycopy(tg, 0, gapMap, 0, tg.length);
 +            for (int p = tg.length; p < gapMap.length; p++)
 +            {
 +              gapMap[p] = false; // init as a gap
 +            }
 +          }
 +          for (int apos : sq.gapMap())
 +          {
 +            char sqc = sq.getCharAt(apos);
 +            if (!filterNonStandardResidues
 +                    || (sq.isProtein() ? ResidueProperties.aaIndex[sqc] < 20
 +                            : ResidueProperties.nucleotideIndex[sqc] < 5))
 +            {
 +              gapMap[apos] = true; // aligned and real amino acid residue
 +            }
 +            ;
 +          }
 +        }
 +        else
 +        {
 +          // TODO: add ability to exclude hidden regions
 +          seqs.add(seq = new jalview.datamodel.Sequence(newname,
 +                  AlignSeq.extractGaps(jalview.util.Comparison.GapChars,
 +                          sq.getSequenceAsString(start, end + 1))));
 +          // for annotation need to also record map to sequence start/end
 +          // position in range
 +          // then transfer back to original sequence on return.
 +        }
 +        if (seq.getLength() > ln)
 +        {
 +          ln = seq.getLength();
 +        }
 +      }
 +    }
 +    if (alignedSeqs && submitGaps)
 +    {
 +      realw = 0;
 +      for (int i = 0; i < gapMap.length; i++)
 +      {
 +        if (gapMap[i])
 +        {
 +          realw++;
 +        }
 +      }
 +      // try real hard to return something submittable
 +      // TODO: some of AAcon measures need a minimum of two or three amino
 +      // acids at each position, and AAcon doesn't gracefully degrade.
 +      for (int p = 0; p < seqs.size(); p++)
 +      {
 +        SequenceI sq = seqs.get(p);
 +        // strip gapped columns
 +        char[] padded = new char[realw],
 +                orig = sq.getSequence();
 +        for (int i = 0, pp = 0; i < realw; pp++)
 +        {
 +          if (gapMap[pp])
 +          {
 +            if (orig.length > pp)
 +            {
 +              padded[i++] = orig[pp];
 +            }
 +            else
 +            {
 +              padded[i++] = '-';
 +            }
 +          }
 +        }
 +        seqs.set(p, new jalview.datamodel.Sequence(sq.getName(),
 +                new String(padded)));
 +      }
 +    }
 +    return seqs;
 +  }
 +
 +  @Override
 +  public void updateAnnotation()
 +  {
 +    updateResultAnnotation(false);
 +  }
 +
 +  public void updateResultAnnotation(boolean immediate)
 +  {
 +    if ((immediate || !calcMan.isWorking(this)) && running != null
 +            && running.hasResults())
 +    {
 +      List<AlignmentAnnotation> ourAnnot = running.getAnnotation(),
 +              newAnnots = new ArrayList<>();
 +      //
 +      // update graphGroup for all annotation
 +      //
 +      /**
 +       * find a graphGroup greater than any existing ones this could be a method
 +       * provided by alignment Alignment.getNewGraphGroup() - returns next
 +       * unused graph group
 +       */
 +      int graphGroup = 1;
 +      if (alignViewport.getAlignment().getAlignmentAnnotation() != null)
 +      {
 +        for (AlignmentAnnotation ala : alignViewport.getAlignment()
 +                .getAlignmentAnnotation())
 +        {
 +          if (ala.graphGroup > graphGroup)
 +          {
 +            graphGroup = ala.graphGroup;
 +          }
 +        }
 +      }
 +      /**
 +       * update graphGroup in the annotation rows returned from service
 +       */
 +      // TODO: look at sequence annotation rows and update graph groups in the
 +      // case of reference annotation.
 +      for (AlignmentAnnotation ala : ourAnnot)
 +      {
 +        if (ala.graphGroup > 0)
 +        {
 +          ala.graphGroup += graphGroup;
 +        }
 +        SequenceI aseq = null;
 +
 +        /**
 +         * transfer sequence refs and adjust gapmap
 +         */
 +        if (ala.sequenceRef != null)
 +        {
 +          SequenceI seq = running.getSeqNames()
 +                  .get(ala.sequenceRef.getName());
 +          aseq = seq;
 +          while (seq.getDatasetSequence() != null)
 +          {
 +            seq = seq.getDatasetSequence();
 +          }
 +        }
 +        Annotation[] resAnnot = ala.annotations,
 +                gappedAnnot = new Annotation[Math.max(
 +                        alignViewport.getAlignment().getWidth(),
 +                        gapMap.length)];
 +        for (int p = 0, ap = start; ap < gappedAnnot.length; ap++)
 +        {
 +          if (gapMap != null && gapMap.length > ap && !gapMap[ap])
 +          {
 +            gappedAnnot[ap] = new Annotation("", "", ' ', Float.NaN);
 +          }
 +          else if (p < resAnnot.length)
 +          {
 +            gappedAnnot[ap] = resAnnot[p++];
 +          }
 +        }
 +        ala.sequenceRef = aseq;
 +        ala.annotations = gappedAnnot;
 +        AlignmentAnnotation newAnnot = getAlignViewport().getAlignment()
 +                .updateFromOrCopyAnnotation(ala);
 +        if (aseq != null)
 +        {
 +
 +          aseq.addAlignmentAnnotation(newAnnot);
 +          newAnnot.adjustForAlignment();
 +
 +          AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(
 +                  newAnnot, newAnnot.label, newAnnot.getCalcId());
 +        }
 +        newAnnots.add(newAnnot);
 +
 +      }
 +      for (SequenceI sq : running.getSeqs())
 +      {
 +        if (!sq.getFeatures().hasFeatures()
 +                && (sq.getDBRefs() == null || sq.getDBRefs().size() == 0))
 +        {
 +          continue;
 +        }
 +        running.setTransferSequenceFeatures(true);
 +        SequenceI seq = running.getSeqNames().get(sq.getName());
 +        SequenceI dseq;
 +        ContiguousI seqRange = seq.findPositions(start, end);
 +
 +        while ((dseq = seq).getDatasetSequence() != null)
 +        {
 +          seq = seq.getDatasetSequence();
 +        }
 +        List<ContiguousI> sourceRange = new ArrayList();
 +        if (gapMap != null && gapMap.length >= end)
 +        {
 +          int lastcol = start, col = start;
 +          do
 +          {
 +            if (col == end || !gapMap[col])
 +            {
 +              if (lastcol <= (col - 1))
 +              {
 +                seqRange = seq.findPositions(lastcol, col);
 +                sourceRange.add(seqRange);
 +              }
 +              lastcol = col + 1;
 +            }
 +          } while (++col <= end);
 +        }
 +        else
 +        {
 +          sourceRange.add(seq.findPositions(start, end));
 +        }
 +        int i = 0;
 +        int source_startend[] = new int[sourceRange.size() * 2];
 +
 +        for (ContiguousI range : sourceRange)
 +        {
 +          source_startend[i++] = range.getBegin();
 +          source_startend[i++] = range.getEnd();
 +        }
 +        Mapping mp = new Mapping(
 +                new MapList(source_startend, new int[]
 +                { seq.getStart(), seq.getEnd() }, 1, 1));
 +        dseq.transferAnnotation(sq, mp);
 +
 +      }
 +      updateOurAnnots(newAnnots);
 +    }
 +  }
 +
 +  protected void updateOurAnnots(List<AlignmentAnnotation> ourAnnot)
 +  {
 +    List<AlignmentAnnotation> our = ourAnnots;
 +    ourAnnots = ourAnnot;
 +    AlignmentI alignment = alignViewport.getAlignment();
 +    if (our != null)
 +    {
 +      if (our.size() > 0)
 +      {
 +        for (AlignmentAnnotation an : our)
 +        {
 +          if (!ourAnnots.contains(an))
 +          {
 +            // remove the old annotation
 +            alignment.deleteAnnotation(an);
 +          }
 +        }
 +      }
 +      our.clear();
 +    }
 +
 +    // validate rows and update Alignmment state
 +    for (AlignmentAnnotation an : ourAnnots)
 +    {
 +      alignViewport.getAlignment().validateAnnotation(an);
 +    }
 +    // TODO: may need a menu refresh after this
 +    // af.setMenusForViewport();
 +    ap.adjustAnnotationHeight();
 +
 +  }
 +
 +  public SequenceAnnotationServiceI getService()
 +  {
 +    return annotService;
 +  }
 +
 +}
@@@ -132,26 -117,20 +132,35 @@@ public class SequenceAnnotationWSClien
          if (editParams)
          {
            paramset = worker.getArguments();
 -          preset = worker.getPreset();
 +          preset_ = worker.getPreset();
          }
 -
 -        if (!processParams(sh, editParams, true))
 +        else
          {
 -          return;
 +          preset_ = preset;
          }
 -        // reinstate worker if it was blacklisted (might have happened due to
 -        // invalid parameters)
 -        alignFrame.getViewport().getCalcManager().enableWorker(worker);
 -        worker.updateParameters(this.preset, paramset);
 +        processParams(sh, editParams, true).thenAccept((startJob) -> {
 +          if (startJob)
 +          {
 +            // reinstate worker if it was blacklisted (might have happened due
 +            // to
 +            // invalid parameters)
 +            alignFrame.getViewport().getCalcManager().enableWorker(worker);
 +            worker.updateParameters(this.preset, paramset);
 +            startSeqAnnotationWorker(sh, alignFrame, preset_, editParams);
 +          }
 +        });
        }
      }
--    if (sh.action.toLowerCase(Locale.ROOT).contains("disorder"))
++    else
++    {
++      startSeqAnnotationWorker(sh, alignFrame, preset, editParams);
++    }
++  }
++
++  private void startSeqAnnotationWorker(ServiceWithParameters sh,
++      AlignFrame alignFrame, WsParamSetI preset, boolean editParams)
++  {
++    if (!sh.isInteractiveUpdate())
      {
        // build IUPred style client. take sequences, returns annotation per
        // sequence.
index a221aa0,0000000..69cd008
mode 100644,000000..100644
--- /dev/null
@@@ -1,339 -1,0 +1,340 @@@
 +/*
 + * 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.ws.jws2.jabaws2;
 +
 +import jalview.analysis.AlignmentAnnotationUtils;
 +import jalview.api.FeatureColourI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.GraphLine;
 +import jalview.datamodel.SequenceFeature;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherSetI;
 +import jalview.schemes.FeatureColour;
 +import jalview.util.ColorUtils;
 +
 +import java.awt.Color;
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.Hashtable;
 +import java.util.Iterator;
 +import java.util.List;
 +import java.util.Map;
 +
 +import compbio.data.sequence.Range;
 +import compbio.data.sequence.Score;
 +import compbio.data.sequence.ScoreManager.ScoreHolder;
 +
 +public class AADisorderClient extends JabawsAnnotationInstance
 +{
 +  // static configuration
 +  public static String getServiceActionText()
 +  {
 +    return "Submitting amino acid sequences for disorder prediction.";
 +  }
 +
 +  // minSeq = 1; protein only, no gaps
 +
 +  // instance
 +  public AADisorderClient(Jws2Instance handle)
 +  {
 +    super(handle);
 +
 +  }
 +
 +  @Override
 +  List<AlignmentAnnotation> annotationFromScoreManager(AlignmentI seqs,
 +          Map<String, FeatureColourI> featureColours,
 +          Map<String, FeatureMatcherSetI> featureFilters)
 +  {
 +
 +    Map<String, String[]> featureTypeMap = featureMap.get(our.getName());
 +    Map<String, Map<String, Object>> annotTypeMap = annotMap
 +            .get(our.getName());
 +    boolean dispFeatures = false;
 +    Map<String, SequenceFeature> fc = new Hashtable<>(),
 +            fex = new Hashtable();
 +    List<AlignmentAnnotation> ourAnnot = new ArrayList<>();
 +    int graphGroup = 1, lastAnnot = 0;
 +
 +    for (SequenceI seq : seqs.getSequences())
 +    {
 +      String seqId = seq.getName();
 +      boolean sameGroup = false;
 +      SequenceI dseq, aseq;
 +      int base = seq.findPosition(0) - 1;
 +      aseq = seq;
 +      while ((dseq = seq).getDatasetSequence() != null)
 +      {
 +        seq = seq.getDatasetSequence();
 +      }
 +      ScoreHolder scores = null;
 +      try
 +      {
 +        scores = scoremanager.getAnnotationForSequence(seqId);
 +      } catch (Exception q)
 +      {
-         Cache.log.info("Couldn't recover disorder prediction for sequence "
++        Console.info("Couldn't recover disorder prediction for sequence "
 +                + seq.getName() + "(Prediction name was " + seqId + ")"
 +                + "\nSee http://issues.jalview.org/browse/JAL-1319 for one possible reason why disorder predictions might fail.",
 +                q);
 +      }
 +      float last = Float.NaN, val = Float.NaN;
 +      if (scores != null && scores.scores != null)
 +      {
 +        for (Score scr : scores.scores)
 +        {
 +
 +          if (scr.getRanges() != null && scr.getRanges().size() > 0)
 +          {
 +            Iterator<Float> vals = scr.getScores().iterator();
 +            // make features on sequence
 +            for (Range rn : scr.getRanges())
 +            {
 +              // TODO: Create virtual feature settings
 +              SequenceFeature sf;
 +              String[] type = featureTypeMap.get(scr.getMethod());
 +              if (type == null)
 +              {
 +                // create a default type for this feature
 +                type = new String[] {
 +                    typeName + " (" + scr.getMethod() + ")",
 +                    our.getActionText() };
 +              }
 +              if (vals.hasNext())
 +              {
 +                val = vals.next().floatValue();
 +                sf = new SequenceFeature(type[0], type[1], base + rn.from,
 +                        base + rn.to, val, methodName);
 +              }
 +              else
 +              {
 +                sf = new SequenceFeature(type[0], type[1], base + rn.from,
 +                        base + rn.to, methodName);
 +              }
 +              dseq.addSequenceFeature(sf);
 +              // mark feature as requiring a graduated colourscheme if has
 +              // variable scores
 +              if (!Float.isNaN(last) && !Float.isNaN(val) && last != val)
 +              {
 +                fc.put(sf.getType(), sf);
 +              } else 
 +              {
 +                fex.put(sf.getType(), sf);
 +              }
 +              last = val;
 +              dispFeatures = true;
 +            }
 +          }
 +          else
 +          {
 +            if (scr.getScores().size() == 0)
 +            {
 +              continue;
 +            }
 +            String typename, calcName;
 +            AlignmentAnnotation annot = createAnnotationRowsForScores(
 +                    seqs, null, ourAnnot,
 +                    typename = our.getName() + " (" + scr.getMethod() + ")",
 +                    calcName = our.getNameURI() + "/" + scr.getMethod(),
 +                    aseq, base + 1, scr);
 +            annot.graph = AlignmentAnnotation.LINE_GRAPH;
 +
 +            Map<String, Object> styleMap = (annotTypeMap == null) ? null
 +                    : annotTypeMap.get(scr.getMethod());
 +
 +            annot.visible = (styleMap == null
 +                    || styleMap.get(INVISIBLE) == null);
 +            double[] thrsh = (styleMap == null) ? null
 +                    : (double[]) styleMap.get(THRESHOLD);
 +            float[] range = (styleMap == null) ? null
 +                    : (float[]) styleMap.get(RANGE);
 +            if (range != null)
 +            {
 +              annot.graphMin = range[0];
 +              annot.graphMax = range[1];
 +            }
 +            if (styleMap == null || styleMap.get(DONTCOMBINE) == null)
 +            {
 +              {
 +                if (!sameGroup)
 +                {
 +                  graphGroup++;
 +                  sameGroup = true;
 +                }
 +
 +                annot.graphGroup = graphGroup;
 +              }
 +            }
 +
 +            annot.description = "<html>" + our.getActionText()
 +                    + " - raw scores";
 +            if (thrsh != null)
 +            {
 +              String threshNote = (thrsh[0] > 0 ? "Above " : "Below ")
 +                      + thrsh[1] + " indicates disorder";
 +              annot.threshold = new GraphLine((float) thrsh[1], threshNote,
 +                      Color.red);
 +              annot.description += "<br/>" + threshNote;
 +            }
 +            annot.description += "</html>";
 +            Color col = ColorUtils
 +                    .createColourFromName(typeName + scr.getMethod());
 +            for (int p = 0, ps = annot.annotations.length; p < ps; p++)
 +            {
 +              if (annot.annotations[p] != null)
 +              {
 +                annot.annotations[p].colour = col;
 +              }
 +            }
 +            annot._linecolour = col;
 +            // finally, update any dataset annotation
 +            AlignmentAnnotationUtils.replaceAnnotationOnAlignmentWith(annot,
 +                    typename, calcName);
 +          }
 +        }
 +      }
 +      if (lastAnnot + 1 == ourAnnot.size())
 +      {
 +        // remove singleton alignment annotation row
 +        ourAnnot.get(lastAnnot).graphGroup = -1;
 +      }
 +    }
 +    {
 +      if (dispFeatures)
 +      {
 +        // TODO: virtual feature settings
 +        // feature colours need to merged with current viewport's colours
 +        // simple feature colours promoted to colour-by-score ranges using
 +        // currently assigned or created feature colour
 +        for (String ft : fex.keySet())
 +        {
 +          Color col = ColorUtils.createColourFromName(ft);
 +          // set graduated color as fading to white for minimum, and
 +          // autoscaling to values on alignment
 +          
 +          FeatureColourI ggc;
 +          if (fc.get(ft) != null)
 +          {
 +            ggc = new FeatureColour(col, Color.white, col,
 +
 +                  Color.white, Float.MIN_VALUE, Float.MAX_VALUE);
 +            ggc.setAutoScaled(true);
 +          }
 +          else
 +          {
 +            ggc = new FeatureColour(col);
 +          }
 +          featureColours.put(ft, ggc);
 +        }
 +
 +      }
 +      return ourAnnot;
 +    }
 +  }
 +
 +  private static final String THRESHOLD = "THRESHOLD";
 +
 +  private static final String RANGE = "RANGE";
 +
 +  String typeName;
 +
 +  String methodName;
 +
 +  String groupName;
 +
 +  private static Map<String, Map<String, String[]>> featureMap;
 +
 +  private static Map<String, Map<String, Map<String, Object>>> annotMap;
 +
 +  private static String DONTCOMBINE = "DONTCOMBINE";
 +
 +  private static String INVISIBLE = "INVISIBLE";
 +  static
 +  {
 +    // TODO: turn this into some kind of configuration file that's a bit easier
 +    // to edit
 +    featureMap = new HashMap<>();
 +    Map<String, String[]> fmap;
 +    featureMap.put(compbio.ws.client.Services.IUPredWS.toString(),
 +            fmap = new HashMap<>());
 +    fmap.put("Glob",
 +            new String[]
 +            { "Globular Domain", "Predicted globular domain" });
 +    featureMap.put(compbio.ws.client.Services.JronnWS.toString(),
 +            fmap = new HashMap<>());
 +    featureMap.put(compbio.ws.client.Services.DisemblWS.toString(),
 +            fmap = new HashMap<>());
 +    fmap.put("REM465", new String[] { "REM465", "Missing density" });
 +    fmap.put("HOTLOOPS", new String[] { "HOTLOOPS", "Flexible loops" });
 +    fmap.put("COILS", new String[] { "COILS", "Random coil" });
 +    featureMap.put(compbio.ws.client.Services.GlobPlotWS.toString(),
 +            fmap = new HashMap<>());
 +    fmap.put("GlobDoms",
 +            new String[]
 +            { "Globular Domain", "Predicted globular domain" });
 +    fmap.put("Disorder",
 +            new String[]
 +            { "Protein Disorder", "Probable unstructured peptide region" });
 +    Map<String, Map<String, Object>> amap;
 +    annotMap = new HashMap<>();
 +    annotMap.put(compbio.ws.client.Services.GlobPlotWS.toString(),
 +            amap = new HashMap<>());
 +    amap.put("Dydx", new HashMap<String, Object>());
 +    amap.get("Dydx").put(DONTCOMBINE, DONTCOMBINE);
 +    amap.get("Dydx").put(THRESHOLD, new double[] { 1, 0 });
 +    amap.get("Dydx").put(RANGE, new float[] { -1, +1 });
 +
 +    amap.put("SmoothedScore", new HashMap<String, Object>());
 +    amap.get("SmoothedScore").put(INVISIBLE, INVISIBLE);
 +    amap.put("RawScore", new HashMap<String, Object>());
 +    amap.get("RawScore").put(INVISIBLE, INVISIBLE);
 +    annotMap.put(compbio.ws.client.Services.DisemblWS.toString(),
 +            amap = new HashMap<>());
 +    amap.put("COILS", new HashMap<String, Object>());
 +    amap.put("HOTLOOPS", new HashMap<String, Object>());
 +    amap.put("REM465", new HashMap<String, Object>());
 +    amap.get("COILS").put(THRESHOLD, new double[] { 1, 0.516 });
 +    amap.get("COILS").put(RANGE, new float[] { 0, 1 });
 +
 +    amap.get("HOTLOOPS").put(THRESHOLD, new double[] { 1, 0.6 });
 +    amap.get("HOTLOOPS").put(RANGE, new float[] { 0, 1 });
 +    amap.get("REM465").put(THRESHOLD, new double[] { 1, 0.1204 });
 +    amap.get("REM465").put(RANGE, new float[] { 0, 1 });
 +
 +    annotMap.put(compbio.ws.client.Services.IUPredWS.toString(),
 +            amap = new HashMap<>());
 +    amap.put("Long", new HashMap<String, Object>());
 +    amap.put("Short", new HashMap<String, Object>());
 +    amap.get("Long").put(THRESHOLD, new double[] { 1, 0.5 });
 +    amap.get("Long").put(RANGE, new float[] { 0, 1 });
 +    amap.get("Short").put(THRESHOLD, new double[] { 1, 0.5 });
 +    amap.get("Short").put(RANGE, new float[] { 0, 1 });
 +    annotMap.put(compbio.ws.client.Services.JronnWS.toString(),
 +            amap = new HashMap<>());
 +    amap.put("JRonn", new HashMap<String, Object>());
 +    amap.get("JRonn").put(THRESHOLD, new double[] { 1, 0.5 });
 +    amap.get("JRonn").put(RANGE, new float[] { 0, 1 });
 +  }
 +
 +}
index 9e8cefa,0000000..07f5614
mode 100644,000000..100644
--- /dev/null
@@@ -1,212 -1,0 +1,213 @@@
 +package jalview.ws.jws2.jabaws2;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.gui.WebserviceInfo;
 +import jalview.util.MessageManager;
 +import jalview.ws.gui.WsJob;
 +import jalview.ws.gui.WsJob.JobState;
 +import jalview.ws.jws2.JabaParamStore;
 +import jalview.ws.jws2.JabaPreset;
 +import jalview.ws.jws2.dm.JabaWsParamSet;
 +import jalview.ws.params.WsParamSetI;
 +
 +import java.util.ArrayList;
 +import java.util.HashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +import compbio.metadata.Argument;
 +import compbio.metadata.ChunkHolder;
 +import compbio.metadata.JobStatus;
 +import compbio.metadata.Preset;
 +
 +/**
 + * Base class for JABAWS service instances. Provides helper methods for
 + * interfacing with Jalview.
 + * 
 + * @author jprocter
 + *
 + */
 +public class JabawsServiceInstance<T extends compbio.data.msa.JManagement>
 +        implements
 +        jalview.ws.api.JalviewWebServiceI, jalview.ws.api.CancellableI
 +{
 +  /**
 +   * our service instance handler generated by the discoverer
 +   */
 +  Jws2Instance our;
 +  protected T service;
 +  protected Map<JobStatus, JobState> jwsState = new HashMap<>();
 +
 +  @Override
 +  public boolean cancel(WsJob job)
 +  {
 +    service.cancelJob(job.getJobId());
 +    // if the Jaba server indicates the job can't be cancelled, its
 +    // because its running on the server's local execution engine
 +    // so we just close the window anyway.
 +  
 +    return true;
 +  }
 +
 +
 +  public JabawsServiceInstance(Jws2Instance handle)
 +  {
 +    our = handle;
 +    service = (T) handle.service;
 +  }
 +
 +  @Override
 +  public void updateStatus(WsJob job)
 +  {
 +    JobStatus jwsstatus = service.getJobStatus(job.getJobId());
 +    job.setState(jwsState.get(jwsstatus));
 +  }
 +
 +  @Override
 +  public boolean updateJobProgress(WsJob job) throws Exception
 +  {
 +    StringBuilder response = new StringBuilder(job.getStatus());
 +    long lastchunk = job.getNextChunk();
 +    if (lastchunk == -1)
 +    {
-       Cache.log.debug("No more status messages for job " + job.getJobId());
++      Console.debug("No more status messages for job " + job.getJobId());
 +      return false;
 +    }
 +    boolean changed = false;
 +    do
 +    {
 +      ChunkHolder chunk = service.pullExecStatistics(job.getJobId(),
 +              lastchunk);
 +      if (chunk != null)
 +      {
 +        changed |= chunk.getChunk().length() > 0;
 +        response.append(chunk.getChunk());
 +        lastchunk = chunk.getNextPosition();
 +        try
 +        {
 +          Thread.sleep(50);
 +        } catch (InterruptedException x)
 +        {
 +        }
 +        ;
 +      }
 +      ;
 +      job.setnextChunk(lastchunk);
 +    } while (lastchunk >= 0 && job.getNextChunk() != lastchunk);
 +    if (job instanceof WsJob)
 +    {
 +      // TODO decide if WsJob will be the bean for all ng-webservices
 +      job.setStatus(response.toString());
 +    }
 +    return changed;
 +  }
 +
 +  {
 +    jwsState.put(JobStatus.CANCELLED, JobState.CANCELLED);
 +    jwsState.put(JobStatus.COLLECTED, JobState.FINISHED);
 +    jwsState.put(JobStatus.FAILED, JobState.FAILED);
 +    jwsState.put(JobStatus.FINISHED, JobState.FINISHED);
 +    jwsState.put(JobStatus.PENDING, JobState.QUEUED);
 +    jwsState.put(JobStatus.RUNNING, JobState.RUNNING);
 +    jwsState.put(JobStatus.STARTED, JobState.RUNNING);
 +    jwsState.put(JobStatus.SUBMITTED, JobState.SUBMITTED);
 +    jwsState.put(JobStatus.UNDEFINED, JobState.UNKNOWN);
 +  }
 +
 +  public boolean isPresetJob(WsJob job)
 +  {
 +    return job.getPreset() != null && job.getPreset() instanceof JabaPreset;
 +  }
 +
 +  public Preset getServerPreset(WsJob job)
 +  {
 +    return (isPresetJob(job))
 +            ? ((JabaPreset) job.getPreset()).getJabaPreset()
 +            : null;
 +  }
 +
 +  public List<Argument> getJabaArguments(WsParamSetI preset)
 +  {
 +    List<Argument> newargs = new ArrayList<>();
 +    if (preset != null)
 +    {
 +      if (preset instanceof JabaWsParamSet)
 +      {
 +        newargs.addAll(((JabaWsParamSet) preset).getjabaArguments());
 +      }
 +      else
 +      {
 +        newargs.addAll(
 +                JabaParamStore.getJabafromJwsArgs(preset.getArguments()));
 +      }
 +    }
 +    return newargs;
 +  }
 +
 +  @Override
 +  public boolean handleSubmitError(Throwable _lex, WsJob j,
 +          WebserviceInfo wsInfo) throws Exception, Error
 +  {
 +    if (_lex instanceof compbio.metadata.UnsupportedRuntimeException)
 +    {
 +      wsInfo.appendProgressText(MessageManager.formatMessage(
 +              "info.job_couldnt_be_run_server_doesnt_support_program",
 +              new String[]
 +              { _lex.getMessage() }));
 +      wsInfo.warnUser(_lex.getMessage(),
 +              MessageManager.getString("warn.service_not_supported"));
 +      wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_SERVERERROR);
 +      wsInfo.setStatus(j.getJobnum(),
 +              WebserviceInfo.STATE_STOPPED_SERVERERROR);
 +      return true;
 +    }
 +    if (_lex instanceof compbio.metadata.LimitExceededException)
 +    {
 +      wsInfo.appendProgressText(MessageManager.formatMessage(
 +              "info.job_couldnt_be_run_exceeded_hard_limit", new String[]
 +              { _lex.getMessage() }));
 +      wsInfo.warnUser(_lex.getMessage(),
 +              MessageManager.getString("warn.input_is_too_big"));
 +      wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
 +      wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
 +      return true;
 +    }
 +    if (_lex instanceof compbio.metadata.WrongParameterException)
 +    {
 +      wsInfo.warnUser(_lex.getMessage(),
 +              MessageManager.getString("warn.invalid_job_param_set"));
 +      wsInfo.appendProgressText(MessageManager.formatMessage(
 +              "info.job_couldnt_be_run_incorrect_param_setting",
 +              new String[]
 +              { _lex.getMessage() }));
 +      wsInfo.setStatus(WebserviceInfo.STATE_STOPPED_ERROR);
 +      wsInfo.setStatus(j.getJobnum(), WebserviceInfo.STATE_STOPPED_ERROR);
 +      return true;
 +    }
 +    // pass on to generic error handler
 +    return false;
 +  }
 +
 +  @Override
 +  public boolean handleCollectionException(Exception ex, WsJob msjob,
 +          WebserviceInfo wsInfo)
 +  {
 +    if (ex instanceof compbio.metadata.ResultNotAvailableException)
 +    {
 +      // job has failed for some reason - probably due to invalid
 +      // parameters
-       Cache.log.debug(
++      Console.debug(
 +              "Results not available for finished job - marking as broken job.",
 +              ex);
 +      String status = msjob.getStatus();
 +
 +      msjob.setStatus(status
 +              + "\nResult not available. Probably due to invalid input or parameter settings. Server error message below:\n\n"
 +              + ex.getLocalizedMessage());
 +      msjob.setState(WsJob.JobState.BROKEN);
 +      return true;
 +    }
 +    return false;
 +  }
 +}
@@@ -40,9 -40,9 +40,10 @@@ import org.apache.http.HttpEntity
  import org.apache.http.HttpResponse;
  import org.apache.http.client.methods.HttpRequestBase;
  import org.apache.http.entity.mime.MultipartEntity;
 +import org.apache.http.util.EntityUtils;
  import org.apache.james.mime4j.MimeException;
  import org.apache.james.mime4j.parser.MimeStreamParser;
  /**
   * data source instantiated from the response of an httpclient request.
   * 
@@@ -130,8 -131,8 +131,8 @@@ public class HttpResultSet extends File
        {
          error = true;
          errormessage = "Couldn't parse message from web service.";
-         Cache.log.warn("Failed to parse MIME multipart content", me);
+         Console.warn("Failed to parse MIME multipart content", me);
 -        en.consumeContent();
 +        EntityUtils.consume(en);
        }
        return new ParsePackedSet().getAlignment(ds,
                handler.getJalviewDataProviders());
                  : new InputStreamReader(en.getContent());
        } catch (UnsupportedEncodingException e)
        {
-         Cache.log.error("Can't handle encoding '" + enc
+         Console.error("Can't handle encoding '" + enc
                  + "' for response from webservice.", e);
 -        en.consumeContent();
 +        EntityUtils.consume(en);
          error = true;
          errormessage = "Can't handle encoding for response from webservice";
          return;
Simple merge
@@@ -50,27 -49,8 +50,24 @@@ import javax.swing.event.MenuListener
   * 
   */
  public class RestClient extends WSClient
 -        implements WSClientI, WSMenuEntryProviderI
 +implements WSClientI, WSMenuEntryProviderI, ApplicationSingletonI
  {
 +  @SuppressWarnings("unused")
 +  private RestClient()
 +  {
 +    // accessed by ApplicationSingletonProvider
 +  }
 +
 +  
 +private static RestClient getInstance()
 +{
 +return (RestClient) ApplicationSingletonProvider.getInstance(RestClient.class);
 +}
 +
 +public static final String RSBS_SERVICES = "RSBS_SERVICES";
 +
 +
 +  protected Vector<String> services = null;
    RestServiceDescription service;
  
    public RestClient(RestServiceDescription rsd)
      }
    }
  
 -  public static RestClient makeShmmrRestClient()
 -  {
 -    String action = "Analysis",
 -            description = "Sequence Harmony and Multi-Relief (Brandt et al. 2010)",
 -            name = MessageManager.getString("label.multiharmony");
 -    Hashtable<String, InputType> iparams = new Hashtable<String, InputType>();
 -    jalview.ws.rest.params.JobConstant toolp;
 -    // toolp = new jalview.ws.rest.JobConstant("tool","jalview");
 -    // iparams.put(toolp.token, toolp);
 -    // toolp = new jalview.ws.rest.params.JobConstant("mbjob[method]","shmr");
 -    // iparams.put(toolp.token, toolp);
 -    // toolp = new
 -    // jalview.ws.rest.params.JobConstant("mbjob[description]","step 1");
 -    // iparams.put(toolp.token, toolp);
 -    // toolp = new jalview.ws.rest.params.JobConstant("start_search","1");
 -    // iparams.put(toolp.token, toolp);
 -    // toolp = new jalview.ws.rest.params.JobConstant("blast","0");
 -    // iparams.put(toolp.token, toolp);
 -
 -    jalview.ws.rest.params.Alignment aliinput = new jalview.ws.rest.params.Alignment();
 -    // SHMR server has a 65K limit for content pasted into the 'ali' parameter,
 -    // so we always upload our files.
 -    aliinput.token = "ali_file";
 -    aliinput.writeAsFile = true;
 -    iparams.put(aliinput.token, aliinput);
 -    jalview.ws.rest.params.SeqGroupIndexVector sgroups = new jalview.ws.rest.params.SeqGroupIndexVector();
 -    sgroups.setMinsize(2);
 -    sgroups.min = 2;// need at least two group defined to make a partition
 -    iparams.put("groups", sgroups);
 -    sgroups.token = "groups";
 -    sgroups.sep = " ";
 -    RestServiceDescription shmrService = new RestServiceDescription(action,
 -            description, name,
 -            "http://zeus.few.vu.nl/programs/shmrwww/index.php?tool=jalview", // ?tool=jalview&mbjob[method]=shmr&mbjob[description]=step1",
 -            "?tool=jalview", iparams, true, false, '-');
 -    // a priori knowledge of the data returned from the service
 -    shmrService.addResultDatatype(JvDataType.ANNOTATION);
 -    return new RestClient(shmrService);
 -  }
    public AlignmentPanel recoverAlignPanelForView()
    {
      AlignmentPanel[] aps = Desktop
      return true;
    }
  
 -  protected static Vector<String> services = null;
 -
 -  public static final String RSBS_SERVICES = "RSBS_SERVICES";
    public static RestClient[] getRestClients()
    {
 +    return getInstance().getClients();
 +  }
 +    
 +  private RestClient[] getClients()
 +  {
      if (services == null)
      {
 -      services = new Vector<String>();
 +      services = new Vector<>();
        try
        {
          for (RestServiceDescription descr : RestServiceDescription
-                 .parseDescriptions(jalview.bin.Cache.getDefault(
 -                .parseDescriptions(Cache.getDefault(RSBS_SERVICES,
 -                        makeShmmrRestClient().service.toString())))
++                .parseDescriptions(Cache.getDefault(
 +                        RSBS_SERVICES,
 +                        ShmrRestClient.makeShmmrRestClient().service.toString())))
          {
            services.add(descr.toString());
          }
@@@ -280,18 -286,31 +280,16 @@@ public class RestJobThread extends AWST
        {
        case 200:
          rj.running = false;
-         Cache.log.debug("Processing result set.");
+         Console.debug("Processing result set.");
          processResultSet(rj, response, request);
          break;
        case 202:
 -        rj.statMessage = "<br>Job submitted successfully. Results available at this URL:\n"
 -                + "<a href=" + rj.getJobId() + "\">" + rj.getJobId()
 -                + "</a><br>";
 -        rj.running = true;
 +        markJobAsRunning(rj);
          break;
 +      case 201:
 +        // Created - redirect may be present. Fallthrough to 302
        case 302:
 -        Header[] loc;
 -        if (!rj.isSubmitted()
 -                && (loc = response
 -                        .getHeaders(HTTPConstants.HEADER_LOCATION)) != null
 -                && loc.length > 0)
 -        {
 -          if (loc.length > 1)
 -          {
 -            Console.warn("Ignoring additional " + (loc.length - 1)
 -                    + " location(s) provided in response header ( next one is '"
 -                    + loc[1].getValue() + "' )");
 -          }
 -          rj.setJobId(loc[0].getValue());
 -          rj.setSubmitted(true);
 -        }
 +        extractJobId(rj, response);
          completeStatus(rj, response);
          break;
        case 500:
      }
    }
  
 +  private void markAsFailed(RestJob rj, HttpResponse response)
 +  {
 +    // Failed.
 +    rj.setSubmitted(true);
 +    rj.setAllowedServerExceptions(0);
 +    rj.setSubjobComplete(true);
 +    rj.error = true;
 +    rj.running = false;
 +  }
 +
 +  /**
 +   * set the jobRunning flag and post a link to the physical result page encoded
 +   * in rj.getJobId()
 +   * 
 +   * @param rj
 +   */
 +  private void markJobAsRunning(RestJob rj)
 +  {
 +    rj.statMessage = "<br>Job submitted successfully. Results available at this URL:\n"
 +            + "<a href="
 +            + rj.getJobId()
 +            + "\">"
 +            + rj.getJobId()
 +            + "</a><br>";
 +    rj.running = true;
 +  }
 +
 +  /**
 +   * extract the job ID URL from the redirect page. Does nothing if job is
 +   * already running.
 +   * 
 +   * @param rj
 +   * @param response
 +   */
 +  private void extractJobId(RestJob rj, HttpResponse response)
 +  {
 +    Header[] loc;
 +    if (!rj.isSubmitted())
 +    {
 +
 +      // redirect URL - typical for IBIVU type jobs.
 +      if ((loc = response.getHeaders(HTTPConstants.HEADER_LOCATION)) != null
 +              && loc.length > 0)
 +      {
 +        if (loc.length > 1)
 +        {
-           Cache.log
-                   .warn("Ignoring additional "
++          Console.warn("Ignoring additional "
 +                          + (loc.length - 1)
 +                          + " location(s) provided in response header ( next one is '"
 +                          + loc[1].getValue() + "' )");
 +        }
 +        rj.setJobId(loc[0].getValue());
 +        rj.setSubmitted(true);
 +      }
 +    }
 +  }
    /**
     * job has completed. Something valid should be available from con
     * 
@@@ -256,7 -228,7 +256,7 @@@ public class SiftsClient implements Sif
        // The line below is required for unit testing... don't comment it out!!!
        System.out.println(">>> SIFTS File already downloaded for " + pdbId);
  
--      if (isFileOlderThanThreshold(siftsFile,
++      if (Platform.isFileOlderThanThreshold(siftsFile,
                SiftsSettings.getCacheThresholdInDays()))
        {
          File oldSiftsFile = new File(siftsFileName + "_old");
index c42d42e,0000000..999951a
mode 100644,000000..100644
--- /dev/null
@@@ -1,103 -1,0 +1,104 @@@
 +package jalview.ws.slivkaws;
 +
 +import jalview.api.FeatureColourI;
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentAnnotation;
 +import jalview.datamodel.SequenceI;
 +import jalview.datamodel.features.FeatureMatcherSetI;
 +import jalview.io.AnnotationFile;
 +import jalview.io.DataSourceType;
 +import jalview.io.FeaturesFile;
 +import jalview.util.MessageManager;
 +import jalview.ws.api.JobId;
 +import jalview.ws.api.SequenceAnnotationServiceI;
 +import jalview.ws.params.ArgumentI;
 +import jalview.ws.params.WsParamSetI;
 +import jalview.ws.uimodel.AlignAnalysisUIText;
 +
 +import java.io.IOError;
 +import java.io.IOException;
 +import java.util.Arrays;
 +import java.util.Collection;
 +import java.util.List;
 +import java.util.Map;
 +
 +import compbio.data.msa.Category;
 +import uk.ac.dundee.compbio.slivkaclient.RemoteFile;
 +import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
 +import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
 +
 +public class SlivkaAnnotationServiceInstance extends SlivkaWSInstance implements SequenceAnnotationServiceI
 +{
 +  public SlivkaAnnotationServiceInstance(SlivkaClient client,
 +          SlivkaService service, String category)
 +  {
 +    super(client, service, category);
 +    if (category == Category.CATEGORY_CONSERVATION)
 +    {
 +      /* FIXME: the category name is hardcoded for AACon, names other than
 +       * "AAConWS" doesn't work. */
 +      setAlignAnalysisUI(new AlignAnalysisUIText(getName(),
 +              SlivkaAnnotationServiceInstance.class,
 +              "Slivka.AACons", false, true, true, true, true, 2,
 +              MessageManager.getString("label.aacon_calculations"),
 +              MessageManager.getString("tooltip.aacon_calculations"),
 +              MessageManager.getString("label.aacon_settings"),
 +              MessageManager.getString("tooltip.aacon_settings")));
 +    }
 +    style = ServiceClient.SEQUENCEANNOTATIONWSCLIENT;
 +  }
 +
 +  @Override
 +  public JobId submitToService(List<SequenceI> seqs, WsParamSetI preset, List<ArgumentI> paramset) throws Throwable
 +  {
 +    return super.submit(seqs, preset, paramset);
 +  }
 +
 +  @Override
 +  public List<AlignmentAnnotation> getAnnotationResult(JobId jobId,
 +          List<SequenceI> seqs, Map<String, FeatureColourI> featureColours,
 +          Map<String, FeatureMatcherSetI> featureFilters) throws Throwable
 +  {
 +    RemoteFile annotFile = null;
 +    RemoteFile featFile = null;
 +    try
 +    {
 +      var slivkaJob = client.getJob(jobId.getJobId());
 +      Collection<RemoteFile> files = slivkaJob.getResults();
 +      for (RemoteFile f : files)
 +      {
 +        if (f.getMediaType().equals("application/jalview-annotations"))
 +        {
 +          annotFile = f;
 +        }
 +        else if (f.getMediaType().equals("application/jalview-features"))
 +        {
 +          featFile = f;
 +        }
 +      }
 +    } catch (IOException e)
 +    {
 +      throw new IOError(e);
 +    }
 +    Alignment aln = new Alignment(seqs.toArray(new SequenceI[0]));
 +    if (annotFile == null
 +        || !new AnnotationFile().readAnnotationFileWithCalcId(aln, service.getId(), annotFile.getContentUrl().toString(), DataSourceType.URL))
 +    {
-       Cache.log.debug("No annotation from slivka job\n" + annotFile);
++      Console.debug("No annotation from slivka job\n" + annotFile);
 +    }
 +    else {
-       Cache.log.debug("Annotation file loaded " + annotFile);
++      Console.debug("Annotation file loaded " + annotFile);
 +    }
 +    if (featFile == null
 +        || !new FeaturesFile(featFile.getContentUrl().toString(), DataSourceType.URL).parse(aln, featureColours, true))
 +    {
-       Cache.log.debug("No features from slivka job\n" + featFile);
++      Console.debug("No features from slivka job\n" + featFile);
 +    }
 +    else {
-       Cache.log.debug("Features feil loaded " + featFile);
++      Console.debug("Features feil loaded " + featFile);
 +    }
 +    return Arrays.asList(aln.getAlignmentAnnotation());
 +  }
 +}
index 8653f68,0000000..d21d5d1
mode 100644,000000..100644
--- /dev/null
@@@ -1,237 -1,0 +1,238 @@@
 +package jalview.ws.slivkaws;
 +
 +import jalview.bin.Cache;
++import jalview.bin.Console;
 +import jalview.ws.ServiceChangeListener;
 +import jalview.ws.WSDiscovererI;
 +import jalview.ws.api.ServiceWithParameters;
 +import javajs.http.HttpClientFactory;
 +
 +import java.io.IOException;
 +import java.net.MalformedURLException;
 +import java.net.URL;
 +import java.util.ArrayList;
 +import java.util.Collections;
 +import java.util.List;
 +import java.util.Set;
 +import java.util.Vector;
 +import java.util.concurrent.CompletableFuture;
 +import java.util.concurrent.CopyOnWriteArraySet;
 +import java.util.concurrent.ExecutorService;
 +import java.util.concurrent.Executors;
 +import java.util.concurrent.Future;
 +
 +import compbio.data.msa.Category;
 +import uk.ac.dundee.compbio.slivkaclient.SlivkaClient;
 +import uk.ac.dundee.compbio.slivkaclient.SlivkaService;
 +
 +public class SlivkaWSDiscoverer implements WSDiscovererI
 +{
 +  private static final String SLIVKA_HOST_URLS = "SLIVKAHOSTURLS";
 +
 +  private static final String COMPBIO_SLIVKA = "https://www.compbio.dundee.ac.uk/slivka/";
 +
 +  private static SlivkaWSDiscoverer instance = null;
 +
 +  private List<ServiceWithParameters> services = List.of();
 +
 +  private SlivkaWSDiscoverer()
 +  {
 +  }
 +
 +  public static SlivkaWSDiscoverer getInstance()
 +  {
 +    if (instance == null)
 +    {
 +      instance = new SlivkaWSDiscoverer();
 +    }
 +    return instance;
 +  }
 +
 +  private Set<ServiceChangeListener> serviceListeners = new CopyOnWriteArraySet<>();
 +
 +  @Override
 +  public void addServiceChangeListener(ServiceChangeListener l)
 +  {
 +    serviceListeners.add(l);
 +  }
 +
 +  @Override
 +  public void removeServiceChangeListener(ServiceChangeListener l)
 +  {
 +    serviceListeners.remove(l);
 +  }
 +
 +  public void notifyServiceListeners(List<ServiceWithParameters> services)
 +  {
 +    for (var listener : serviceListeners)
 +    {
 +      listener.servicesChanged(this, services);
 +    }
 +  }
 +
 +  private final ExecutorService executor = Executors
 +          .newSingleThreadExecutor();
 +
 +  private Vector<Future<?>> discoveryTasks = new Vector<>();
 +
 +  public CompletableFuture<WSDiscovererI> startDiscoverer()
 +  {
 +    CompletableFuture<WSDiscovererI> task = CompletableFuture
 +            .supplyAsync(() -> {
 +              reloadServices();
 +              return SlivkaWSDiscoverer.this;
 +            }, executor);
 +    discoveryTasks.add(task);
 +    return task;
 +  }
 +
 +  private List<ServiceWithParameters> reloadServices()
 +  {
-     Cache.log.info("Reloading Slivka services");
++    Console.info("Reloading Slivka services");
 +    notifyServiceListeners(Collections.emptyList());
 +    ArrayList<ServiceWithParameters> instances = new ArrayList<>();
 +
 +    for (String url : getServiceUrls())
 +    {
 +      SlivkaClient client = new SlivkaClient(url);
 +
 +      List<SlivkaService> services;
 +      try
 +      {
 +        services = client.getServices();
 +      } catch (IOException e)
 +      {
 +        e.printStackTrace();
 +        continue;
 +      }
 +      for (SlivkaService service : services)
 +      {
 +        SlivkaWSInstance newInstance = null;
 +        for (String classifier : service.classifiers)
 +        {
 +          String[] path = classifier.split("\\s*::\\s*");
 +          if (path.length >= 3 && path[0].toLowerCase().equals("operation")
 +                  && path[1].toLowerCase().equals("analysis"))
 +          {
 +            switch (path[path.length - 1].toLowerCase())
 +            {
 +            case "rna secondary structure prediction":
 +              newInstance = new RNAalifoldServiceInstance(client,
 +                      service, "Secondary Structure Prediction");
 +              break;
 +            case "sequence alignment analysis (conservation)":
 +              newInstance = new SlivkaAnnotationServiceInstance(client,
 +                      service, Category.CATEGORY_CONSERVATION);
 +              break;
 +            case "protein sequence analysis":
 +              newInstance = new SlivkaAnnotationServiceInstance(client,
 +                      service, Category.CATEGORY_DISORDER);
 +              break;
 +            case "protein secondary structure prediction":
 +              newInstance = new SlivkaAnnotationServiceInstance(client,
 +                      service, "Secondary Structure Prediction");
 +              break;
 +            case "multiple sequence alignment":
 +              newInstance = new SlivkaMsaServiceInstance(client, service,
 +                      Category.CATEGORY_ALIGNMENT);
 +              break;
 +            }
 +          }
 +          if (newInstance != null)
 +            break;
 +        }
 +        if (newInstance != null)
 +          instances.add(newInstance);
 +      }
 +    }
 +
 +    services = instances;
-     Cache.log.info("Slivka services reloading finished");
++    Console.info("Slivka services reloading finished");
 +    notifyServiceListeners(instances);
 +    return instances;
 +  }
 +
 +  @Override
 +  public List<ServiceWithParameters> getServices()
 +  {
 +    return services;
 +  }
 +
 +  @Override
 +  public boolean hasServices()
 +  {
 +    return !isRunning() && services.size() > 0;
 +  }
 +
 +  @Override
 +  public boolean isRunning()
 +  {
 +    return !discoveryTasks.stream().allMatch(Future::isDone);
 +  }
 +
 +  @Override
 +  public void setServiceUrls(List<String> wsUrls)
 +  {
 +    if (wsUrls != null && !wsUrls.isEmpty())
 +    {
 +      Cache.setProperty(SLIVKA_HOST_URLS, String.join(",", wsUrls));
 +    }
 +    else
 +    {
 +      Cache.removeProperty(SLIVKA_HOST_URLS);
 +    }
 +  }
 +
 +  @Override
 +  public List<String> getServiceUrls()
 +  {
 +    String surls = Cache.getDefault(SLIVKA_HOST_URLS, COMPBIO_SLIVKA);
 +    String[] urls = surls.split(",");
 +    ArrayList<String> valid = new ArrayList<>(urls.length);
 +    for (String url : urls)
 +    {
 +      try
 +      {
 +        new URL(url);
 +        valid.add(url);
 +      } catch (MalformedURLException e)
 +      {
-         Cache.log.warn("Problem whilst trying to make a URL from '"
++        Console.warn("Problem whilst trying to make a URL from '"
 +                + ((url != null) ? url : "<null>") + "'");
-         Cache.log.warn(
++        Console.warn(
 +                "This was probably due to a malformed comma separated list"
 +                        + " in the " + SLIVKA_HOST_URLS
 +                        + " entry of $(HOME)/.jalview_properties)");
-         Cache.log.debug("Exception was ", e);
++        Console.debug("Exception was ", e);
 +      }
 +    }
 +    return valid;
 +  }
 +
 +  @Override
 +  public boolean testServiceUrl(URL url)
 +  {
 +    return getServerStatusFor(url.toString()) == STATUS_OK;
 +  }
 +
 +  @Override
 +  public int getServerStatusFor(String url)
 +  {
 +    try
 +    {
 +      List<?> services = new SlivkaClient(url).getServices();
 +      return services.isEmpty() ? STATUS_NO_SERVICES : STATUS_OK;
 +    } catch (IOException | org.json.JSONException e)
 +    {
-       Cache.log.error("Slivka could not retrieve services list", e);
++      Console.error("Slivka could not retrieve services list", e);
 +      return STATUS_INVALID;
 +    }
 +  }
 +
 +  @Override
 +  public String getErrorMessages()
 +  {
 +    // TODO Auto-generated method stub
 +    return "";
 +  }
 +}
  
  package jalview.ws.utils;
  
 -import jalview.util.Platform;
  import java.io.File;
 -import java.io.FileOutputStream;
  import java.io.IOException;
 -import java.net.URL;
 -import java.nio.channels.Channels;
 -import java.nio.channels.ReadableByteChannel;
 -import java.nio.file.Files;
 -import java.nio.file.Path;
 -import java.nio.file.Paths;
 -import java.nio.file.StandardCopyOption;
 +import jalview.util.Platform;
  
  public class UrlDownloadClient
  {
    public static void download(String urlstring, String outfile)
            throws IOException
    {
 -    FileOutputStream fos = null;
 -    ReadableByteChannel rbc = null;
 -    Path temp = null;
 -    try
 -    {
 -      temp = Files.createTempFile(".jalview_", ".tmp");
 -
 -      URL url = new URL(urlstring);
 -      rbc = Channels.newChannel(url.openStream());
 -      fos = new FileOutputStream(temp.toString());
 -      fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
 -
 -      // copy tempfile to outfile once our download completes
 -      // incase something goes wrong
 -      Files.copy(temp, Paths.get(outfile),
 -              StandardCopyOption.REPLACE_EXISTING);
 -    } catch (IOException e)
 -    {
 -      throw e;
 -    } finally
 -    {
 -      try
 -      {
 -        if (fos != null)
 -        {
 -          fos.close();
 -        }
 -      } catch (IOException e)
 -      {
 -        System.out.println(
 -                "Exception while closing download file output stream: "
 -                        + e.getMessage());
 -      }
 -      try
 -      {
 -        if (rbc != null)
 -        {
 -          rbc.close();
 -        }
 -      } catch (IOException e)
 -      {
 -        System.out.println("Exception while closing download channel: "
 -                + e.getMessage());
 -      }
 -      try
 -      {
 -        if (temp != null)
 -        {
 -          Files.deleteIfExists(temp);
 -        }
 -      } catch (IOException e)
 -      {
 -        System.out.println("Exception while deleting download temp file: "
 -                + e.getMessage());
 -      }
 -    }
 +      Platform.download(urlstring, outfile);
    }
  
 -  public static void download(String urlstring, File tempFile)
 -          throws IOException
 +  public static void download(String urlstring, File tempFile) throws IOException
    {
      if (!Platform.setFileBytes(tempFile, urlstring))
      {
Simple merge
@@@ -17,338 -9,178 +17,339 @@@ import javax.swing.JComponent
  
  import swingjs.api.js.HTML5Applet;
  
- public interface JSUtilI {
+ public interface JSUtilI
+ {
  
 -  /**
 -   * Indicate to SwingJS that the given file type is binary.
 -   * 
 -   * @param ext
 -   */
 -  void addBinaryFileType(String ext);
 -
 -  /**
 -   * Indicate to SwingJS that we can load files using AJAX from the given
 -   * domain, such as "www.stolaf.edu", because we know that CORS access has been
 -   * provided.
 -   * 
 -   * @param domain
 -   */
 -  void addDirectDatabaseCall(String domain);
 -
 -  /**
 -   * Cache or uncache data under the given path name.
 -   * 
 -   * @param path
 -   * @param data
 -   *          null to remove from the cache
 -   */
 -  void cachePathData(String path, Object data);
 -
 -  /**
 -   * Get the HTML5 object corresponding to the specified Component, or the
 -   * current thread if null.
 -   * 
 -   * @param c
 -   *          the associated component, or null for the current thread
 -   * @return HTML5 applet object
 -   */
 -  HTML5Applet getAppletForComponent(Component c);
 -
 -  /**
 -   * Get an attribute applet.foo for the applet found using getApplet(null).
 -   * 
 -   * @param key
 -   * @return
 -   */
 -  Object getAppletAttribute(String key);
 -
 -  /**
 -   * Get the code base (swingjs/j2s, probably) for the applet found using
 -   * getApplet(null).
 -   * 
 -   * @return
 -   */
 -  URL getCodeBase();
 -
 -  /**
 -   * Get the document base (wherever the page is) for the applet found using
 -   * getApplet(null).
 -   * 
 -   * @return
 -   */
 -
 -  URL getDocumentBase();
 -
 -  /**
 -   * Get an attribute from the div on the page that is associated with this
 -   * frame, i.e. with id frame.getName() + "-div".
 -   * 
 -   * @param frame
 -   * @param type
 -   *          "node" or "dim"
 -   * @return
 -   */
 -  Object getEmbeddedAttribute(Component frame, String type);
 -
 -  /**
 -   * Get a file synchronously.
 -   * 
 -   * @param path
 -   * @param asString
 -   *          true for String; false for byte[]
 -   * @return byte[] or String
 -   */
 -  Object getFile(String path, boolean asString);
 -
 -  /**
 -   * Get the ç§˜bytes field associated with a file, but only if the File object
 -   * itself has them attached, not downloading them.
 -   * 
 -   * @param f
 -   * @return
 -   */
 -  byte[] getBytes(File f);
 -
 -  /**
 -   * Retrieve a HashMap consisting of whatever the application wants, but
 -   * guaranteed to be unique to this app context, that is, for the applet found
 -   * using getApplet(null).
 -   * 
 -   * @param contextKey
 -   * @return
 -   */
 -  HashMap<?, ?> getJSContext(Object contextKey);
 -
 -  /**
 -   * Load a resource -- probably a core file -- if and only if a particular
 -   * class has not been instantialized. We use a String here because if we used
 -   * a .class object, that reference itself would simply load the class, and we
 -   * want the core package to include that as well.
 -   * 
 -   * @param resourcePath
 -   * @param className
 -   */
 -  void loadResourceIfClassUnknown(String resource, String className);
 -
 -  /**
 -   * Read all applet.__Info properties for the applet found using
 -   * getApplet(null) that start with the given prefix, such as "jalview_". A
 -   * null prefix retrieves all properties. Note that non-string properties will
 -   * be stringified.
 -   * 
 -   * @param prefix
 -   *          an application prefix, or null for all properties
 -   * @param p
 -   *          properties to be appended to
 -   */
 -  void readInfoProperties(String prefix, Properties p);
 -
 -  /**
 -   * Set an attribute for the applet found using getApplet(null). That is,
 -   * applet[key] = val.
 -   * 
 -   * @param key
 -   * @param val
 -   */
 -  void setAppletAttribute(String key, Object val);
 -
 -  /**
 -   * Set an attribute of applet's Info map for the applet found using
 -   * getApplet(null). That is, applet.__Info[key] = val.
 -   * 
 -   * @param infoKey
 -   * @param val
 -   */
 -  void setAppletInfo(String infoKey, Object val);
 -
 -  /**
 -   * Set the given File object's ç§˜bytes field from an InputStream or a byte[]
 -   * array. If the file is a JSTempFile, then also cache those bytes.
 -   * 
 -   * @param f
 -   * @param isOrBytes
 -   *          BufferedInputStream, ByteArrayInputStream, FileInputStream, or
 -   *          byte[]
 -   * @return
 -   */
 -  boolean setFileBytes(File f, Object isOrBytes);
 -
 -  /**
 -   * Same as setFileBytes, but also caches the data if it is a JSTempFile.
 -   * 
 -   * @param is
 -   * @param outFile
 -   * @return
 -   */
 -  boolean streamToFile(InputStream is, File outFile);
 -
 -  /**
 -   * Switch the flag in SwingJS to use or not use the JavaScript Map object in
 -   * Hashtable, HashMap, and HashSet. Default is enabled.
 -   * 
 -   */
 -
 -  void setJavaScriptMapObjectEnabled(boolean enabled);
 +      /**
 +       * The HTML5 canvas delivers [r g b a r g b a ...] which is not a Java option.
 +       * The closest Java option is TYPE_4BYTE_ABGR, but that is not quite what we
 +       * need. SwingJS decodes TYPE_4BYTE_HTML5 as TYPE_4BYTE_RGBA"
 +       * 
 +       * ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
 +       * 
 +       * int[] nBits = { 8, 8, 8, 8 };
 +       * 
 +       * int[] bOffs = { 0, 1, 2, 3 };
 +       * 
 +       * colorModel = new ComponentColorModel(cs, nBits, true, false,
 +       * Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
 +       * 
 +       * raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height,
 +       * width * 4, 4, bOffs, null);
 +       * 
 +       * Note, however, that this buffer type should only be used for direct buffer access
 +       * using
 +       * 
 +       * 
 +       * 
 +       */
 +      public static final int TYPE_4BYTE_HTML5 = -6;
 +      
 +      /**
 +       * The HTML5 VIDEO element wrapped in a BufferedImage. 
 +       * 
 +       * To be extended to allow video capture?
 +       */
 +      public static final int TYPE_HTML5_VIDEO = Integer.MIN_VALUE;
 +
 +      /**
 +       * Indicate to SwingJS that the given file type is binary.
 +       * 
 +       * @param ext
 +       */
 +      void addBinaryFileType(String ext);
 +
 +      /**
 +       * Indicate to SwingJS that we can load files using AJAX from the given domain,
 +       * such as "www.stolaf.edu", because we know that CORS access has been provided.
 +       * 
 +       * @param domain
 +       */
 +      void addDirectDatabaseCall(String domain);
 +
 +      /**
 +       * Cache or uncache data under the given path name.
 +       * 
 +       * @param path
 +       * @param data null to remove from the cache
 +       */
 +      void cachePathData(String path, Object data);
 +
 +      /**
 +       * Get the HTML5 object corresponding to the specified Component, or the current thread if null.
 +       * 
 +       * @param c  the associated component, or null for the current thread
 +       * @return HTML5 applet object
 +       */
 +      HTML5Applet getAppletForComponent(Component c);
 +
 +      /**
 +       * Get an attribute applet.foo for the applet found using getApplet(null).
 +       * 
 +       * @param key
 +       * @return
 +       */
 +      Object getAppletAttribute(String key);
 +
 +
 +      /**
 +       * Get the applet's __Info map or an attribute of that map for the applet found using
 +       * getApplet(null). That is, applet.__Info or applet.__Info[InfoKey].
 +       * 
 +       * @param infoKey if null, return the full __Info map
 +       */
 +      Object getAppletInfo(String infoKey);
 +
 +      /**
 +       * Get the code base (swingjs/j2s, probably) for the applet found using
 +       * getApplet(null).
 +       * 
 +       * @return
 +       */
 +      URL getCodeBase();
 +
 +      /**
 +       * Get the document base (wherever the page is) for the applet found using
 +       * getApplet(null).
 +       * 
 +       * @return
 +       */
 +
 +      URL getDocumentBase();
 +
 +      /**
 +       * Get an attribute from the div on the page that is associated with this frame,
 +       * i.e. with id frame.getName() + "-div".
 +       * 
 +       * @param frame
 +       * @param type  "node" or "dim"
 +       * @return
 +       */
 +      Object getEmbeddedAttribute(Component frame, String type);
 +
 +      /**
 +       * Get a file synchronously.
 +       * 
 +       * @param path
 +       * @param asString true for String; false for byte[]
 +       * @return byte[] or String
 +       */
 +      Object getFile(String path, boolean asString);
 +
 +      /**
 +       * Get the ç§˜bytes field associated with a file, but only if the File object itself has
 +       * them attached, not downloading them.
 +       * 
 +       * @param f
 +       * @return
 +       */
 +      byte[] getBytes(File f);
 +
 +      /**
 +       * Retrieve a HashMap consisting of whatever the application wants, but
 +       * guaranteed to be unique to this app context, that is, for the applet found using
 +       * getApplet(null).
 +       * 
 +       * @param contextKey
 +       * @return
 +       */
 +      HashMap<?, ?> getJSContext(Object contextKey);
 +
 +      /**
 +       * Load a resource -- probably a core file -- if and only if a particular class
 +       * has not been instantialized. We use a String here because if we used a .class
 +       * object, that reference itself would simply load the class, and we want the
 +       * core package to include that as well.
 +       * 
 +       * @param resourcePath
 +       * @param className
 +       */
 +      void loadResourceIfClassUnknown(String resource, String className);
 +
 +      /**
 +       * Read all applet.__Info properties  for the applet found using
 +       * getApplet(null) that start with the given prefix, such as "jalview_".
 +       * A null prefix retrieves all properties. Note that non-string properties will be
 +       * stringified.
 +       * 
 +       * @param prefix an application prefix, or null for all properties
 +       * @param p      properties to be appended to
 +       */
 +      void readInfoProperties(String prefix, Properties p);
 +
 +      /**
 +       * Set an attribute for the applet found using
 +       * getApplet(null). That is, applet[key] = val.
 +       * 
 +       * @param key
 +       * @param val
 +       */
 +      void setAppletAttribute(String key, Object val);
 +
 +      /**
 +       * Set an attribute of applet's Info map for the applet found using
 +       * getApplet(null). That is, applet.__Info[key] = val.
 +       * 
 +       * @param infoKey
 +       * @param val
 +       */
 +      void setAppletInfo(String infoKey, Object val);
 +
 +      /**
 +       * Set the given File object's ç§˜bytes field from an InputStream or a byte[] array.
 +       * If the file is a JSTempFile, then also cache those bytes.
 +       * 
 +       * @param f
 +       * @param isOrBytes BufferedInputStream, ByteArrayInputStream, FileInputStream, or byte[]
 +       * @return
 +       */
 +      boolean setFileBytes(File f, Object isOrBytes);
 +
 +      /**
 +       * Set the given URL object's _streamData field from an InputStream or a byte[] array.
 +       * 
 +       * @param f
 +       * @param isOrBytes BufferedInputStream, ByteArrayInputStream, FileInputStream, or byte[]
 +       * @return
 +       */
 +      boolean setURLBytes(URL url, Object isOrBytes);
 +
 +      /**
 +       * Same as setFileBytes.
 +       * 
 +       * @param is
 +       * @param outFile
 +       * @return
 +       */
 +      boolean streamToFile(InputStream is, File outFile);
 +
 +        /**
 +         * Switch the flag in SwingJS to use or not use the JavaScript Map object in
 +         * Hashtable, HashMap, and HashSet. Default is enabled.
 +         *       * 
 +         */
 +      void setJavaScriptMapObjectEnabled(boolean enabled);
 +
 +
 +      /**
 +       * Open a URL in a browser tab.
 +       * 
 +       * @param url
 +       * @param target null or specific tab, such as "_blank"
 +       */
 +      void displayURL(String url, String target);
 +
 +      /**
 +       * Retrieve cached bytes for a path (with unnormalized name)
 +       * from J2S._javaFileCache.
 +       * 
 +       * @param path
 +       * 
 +       * @return byte[] or null
 +       */
 +      byte[] getCachedBytes(String path);
 +      
 +      /**
 +       * Attach cached bytes to a file-like object, including URL,
 +       * or anything having a ç§˜bytes field (File, URI, Path)
 +       * from J2S._javaFileCache. That is, allow two such objects
 +       * to share the same underlying byte[ ] array.
 +       * 
 +       * 
 +       * @param URLorURIorFile
 +       * @return byte[] or null
 +       */
 +      byte[] addJSCachedBytes(Object URLorURIorFile);
 +
 +      /**
 +       * Seek an open ZipInputStream to the supplied ZipEntry, if possible.
 +       * 
 +       * @param zis the ZipInputStream
 +       * @param ze  the ZipEntry
 +       * @return the length of this entry, or -1 if, for whatever reason, this was not possible
 +       */
 +      long seekZipEntry(ZipInputStream zis, ZipEntry ze);
 +
 +      /**
 +       * Retrieve the byte array associated with a ZipEntry.
 +       * 
 +       * @param ze
 +       * @return
 +       */
 +      byte[] getZipBytes(ZipEntry ze);
 +
 +      /**
 +       * Java 9 method to read all (remaining) bytes from an InputStream. In SwingJS,
 +       * this may just create a new reference to an underlying Int8Array without
 +       * copying it.
 +       * 
 +       * @param zis
 +       * @return
 +       * @throws IOException 
 +       */
 +      byte[] readAllBytes(InputStream zis) throws IOException;
 +
 +      /**
 +       * Java 9 method to transfer all (remaining) bytes from an InputStream to an OutputStream.
 +       * 
 +       * @param is
 +       * @param out
 +       * @return
 +       * @throws IOException
 +       */
 +      long transferTo(InputStream is, OutputStream out) throws IOException;
 +
 +      /**
 +       * Retrieve any bytes already attached to this URL.
 +       * 
 +       * @param url
 +       * @return
 +       */
 +      byte[] getURLBytes(URL url);
 +
 +      /**
 +       * Set a message in the lower-left-hand corner SwingJS status block.
 +       * 
 +       * @param msg
 +       * @param doFadeOut
 +       */
 +      void showStatus(String msg, boolean doFadeOut);
 +
 +      /**
 +       * Asynchronously retrieve the byte[] for a URL.
 +       * 
 +       * @param url
 +       * @param whenDone
 +       */
 +      void getURLBytesAsync(URL url, Function<byte[], Void> whenDone);
 +
 +      /**
 +       * Experimental method to completely disable a Swing Component's user interface.
 +       * 
 +       * @param jc
 +       * @param enabled
 +       */
 +      void setUIEnabled(JComponent jc, boolean enabled);
 +
 +
 +      /**
 +       * Play an audio
 +       * @param buffer
 +       * @param format a javax.sound.sampled.AudioFormat
 +       * @throws Exception 
 +       */
 +      void playAudio(byte[] buffer, Object format) throws Exception;
 +
 +      /**
 +       * For either an applet or an application, get the ORIGINAL __Info as a Map that
 +       * has a full set up lower-case keys along with whatever non-all-lower-case keys
 +       * provided at start-up.
 +       * 
 +       * @return
 +       */
 +      Map<String, Object> getAppletInfoAsMap();
 +
 +      
 +  void setAppClass(Object j);
  
  }
@@@ -28,18 -35,15 +28,19 @@@ import java.io.PrintStream
  import java.util.Arrays;
  import java.util.Random;
  
 -import org.testng.annotations.BeforeClass;
 +import jalview.datamodel.Alignment;
 +import jalview.datamodel.AlignmentI;
 +import jalview.datamodel.Sequence;
 +import jalview.datamodel.SequenceI;
 +import jalview.io.FastaFile;
  
  /**
-  * Generates, and outputs in Fasta format, a random peptide or nucleotide alignment for given
-  * sequence length and count. Will regenerate the same alignment each time if
-  * the same random seed is used (so may be used for reproducible unit tests).
-  * Not guaranteed to reproduce the same results between versions, as the rules
-  * may get tweaked to produce more 'realistic' results.
+  * Generates, and outputs in Fasta format, a random peptide or nucleotide
+  * alignment for given sequence length and count. Will regenerate the same
+  * alignment each time if the same random seed is used (so may be used for
+  * reproducible unit tests). Not guaranteed to reproduce the same results
+  * between versions, as the rules may get tweaked to produce more 'realistic'
+  * results.
   * 
   * @author gmcarstairs
   */
Simple merge
@@@ -95,15 -92,14 +95,15 @@@ public class ResidueCountTes
      assertEquals(rc.getCount(' '), 4);
      assertEquals(rc.getCount('-'), 4);
      assertEquals(rc.getCount('.'), 4);
 +    assertEquals(rc.getTotalResidueCount(), 0);
      assertFalse(rc.isUsingOtherData());
      assertFalse(rc.isCountingInts());
-     
-     rc.set(ResidueCount.GAP_COUNT, Short.MAX_VALUE-2);
-     assertEquals(rc.getGapCount(), Short.MAX_VALUE-2);
+     rc.set(ResidueCount.GAP_COUNT, Short.MAX_VALUE - 2);
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE - 2);
      assertFalse(rc.isCountingInts());
      rc.addGap();
-     assertEquals(rc.getGapCount(), Short.MAX_VALUE-1);
+     assertEquals(rc.getGapCount(), Short.MAX_VALUE - 1);
      assertFalse(rc.isCountingInts());
      rc.addGap();
      assertEquals(rc.getGapCount(), Short.MAX_VALUE);
@@@ -76,7 -77,8 +77,8 @@@ public class JmolCommandsTes
      SequenceRenderer sr = new SequenceRenderer(af.getViewport());
      SequenceI[][] seqs = new SequenceI[][] { { seq1 }, { seq2 } };
      String[] files = new String[] { "seq1.pdb", "seq2.pdb" };
--    StructureSelectionManager ssm = new StructureSelectionManager();
++    StructureSelectionManager ssm = StructureSelectionManager.getStructureSelectionManager(null);
      /*
       * map residues 1-10 to residues 21-30 (atoms 105-150) in structures
       */
      StructureMapping sm2 = new StructureMapping(seq2, "seq2.pdb", "pdb2",
              "B", map, null);
      ssm.addStructureMapping(sm2);
-     String[] commands = testee.colourBySequence(ssm,
-             files,
-             seqs, sr, af.alignPanel);
++    // TODO - comments in testee suggest this tests an obsolete method!
+     String[] commands = testee.colourBySequence(ssm, files, seqs, sr,
+             af.alignPanel);
      assertEquals(commands.length, 2);
-     assertEquals(commands[0].commands.length, 1); // from 2.12 merge from 2.11.2
  
      String chainACommand = commands[0];
      // M colour is #82827d == (130, 130, 125) (see strand.html help page)
Simple merge
@@@ -100,9 -101,8 +103,10 @@@ public class AlignmentPanelTes
      assertEquals(ranges.getEndRes(), oldres);
  
      af.alignPanel.setScrollValues(0, 5);
 +    // no update necessary now
      // setting 0 as x value does not change residue
 +    // no update necessary now
      assertEquals(ranges.getEndRes(), oldres);
  
      af.alignPanel.setScrollValues(5, 5);
  
      // scroll to position after hidden columns sets endres to oldres (width) +
      // position
 -    int scrollpos = 60;
 +    int scrollpos = 53; // was 60, but this is too high to allow full scrolling
 +                        // in Windows
      af.getViewport().hideColumns(30, 50);
      af.alignPanel.setScrollValues(scrollpos, 5);
 +    // no update necessary now
      assertEquals(ranges.getEndRes(), oldres + scrollpos);
  
      // scroll to position within hidden columns, still sets endres to oldres +
@@@ -86,14 -86,9 +86,13 @@@ public class JvSwingUtilsTes
              JvSwingUtils.wrapTooltip(true, tip));
  
      tip = "0123456789012345678901234567890123456789012345678901234567890"; // 61
 -    assertFalse(tip.equals(JvSwingUtils.wrapTooltip(false, tip)));
 -    assertFalse(("<html>" + tip + "</html>")
 -            .equals(JvSwingUtils.wrapTooltip(true, tip)));
 +    // n/a -- message is too long for "false"
 +//  
 +//    assertFalse(tip.equals(JvSwingUtils.wrapTooltip(false, tip)));
 +//    
 +    
 +    assertFalse(("<html>" + tip + "</html>").equals(JvSwingUtils
 +            .wrapTooltip(true, tip)));
    }
  
    /**
Simple merge
@@@ -26,33 -31,20 +26,53 @@@ import static org.testng.Assert.assertN
  import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertTrue;
  
 +import java.awt.Font;
 +import java.awt.FontMetrics;
 +
- import org.testng.annotations.BeforeMethod;
++import org.testng.annotations.BeforeClass;
 +import org.testng.annotations.Test;
 +
  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 jalview.util.Platform;
 +import jalview.viewmodel.ViewportRanges;
  import junit.extensions.PA;
  
  public class SeqCanvasTest
  {
 +  @BeforeClass(alwaysRun = true)
 +  public void setUp()
 +  {
++      // 2.11.2 - beforeMethod setup
++      //          Cache.loadProperties("test/jalview/io/testProps.jvprops");
++      //Cache.applicationProperties.setProperty("SHOW_IDENTITY",
++      //      Boolean.TRUE.toString());
++      //    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
++      //     DataSourceType.FILE);
++      //     /*
++      // * wait for Consensus thread to complete
++     // */
++      // do
++      // {
++      //try
++        //{
++        // Thread.sleep(50);
++      //} catch (InterruptedException x)
++        //{
++        //}
++      //    } while (af.getViewport().getCalcManager().isWorking());
++
 +    Cache.loadProperties(null);
-     Cache.initLogger();
 +    Desktop.getInstance().setVisible(false);
 +  }
 +
+   private AlignFrame af;
    /**
     * Test the method that computes wrapped width in residues, height of wrapped
     * widths in pixels, and the number of widths visible
      assertEquals(repeatingHeight, charHeight * (2 + al.getHeight()));
    }
  
--  @BeforeMethod(alwaysRun = true)
--  public void setUp()
--  {
--    Cache.loadProperties("test/jalview/io/testProps.jvprops");
--    Cache.applicationProperties.setProperty("SHOW_IDENTITY",
--            Boolean.TRUE.toString());
--    af = new FileLoader().LoadFileWaitTillLoaded("examples/uniref50.fa",
--            DataSourceType.FILE);
-   
-     /*
-      * wait for Consensus thread to complete
-      */
-     do
-     {
-       try
-       {
-         Thread.sleep(50);
-       } catch (InterruptedException x)
-       {
-       }
-     } while (af.getViewport().getCalcManager().isWorking());
-   }
 -
 -    /*
 -     * wait for Consensus thread to complete
 -     */
 -    do
 -    {
 -      try
 -      {
 -        Thread.sleep(50);
 -      } catch (InterruptedException x)
 -      {
 -      }
 -    } while (af.getViewport().getCalcManager().isWorking());
 -  }
 -
    @Test(groups = "Functional")
    public void testClear_HighlightAndSelection()
    {
Simple merge
@@@ -115,11 -114,13 +115,11 @@@ public class StructureChooserTes
      upSeq.setDescription("Breast cancer type 1 susceptibility protein");
      upSeq_nocanonical = new Sequence(upSeq);
      upSeq.createDatasetSequence();
-     upSeq.addDBRef(new DBRefEntry("UNIPROT","0","P38398",null,true));
-     
+     upSeq.addDBRef(new DBRefEntry("UNIPROT", "0", "P38398", null, true));
      upSeq_nocanonical.createDatasetSequence();
      // not a canonical reference
 -    upSeq_nocanonical.addDBRef(
 -            new DBRefEntry("UNIPROT", "0", "P38398", null, false));
 -
 +    upSeq_nocanonical.addDBRef(new DBRefEntry("UNIPROT","0","P38398",null,false));
    }
  
    @AfterMethod(alwaysRun = true)
          Thread.sleep(50);
        } catch (InterruptedException x)
        {
-         
        }
      }
-     
 -
    }
 -
    @Test(groups = { "Functional" })
    public void sanitizeSeqNameTest()
    {
Simple merge
Simple merge
@@@ -1,5 -1,27 +1,25 @@@
+ /*
+  * 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.util.Locale;
 -
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertFalse;
  import static org.testng.Assert.assertNotEquals;
@@@ -1,9 -1,25 +1,29 @@@
+ /*
+  * 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 static org.testng.Assert.assertEquals;
 +
 +import jalview.bin.Cache;
 +
  import org.junit.Assert;
  import org.testng.annotations.Test;
  
Simple merge
@@@ -324,9 -329,10 +325,10 @@@ public class SequenceAnnotationReportTe
  
      sb.setLength(0);
      sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
-     expected = "<i>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
+     expected = "<i>SeqDesc\n" + "\n"
+             + "<br/>Metal ; Desc<br/>Type1 ; Nonpos</i>";
      assertEquals(expected, sb.toString());
 -
 +    
      /*
       * 'linkonly' features are ignored; this is obsolete, as linkonly
       * is only set by DasSequenceFetcher, and DAS is history
      {
        seq.addDBRef(new DBRefEntry("Uniprot", "0", "P3041" + i));
      }
 -
 +  
      sar.createSequenceAnnotationReport(sb, seq, true, true, null, true);
      String report = sb.toString();
-     assertTrue(report
-             .startsWith(
-                     "<i><br>UNIPROT P30410, P30411, P30412, P30413,...<br>PDB0 3iu1"));
-     assertTrue(report
-             .endsWith(
-                     "<br>PDB7 3iu1<br>PDB8,...<br>(Output Sequence Details to list all database references)</i>"));
+     assertTrue(report.startsWith("<i>\n" + "<br/>\n" + "UNIPROT P30410,\n"
+             + " P30411,\n" + " P30412,\n" + " P30413,...<br/>\n"
+             + "PDB0 3iu1<br/>\n" + "PDB1 3iu1<br/>"));
+     assertTrue(report.endsWith("PDB5 3iu1<br/>\n" + "PDB6 3iu1<br/>\n"
+             + "PDB7 3iu1<br/>\n" + "PDB8,...<br/>\n"
+             + "(Output Sequence Details to list all database references)\n"
+             + "</i>"));
    }
  
    /**
@@@ -366,16 -377,17 +377,18 @@@ public class StockholmFileTes
                    "Sequence Features were not equivalent"
                            + (ignoreFeatures ? " ignoring." : ""),
                    ignoreFeatures
-                           || (seq_original[i].getSequenceFeatures() == null && seq_new[in]
-                                   .getSequenceFeatures() == null)
-                           || (seq_original[i].getSequenceFeatures() != null && seq_new[in]
-                                   .getSequenceFeatures() != null));
+                           || (seq_original[i].getSequenceFeatures() == null
+                                   && seq_new[in]
+                                           .getSequenceFeatures() == null)
+                           || (seq_original[i].getSequenceFeatures() != null
+                                   && seq_new[in]
+                                           .getSequenceFeatures() != null));
            // compare sequence features
 -          if (seq_original[i].getSequenceFeatures() != null
 +          if (!ignoreFeatures
 +                  && seq_original[i].getSequenceFeatures() != null
                    && seq_new[in].getSequenceFeatures() != null)
            {
 -            System.out.println("There are feature!!!");
 +            System.out.println("Checking feature equivalence.");
              sequenceFeatures_original = seq_original[i]
                      .getSequenceFeatures();
              sequenceFeatures_new = seq_new[in].getSequenceFeatures();
@@@ -93,8 -92,6 +94,7 @@@ import jalview.util.matcher.Condition
  import jalview.viewmodel.AlignmentViewport;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  
 +import junit.extensions.PA;
  @Test(singleThreaded = true)
  public class Jalview2xmlTests extends Jalview2xmlBase
  {
      AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
              "examples/exampleFile_2_7.jar", DataSourceType.FILE);
      assertNotNull(af, "Didn't read in the example file correctly.");
 -    assertTrue(Desktop.getAlignFrames().length == 1 + origCount,
 +    assertEquals(Desktop.getAlignFrames().length,
 +            1 + origCount,
              "Didn't gather the views in the example file.");
    }
  
    /**
@@@ -86,15 -81,13 +81,13 @@@ public class ResidueColourFinderTes
    {
      SequenceI seq = new Sequence("name", "MA--TVLGSPRAPAFF");
      AlignmentI al = new Alignment(new SequenceI[] { seq });
 -    final AlignViewport av = new AlignViewport(al);
 +    final AlignViewportI av = new AlignViewport(al);
      ResidueColourFinder rcf = new ResidueColourFinder();
  
-     assertEquals(Color.white,
-             rcf.getResidueColour(true, av.getResidueShading(),
-             null, seq, 0, null));
-     assertEquals(Color.white,
-             rcf.getResidueColour(true, av.getResidueShading(),
-             null, seq, 2, null));
+     assertEquals(Color.white, rcf.getResidueColour(true,
+             av.getResidueShading(), null, seq, 0, null));
+     assertEquals(Color.white, rcf.getResidueColour(true,
+             av.getResidueShading(), null, seq, 2, null));
  
      // no change if showBoxes is false
      assertEquals(Color.white, rcf.getResidueColour(false,
@@@ -5,12 -25,12 +25,12 @@@ import static org.testng.Assert.assertF
  import static org.testng.Assert.assertNotEquals;
  import static org.testng.Assert.assertNull;
  import static org.testng.Assert.assertTrue;
  import java.awt.Color;
  import java.util.List;
  
 +import org.testng.annotations.BeforeClass;
  import org.testng.annotations.BeforeMethod;
+ import org.testng.annotations.BeforeTest;
  import org.testng.annotations.Test;
  
  import jalview.api.FeatureColourI;
@@@ -24,6 -44,6 +44,7 @@@ import jalview.schemes.FeatureColour
  import jalview.viewmodel.seqfeatures.FeatureRendererModel;
  import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
  
++
  /**
   * Unit tests for feature colour determination, including but not limited to
   * <ul>
@@@ -143,6 -162,27 +164,26 @@@ public class FeatureColourFinderTes
      assertNull(c);
    }
  
+   /**
+    * Nested features coloured by label - expect the colour of the enclosed
+    * feature
+    */
+   @Test(groups = "Functional")
+   public void testFindFeatureColour_nestedFeatures()
+   {
+     SequenceFeature sf1 = new SequenceFeature("domain", "peptide", 1, 120, 0f, null);
+     seq.addSequenceFeature(sf1);
+     SequenceFeature sf2 = new SequenceFeature("domain", "binding", 10, 20,
+             0f, null);
+     seq.addSequenceFeature(sf2);
+     FeatureColourI fc = new FeatureColour(Color.red);
+     fc.setColourByLabel(true);
+     fr.setColour("domain", fc);
+     fr.featuresAdded();
+     av.setShowSequenceFeatures(true);
+     Color c = finder.findFeatureColour(null, seq, 15);
+     assertEquals(c, fr.getColor(sf2, fc));
+   }
 -
    @Test(groups = "Functional")
    public void testFindFeatureColour_multipleFeaturesAtPositionNoTransparency()
    {
@@@ -85,7 -105,9 +105,9 @@@ public class ClustalxColourSchemeTes
  
      // TODO more test cases; check if help documentation matches implementation
    }
+   // @formatter:on
 -
 +  
    /**
     * Test for colour calculation when the consensus percentage ignores gapped
     * sequences
@@@ -72,13 -72,14 +72,15 @@@ public class Mappin
        // original numbers taken from
        // http://www.ebi.ac.uk/pdbe-srv/view/entry/1qcf/secondary.html
        // these are in numbering relative to the subsequence above
-       int coils[] = { 266, 275, 278, 287, 289, 298, 302, 316 }, helices[] = new int[]
-       { 303, 315 }, sheets[] = new int[] { 267, 268, 269, 270 };
+       int coils[] = { 266, 275, 278, 287, 289, 298, 302, 316 },
+               helices[] = new int[]
+               { 303, 315 }, sheets[] = new int[] { 267, 268, 269, 270 };
  
 -      StructureSelectionManager ssm = new jalview.structure.StructureSelectionManager();
 +      StructureSelectionManager ssm = StructureSelectionManager
 +              .getStructureSelectionManager(null);
        StructureFile pmap = ssm.setMapping(true, new SequenceI[] { uprot },
-               new String[] { "A" }, "test/jalview/ext/jmol/1QCF.pdb",
+               new String[]
+               { "A" }, "test/jalview/ext/jmol/1QCF.pdb",
                DataSourceType.FILE);
        assertTrue(pmap != null);
        SequenceI protseq = pmap.getSeqsAsArray()[0];
    @Test(groups = { "Functional" })
    public void mapFer1From3W5V() throws Exception
    {
-     AlignFrame seqf = new FileLoader(false)
-             .LoadFileWaitTillLoaded(
-                     ">FER1_MAIZE/1-150 Ferredoxin-1, chloroplast precursor\nMATVLGSPRAPAFFFSSSSLRAAPAPTAVALPAAKVGIMGRSASSRRRLRAQATYNVKLITPEGEVELQVPD\nDVYILDQAEEDGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQSYLDDGQIADGWVLTCHAYPTSDVVIETHKE\nEELTGA",
-                     DataSourceType.PASTE, FileFormat.Fasta);
+     AlignFrame seqf = new FileLoader(false).LoadFileWaitTillLoaded(
+             ">FER1_MAIZE/1-150 Ferredoxin-1, chloroplast precursor\nMATVLGSPRAPAFFFSSSSLRAAPAPTAVALPAAKVGIMGRSASSRRRLRAQATYNVKLITPEGEVELQVPD\nDVYILDQAEEDGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQSYLDDGQIADGWVLTCHAYPTSDVVIETHKE\nEELTGA",
+             DataSourceType.PASTE, FileFormat.Fasta);
      SequenceI newseq = seqf.getViewport().getAlignment().getSequenceAt(0);
 -    StructureSelectionManager ssm = new jalview.structure.StructureSelectionManager();
 +    StructureSelectionManager ssm = StructureSelectionManager
 +            .getStructureSelectionManager(null);
      StructureFile pmap = ssm.setMapping(true, new SequenceI[] { newseq },
-             new String[] { null }, "examples/3W5V.pdb",
-             DataSourceType.FILE);
+             new String[]
+             { null }, "examples/3W5V.pdb", DataSourceType.FILE);
      if (pmap == null)
      {
        AssertJUnit.fail("Couldn't make a mapping for 3W5V to FER1_MAIZE");
@@@ -135,10 -136,9 +136,9 @@@ public class AAStructureBindingModelTes
      // ideally, we would match on the actual data for the 'File' handle for
      // pasted files,
      // see JAL-623 - pasting is still not correctly handled...
-     PDBEntry importedPDB = new PDBEntry("3A6S", "", Type.PDB,
-             "Paste");
+     PDBEntry importedPDB = new PDBEntry("3A6S", "", Type.PDB, "Paste");
      AAStructureBindingModel binder = new AAStructureBindingModel(
 -            new StructureSelectionManager(), new PDBEntry[]
 +            StructureSelectionManager.getStructureSelectionManager(null), new PDBEntry[]
              { importedPDB },
              new SequenceI[][]
              { importedAl.getSequencesArray() }, null)
@@@ -24,13 -24,13 +24,13 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertNull;
  import static org.testng.AssertJUnit.assertSame;
  
 -import jalview.gui.JvOptionPane;
  import java.awt.Color;
  
  import org.testng.annotations.BeforeClass;
  import org.testng.annotations.Test;
  
 +import jalview.gui.JvOptionPane;
  public class ColorUtilsTest
  {
  
       */
      assertNull(ColorUtils.parseColourString(null));
      assertNull(ColorUtils.parseColourString("rubbish"));
-     assertNull(ColorUtils.parseColourString("-1"));
-     assertNull(ColorUtils.parseColourString(String
-             .valueOf(Integer.MAX_VALUE)));
++    // from 2.11.2
+     assertEquals(Color.WHITE, ColorUtils.parseColourString("-1"));
+     assertNull(ColorUtils
+             .parseColourString(String.valueOf(Integer.MAX_VALUE)));
      assertNull(ColorUtils.parseColourString("100,200,300")); // out of range
      assertNull(ColorUtils.parseColourString("100,200")); // too few
      assertNull(ColorUtils.parseColourString("100,200,100,200")); // too many
Simple merge
@@@ -1330,86 -1331,123 +1332,178 @@@ public class MappingUtilsTes
    }
  
    @Test(groups = "Functional")
-   public void testListToArray()
+   public void testFindOverlap()
    {
      List<int[]> ranges = new ArrayList<>();
+     ranges.add(new int[] { 4, 8 });
+     ranges.add(new int[] { 10, 12 });
+     ranges.add(new int[] { 16, 19 });
+     int[] overlap = MappingUtils.findOverlap(ranges, 5, 13);
+     assertArrayEquals(overlap, new int[] { 5, 12 });
+     overlap = MappingUtils.findOverlap(ranges, -100, 100);
+     assertArrayEquals(overlap, new int[] { 4, 19 });
+     overlap = MappingUtils.findOverlap(ranges, 7, 17);
+     assertArrayEquals(overlap, new int[] { 7, 17 });
+     overlap = MappingUtils.findOverlap(ranges, 13, 15);
+     assertNull(overlap);
+   }
  
-     int[] result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 0);
-     ranges.add(new int[] { 24, 12 });
-     result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 2);
-     assertEquals(result[0], 24);
-     assertEquals(result[1], 12);
-     ranges.add(new int[] { -7, 30 });
-     result = MappingUtils.rangeListToArray(ranges);
-     assertEquals(result.length, 4);
-     assertEquals(result[0], 24);
-     assertEquals(result[1], 12);
-     assertEquals(result[2], -7);
-     assertEquals(result[3], 30);
-     try
-     {
-       MappingUtils.rangeListToArray(null);
-       fail("Expected exception");
-     } catch (NullPointerException e)
-     {
-       // expected
-     }
+   /**
+    * Test mapping a sequence group where sequences in and outside the group
+    * share a dataset sequence (e.g. alternative CDS for the same gene)
+    * <p>
+    * This scenario doesn't arise after JAL-3763 changes, but test left as still
+    * valid
+    * 
+    * @throws IOException
+    */
+   @Test(groups = { "Functional" })
+   public void testMapSequenceGroup_sharedDataset() throws IOException
+   {
+     /*
+      * Set up dna and protein Seq1/2/3 with mappings (held on the protein
+      * viewport). CDS sequences share the same 'gene' dataset sequence.
+      */
+     SequenceI dna = new Sequence("dna", "aaatttgggcccaaatttgggccc");
+     SequenceI cds1 = new Sequence("cds1/1-6", "aaattt");
+     SequenceI cds2 = new Sequence("cds1/4-9", "tttggg");
+     SequenceI cds3 = new Sequence("cds1/19-24", "gggccc");
+     cds1.setDatasetSequence(dna);
+     cds2.setDatasetSequence(dna);
+     cds3.setDatasetSequence(dna);
+     SequenceI pep1 = new Sequence("pep1", "KF");
+     SequenceI pep2 = new Sequence("pep2", "FG");
+     SequenceI pep3 = new Sequence("pep3", "GP");
+     pep1.createDatasetSequence();
+     pep2.createDatasetSequence();
+     pep3.createDatasetSequence();
+     /*
+      * add mappings from coding positions of dna to respective peptides
+      */
+     AlignedCodonFrame acf = new AlignedCodonFrame();
+     acf.addMap(dna, pep1,
+             new MapList(new int[]
+             { 1, 6 }, new int[] { 1, 2 }, 3, 1));
+     acf.addMap(dna, pep2,
+             new MapList(new int[]
+             { 4, 9 }, new int[] { 1, 2 }, 3, 1));
+     acf.addMap(dna, pep3,
+             new MapList(new int[]
+             { 19, 24 }, new int[] { 1, 2 }, 3, 1));
+     List<AlignedCodonFrame> acfList = Arrays
+             .asList(new AlignedCodonFrame[]
+             { acf });
+     AlignmentI cdna = new Alignment(new SequenceI[] { cds1, cds2, cds3 });
+     AlignmentI protein = new Alignment(
+             new SequenceI[]
+             { pep1, pep2, pep3 });
+     AlignViewportI cdnaView = new AlignViewport(cdna);
+     AlignViewportI peptideView = new AlignViewport(protein);
+     protein.setCodonFrames(acfList);
+     /*
+      * Select pep1 and pep3 in the protein alignment
+      */
+     SequenceGroup sg = new SequenceGroup();
+     sg.setColourText(true);
+     sg.setIdColour(Color.GREEN);
+     sg.setOutlineColour(Color.LIGHT_GRAY);
+     sg.addSequence(pep1, false);
+     sg.addSequence(pep3, false);
+     sg.setEndRes(protein.getWidth() - 1);
+     /*
+      * Verify the mapped sequence group in dna is cds1 and cds3
+      */
+     SequenceGroup mappedGroup = MappingUtils.mapSequenceGroup(sg,
+             peptideView, cdnaView);
+     assertTrue(mappedGroup.getColourText());
+     assertSame(sg.getIdColour(), mappedGroup.getIdColour());
+     assertSame(sg.getOutlineColour(), mappedGroup.getOutlineColour());
+     assertEquals(2, mappedGroup.getSequences().size());
+     assertSame(cds1, mappedGroup.getSequences().get(0));
+     assertSame(cds3, mappedGroup.getSequences().get(1));
+     // columns 1-6 selected (0-5 base zero)
+     assertEquals(0, mappedGroup.getStartRes());
+     assertEquals(5, mappedGroup.getEndRes());
+     /*
+      * Select mapping sequence group from dna to protein
+      */
+     sg.clear();
+     sg.addSequence(cds2, false);
+     sg.addSequence(cds1, false);
+     sg.setStartRes(0);
+     sg.setEndRes(cdna.getWidth() - 1);
+     mappedGroup = MappingUtils.mapSequenceGroup(sg, cdnaView, peptideView);
+     assertTrue(mappedGroup.getColourText());
+     assertSame(sg.getIdColour(), mappedGroup.getIdColour());
+     assertSame(sg.getOutlineColour(), mappedGroup.getOutlineColour());
+     assertEquals(2, mappedGroup.getSequences().size());
+     assertSame(protein.getSequenceAt(1), mappedGroup.getSequences().get(0));
+     assertSame(protein.getSequenceAt(0), mappedGroup.getSequences().get(1));
+     assertEquals(0, mappedGroup.getStartRes());
+     assertEquals(1, mappedGroup.getEndRes()); // two columns
    }
 +
 +  // new for 2.12
 +  @Test(groups = "Functional")
 +  public void testAddRange()
 +  {
 +    int[] range = { 1, 5 };
 +    List<int[]> ranges = new ArrayList<>();
 +  
 +    // add to empty list:
 +    MappingUtils.addRange(range, ranges);
 +    assertEquals(1, ranges.size());
 +    assertSame(range, ranges.get(0));
 +  
 +    // extend contiguous (same position):
 +    MappingUtils.addRange(new int[] { 5, 10 }, ranges);
 +    assertEquals(1, ranges.size());
 +    assertEquals(1, ranges.get(0)[0]);
 +    assertEquals(10, ranges.get(0)[1]);
 +  
 +    // extend contiguous (next position):
 +    MappingUtils.addRange(new int[] { 11, 15 }, ranges);
 +    assertEquals(1, ranges.size());
 +    assertEquals(1, ranges.get(0)[0]);
 +    assertEquals(15, ranges.get(0)[1]);
 +  
 +    // change direction: range is not merged:
 +    MappingUtils.addRange(new int[] { 16, 10 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(10, ranges.get(1)[1]);
 +  
 +    // extend reverse contiguous (same position):
 +    MappingUtils.addRange(new int[] { 10, 8 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(8, ranges.get(1)[1]);
 +  
 +    // extend reverse contiguous (next position):
 +    MappingUtils.addRange(new int[] { 7, 6 }, ranges);
 +    assertEquals(2, ranges.size());
 +    assertEquals(16, ranges.get(1)[0]);
 +    assertEquals(6, ranges.get(1)[1]);
 +  
 +    // change direction: range is not merged:
 +    MappingUtils.addRange(new int[] { 6, 9 }, ranges);
 +    assertEquals(3, ranges.size());
 +    assertEquals(6, ranges.get(2)[0]);
 +    assertEquals(9, ranges.get(2)[1]);
 +  
 +    // not contiguous: not merged
 +    MappingUtils.addRange(new int[] { 11, 12 }, ranges);
 +    assertEquals(4, ranges.size());
 +    assertEquals(11, ranges.get(3)[0]);
 +    assertEquals(12, ranges.get(3)[1]);
 +  }
  }
@@@ -140,19 -139,7 +140,20 @@@ public class PlatformTes
    {
      assertNull(Platform.escapeBackslashes(null));
      assertEquals(Platform.escapeBackslashes("hello world"), "hello world");
-     assertEquals(Platform.escapeBackslashes("hello\\world"), "hello\\\\world");
+     assertEquals(Platform.escapeBackslashes("hello\\world"),
+             "hello\\\\world");
    }
 +
 +  @Test(groups = { "Functional" })
 +  public void getLeadingIntegerFromString()
 +  {
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("1234abcd", -1),
 +            1234);
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("1234", -1), 1234);
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("abcd", -1), -1);
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("abcd1234", -1),
 +            -1);
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("None", -1), -1);
 +    Assert.assertEquals(Platform.getLeadingIntegerValue("Null", -1), -1);
 +  }
  }
@@@ -79,7 -78,7 +81,6 @@@ public class Jws2ParamVie
    public static void setUpBeforeClass() throws Exception
    {
      Cache.loadProperties("test/jalview/io/testProps.jvprops");
-     Cache.initLogger();
 -    Console.initLogger();
      disc = JalviewJabawsTestUtils.getJabawsDiscoverer();
    }
  
    public void testJws2Gui()
    {
      Iterator<String> presetEnum = presetTests.iterator();
 -    for (Jws2Instance service : disc.getServices())
 +    for (ServiceWithParameters _service : disc.getServices())
      {
 -      if (serviceTests.size() == 0 || serviceTests
 -              .contains(service.serviceType.toLowerCase(Locale.ROOT)))
 +      // This will fail for non-jabaws services
 +      Jws2Instance service = (Jws2Instance) _service;
 +      if (serviceTests.size() == 0
-               || serviceTests.contains(service.serviceType.toLowerCase(Locale.ROOT)))
++        || serviceTests.contains(service.getName().toLowerCase(Locale.ROOT)))
        {
          List<Preset> prl = null;
          Preset pr = null;
@@@ -85,11 -85,11 +86,12 @@@ public class DisorderAnnotExportImpor
        Thread.sleep(100);
      }
  
 -    iupreds = new ArrayList<Jws2Instance>();
 -    for (Jws2Instance svc : disc.getServices())
 +    SlivkaWSDiscoverer disc2 = SlivkaWSDiscoverer.getInstance();
 +    disc2.startDiscoverer();
 +    while (disc2.isRunning())
      {
-       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT).contains("iupredws"))
+       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT)
+               .contains("iupredws"))
        {
          iupreds.add(svc);
        }
@@@ -98,12 -98,13 +99,13 @@@ public class RNAStructExportImpor
        Thread.sleep(100);
      }
  
 -    for (Jws2Instance svc : disc.getServices())
 +    for (ServiceWithParameters svc : disc.getServices())
      {
  
-       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT).contains("rnaalifoldws"))
+       if (svc.getServiceTypeURI().toLowerCase(Locale.ROOT)
+               .contains("rnaalifoldws"))
        {
 -        rnaalifoldws = svc;
 +        rnaalifoldws = (Jws2Instance) svc;
        }
      }
  
@@@ -26,9 -27,8 +26,10 @@@ import static org.testng.AssertJUnit.as
  import static org.testng.AssertJUnit.assertTrue;
  
  import jalview.bin.Cache;
+ import jalview.bin.Console;
  import jalview.gui.JvOptionPane;
 +import jalview.ws.api.ServiceWithParameters;
 +import jalview.ws.api.UIinfo;
  import jalview.ws.jabaws.JalviewJabawsTestUtils;
  import jalview.ws.jws2.jabaws2.Jws2Instance;
  
@@@ -132,10 -131,10 +133,10 @@@ public class ParameterUtilsTes
     * @param service
     * @return
     */
 -  public boolean isForTesting(Jws2Instance service)
 +  public boolean isForTesting(UIinfo service)
    {
-     return serviceTests.size() == 0
-             || serviceTests.contains(service.serviceType.toLowerCase(Locale.ROOT));
+     return serviceTests.size() == 0 || serviceTests
+             .contains(service.serviceType.toLowerCase(Locale.ROOT));
    }
  
    @Test(groups = { "Network" })
@@@ -23,6 -23,17 +23,7 @@@ package jalview.ws.sifts
  import static org.testng.Assert.assertEquals;
  import static org.testng.Assert.assertTrue;
  
 -import jalview.api.DBRefEntryI;
 -import jalview.bin.Cache;
 -import jalview.datamodel.DBRefEntry;
 -import jalview.datamodel.DBRefSource;
 -import jalview.datamodel.Sequence;
 -import jalview.datamodel.SequenceI;
 -import jalview.gui.JvOptionPane;
 -import jalview.io.DataSourceType;
 -import jalview.structure.StructureMapping;
 -import jalview.xml.binding.sifts.Entry.Entity;
  import java.io.File;
  import java.io.IOException;
  import java.util.ArrayList;
Simple merge