Merge branch 'develop' into spike/JAL-1950_hmmer3client
authorJim Procter <jprocter@issues.jalview.org>
Thu, 31 May 2018 12:41:36 +0000 (13:41 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Thu, 31 May 2018 12:41:36 +0000 (13:41 +0100)
430 files changed:
.ant-targets-build.xml
.checkstyle
.classpath
.gitignore
.settings/org.eclipse.jdt.core.prefs
README
RELEASE
THIRDPARTYLIBS
benchmarking/.classpath [new file with mode: 0644]
benchmarking/.gitignore [new file with mode: 0644]
benchmarking/.settings/org.eclipse.core.resources.prefs [new file with mode: 0644]
benchmarking/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
benchmarking/.settings/org.eclipse.m2e.core.prefs [new file with mode: 0644]
benchmarking/README [new file with mode: 0644]
benchmarking/pom.xml [new file with mode: 0644]
benchmarking/src/main/java/org/jalview/HiddenColsIteratorsBenchmark.java [new file with mode: 0644]
benchmarking/src/main/java/org/jalview/HiddenColumnsBenchmark.java [new file with mode: 0644]
benchmarking/src/main/java/org/jalview/SeqWidthBenchmark.java [new file with mode: 0644]
build.xml
examples/exampleFeatures.txt
examples/groovy/PIDmatrix.groovy [new file with mode: 0644]
examples/testdata/4IM2_missing.pdb [new file with mode: 0644]
examples/testdata/4IM2_missing_noid.pdb [new file with mode: 0644]
examples/testdata/4IM2_nterm.pdb [new file with mode: 0644]
examples/testdata/test.aln
help/help.jhm
help/helpTOC.xml
help/html/calculations/pairwise.html
help/html/calculations/sorting.html
help/html/features/chimera.html
help/html/features/jmol.html
help/html/features/pdbseqfetcher.png
help/html/features/pdbsequencefetcher.html
help/html/features/preferences.html
help/html/features/schooser_enter-id.png
help/html/features/schooser_main.png
help/html/features/splitView.html
help/html/features/structurechooser.html
help/html/features/uniprotseqfetcher.png
help/html/features/uniprotsequencefetcher.html
help/html/features/viewingpdbs.html
help/html/menus/desktopMenu.html
help/html/releases.html
help/html/whatsNew.html
lib/VAqua4.jar [new file with mode: 0644]
lib/groovy-all-2.4.12-indy.jar [moved from lib/groovy-all-2.4.6-indy.jar with 62% similarity]
lib/htsjdk-2.12.0.jar [new file with mode: 0644]
resources/images/idwidth.gif [deleted file]
resources/lang/Messages.properties
resources/lang/Messages_es.properties
resources/uniprot_mapping.xml
schemas/JalviewUserColours.xsd
schemas/jalview.xsd
src/MCview/AppletPDBCanvas.java
src/MCview/PDBCanvas.java
src/MCview/PDBChain.java
src/ext/edu/ucsf/rbvi/strucviz2/ChimeraResidue.java
src/ext/edu/ucsf/rbvi/strucviz2/StructureManager.java
src/jalview/analysis/AlignSeq.java
src/jalview/analysis/AlignmentUtils.java
src/jalview/analysis/Dna.java
src/jalview/api/AlignViewportI.java
src/jalview/api/AlignmentViewPanel.java
src/jalview/api/FeatureColourI.java
src/jalview/api/FeatureRenderer.java
src/jalview/api/structures/JalviewStructureDisplayI.java
src/jalview/appletgui/APopupMenu.java
src/jalview/appletgui/AlignFrame.java
src/jalview/appletgui/AlignViewport.java
src/jalview/appletgui/AlignmentPanel.java
src/jalview/appletgui/AnnotationColourChooser.java
src/jalview/appletgui/AnnotationColumnChooser.java
src/jalview/appletgui/AnnotationLabels.java
src/jalview/appletgui/AnnotationPanel.java
src/jalview/appletgui/AnnotationRowFilter.java
src/jalview/appletgui/AppletJmol.java
src/jalview/appletgui/AppletJmolBinding.java
src/jalview/appletgui/ExtJmol.java
src/jalview/appletgui/FeatureColourChooser.java
src/jalview/appletgui/FeatureRenderer.java
src/jalview/appletgui/FeatureSettings.java
src/jalview/appletgui/FontChooser.java
src/jalview/appletgui/IdCanvas.java
src/jalview/appletgui/IdPanel.java
src/jalview/appletgui/IdwidthAdjuster.java
src/jalview/appletgui/OverviewCanvas.java
src/jalview/appletgui/OverviewPanel.java
src/jalview/appletgui/PaintRefresher.java
src/jalview/appletgui/RedundancyPanel.java
src/jalview/appletgui/ScalePanel.java
src/jalview/appletgui/SeqCanvas.java
src/jalview/appletgui/SeqPanel.java
src/jalview/appletgui/SliderPanel.java
src/jalview/appletgui/SplitFrame.java
src/jalview/appletgui/UserDefinedColours.java
src/jalview/bin/Cache.java
src/jalview/bin/Jalview.java
src/jalview/bin/JalviewLite.java
src/jalview/binding/Colour.java
src/jalview/binding/CompoundMatcher.java [new file with mode: 0644]
src/jalview/binding/FeatureMatcher.java [new file with mode: 0644]
src/jalview/binding/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/binding/Filter.java [new file with mode: 0644]
src/jalview/binding/JalviewUserColours.java
src/jalview/binding/MatchCondition.java [new file with mode: 0644]
src/jalview/binding/MatcherSet.java [new file with mode: 0644]
src/jalview/binding/types/ColourThreshTypeType.java [new file with mode: 0644]
src/jalview/binding/types/FeatureMatcherByType.java [new file with mode: 0644]
src/jalview/binding/types/NoValueColour.java [new file with mode: 0644]
src/jalview/controller/AlignViewController.java
src/jalview/datamodel/Alignment.java
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/AlignmentI.java
src/jalview/datamodel/CigarArray.java
src/jalview/datamodel/ContiguousI.java
src/jalview/datamodel/DBRefEntry.java
src/jalview/datamodel/DBRefSource.java
src/jalview/datamodel/GeneLociI.java [new file with mode: 0644]
src/jalview/datamodel/HiddenColumns.java
src/jalview/datamodel/HiddenColumnsCursor.java [new file with mode: 0644]
src/jalview/datamodel/HiddenCursorPosition.java [new file with mode: 0644]
src/jalview/datamodel/Mapping.java
src/jalview/datamodel/Range.java
src/jalview/datamodel/RangeElementsIterator.java [new file with mode: 0644]
src/jalview/datamodel/RangeIterator.java [new file with mode: 0644]
src/jalview/datamodel/SearchResults.java
src/jalview/datamodel/Sequence.java
src/jalview/datamodel/SequenceCursor.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/datamodel/SequenceGroup.java
src/jalview/datamodel/SequenceI.java
src/jalview/datamodel/StartRegionIterator.java [new file with mode: 0644]
src/jalview/datamodel/VisibleColsCollection.java
src/jalview/datamodel/VisibleColsIterator.java [deleted file]
src/jalview/datamodel/VisibleContigsIterator.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureAttributeType.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureAttributes.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureLocationI.java
src/jalview/datamodel/features/FeatureMatcher.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureMatcherSetI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSource.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSourceI.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureSources.java [new file with mode: 0644]
src/jalview/datamodel/features/FeatureStore.java
src/jalview/datamodel/features/NCList.java
src/jalview/datamodel/features/NCNode.java
src/jalview/datamodel/features/RangeComparator.java
src/jalview/datamodel/features/SequenceFeatures.java
src/jalview/datamodel/features/SequenceFeaturesI.java
src/jalview/datamodel/xdb/uniprot/UniprotFeature.java
src/jalview/ext/ensembl/EnsemblCdna.java
src/jalview/ext/ensembl/EnsemblData.java [new file with mode: 0644]
src/jalview/ext/ensembl/EnsemblFeatures.java
src/jalview/ext/ensembl/EnsemblGene.java
src/jalview/ext/ensembl/EnsemblGenome.java
src/jalview/ext/ensembl/EnsemblGenomes.java
src/jalview/ext/ensembl/EnsemblInfo.java
src/jalview/ext/ensembl/EnsemblLookup.java
src/jalview/ext/ensembl/EnsemblMap.java [new file with mode: 0644]
src/jalview/ext/ensembl/EnsemblProtein.java
src/jalview/ext/ensembl/EnsemblRestClient.java
src/jalview/ext/ensembl/EnsemblSeqProxy.java
src/jalview/ext/ensembl/EnsemblSequenceFetcher.java
src/jalview/ext/ensembl/EnsemblSymbol.java
src/jalview/ext/ensembl/EnsemblXref.java
src/jalview/ext/ensembl/Species.java
src/jalview/ext/htsjdk/HtsContigDb.java
src/jalview/ext/htsjdk/VCFReader.java [new file with mode: 0644]
src/jalview/ext/jmol/JalviewJmolBinding.java
src/jalview/ext/jmol/JmolCommands.java
src/jalview/ext/jmol/JmolParser.java
src/jalview/ext/rbvi/chimera/AtomSpecModel.java
src/jalview/fts/api/GFTSPanelI.java
src/jalview/fts/core/FTSRestClient.java
src/jalview/fts/core/GFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSPanel.java
src/jalview/fts/service/pdb/PDBFTSRestClient.java
src/jalview/fts/service/uniprot/UniProtFTSRestClient.java
src/jalview/fts/service/uniprot/UniprotFTSPanel.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/AlignViewport.java
src/jalview/gui/AlignmentPanel.java
src/jalview/gui/AnnotationChooser.java
src/jalview/gui/AnnotationColourChooser.java
src/jalview/gui/AnnotationColumnChooser.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/AquaInternalFrameManager.java [new file with mode: 0644]
src/jalview/gui/CalculationChooser.java
src/jalview/gui/ChimeraViewFrame.java
src/jalview/gui/CrossRefAction.java
src/jalview/gui/CutAndPasteHtmlTransfer.java
src/jalview/gui/Desktop.java
src/jalview/gui/FeatureColourChooser.java [deleted file]
src/jalview/gui/FeatureRenderer.java
src/jalview/gui/FeatureSettings.java
src/jalview/gui/FeatureTypeSettings.java [new file with mode: 0644]
src/jalview/gui/FontChooser.java
src/jalview/gui/IProgressIndicator.java
src/jalview/gui/IdCanvas.java
src/jalview/gui/IdPanel.java
src/jalview/gui/IdwidthAdjuster.java
src/jalview/gui/JDatabaseTree.java
src/jalview/gui/Jalview2XML.java
src/jalview/gui/JalviewDialog.java
src/jalview/gui/JvSwingUtils.java
src/jalview/gui/OverviewCanvas.java
src/jalview/gui/OverviewPanel.java
src/jalview/gui/PCAPanel.java
src/jalview/gui/PaintRefresher.java
src/jalview/gui/PairwiseAlignPanel.java
src/jalview/gui/PopupMenu.java
src/jalview/gui/Preferences.java
src/jalview/gui/ProgressBar.java
src/jalview/gui/PromptUserConfig.java
src/jalview/gui/RedundancyPanel.java
src/jalview/gui/RotatableCanvas.java
src/jalview/gui/ScalePanel.java
src/jalview/gui/SeqCanvas.java
src/jalview/gui/SeqPanel.java
src/jalview/gui/SequenceFetcher.java
src/jalview/gui/SequenceRenderer.java
src/jalview/gui/SliderPanel.java
src/jalview/gui/SplitFrame.java
src/jalview/gui/StructureChooser.java
src/jalview/gui/StructureViewer.java
src/jalview/gui/StructureViewerBase.java
src/jalview/gui/TextColourChooser.java
src/jalview/gui/TreePanel.java
src/jalview/gui/UserDefinedColours.java
src/jalview/gui/VamsasApplication.java
src/jalview/gui/ViewSelectionMenu.java
src/jalview/io/AlignFile.java
src/jalview/io/AnnotationFile.java
src/jalview/io/ClustalFile.java
src/jalview/io/FeaturesFile.java
src/jalview/io/FileLoader.java
src/jalview/io/FileParse.java
src/jalview/io/FormatAdapter.java
src/jalview/io/InputStreamParser.java
src/jalview/io/SequenceAnnotationReport.java
src/jalview/io/WSWUBlastClient.java
src/jalview/io/cache/JvCacheableInputBox.java
src/jalview/io/gff/Gff3Helper.java
src/jalview/io/gff/SequenceOntologyI.java
src/jalview/io/gff/SequenceOntologyLite.java
src/jalview/io/vcf/VCFLoader.java [new file with mode: 0644]
src/jalview/javascript/JSFunctionExec.java
src/jalview/javascript/MouseOverStructureListener.java
src/jalview/jbgui/GAlignFrame.java
src/jalview/jbgui/GCutAndPasteHtmlTransfer.java
src/jalview/jbgui/GPreferences.java
src/jalview/jbgui/GStructureChooser.java
src/jalview/renderer/AnnotationRenderer.java
src/jalview/renderer/OverviewRenderer.java
src/jalview/renderer/ScaleRenderer.java
src/jalview/renderer/seqfeatures/FeatureRenderer.java
src/jalview/schemabinding/version2/.castor.cdr
src/jalview/schemabinding/version2/Colour.java
src/jalview/schemabinding/version2/CompoundMatcher.java [new file with mode: 0644]
src/jalview/schemabinding/version2/FeatureMatcher.java [new file with mode: 0644]
src/jalview/schemabinding/version2/FeatureMatcherSet.java [new file with mode: 0644]
src/jalview/schemabinding/version2/Filter.java [new file with mode: 0644]
src/jalview/schemabinding/version2/JalviewUserColours.java
src/jalview/schemabinding/version2/MatchCondition.java [new file with mode: 0644]
src/jalview/schemabinding/version2/MatcherSet.java [new file with mode: 0644]
src/jalview/schemabinding/version2/OtherData.java
src/jalview/schemabinding/version2/Setting.java
src/jalview/schemabinding/version2/descriptors/ColourDescriptor.java
src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/JalviewUserColoursDescriptor.java
src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/descriptors/OtherDataDescriptor.java
src/jalview/schemabinding/version2/descriptors/SettingDescriptor.java
src/jalview/schemabinding/version2/types/.castor.cdr [new file with mode: 0644]
src/jalview/schemabinding/version2/types/ColourThreshTypeType.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/FeatureMatcherByType.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/NoValueColour.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java [new file with mode: 0644]
src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java [new file with mode: 0644]
src/jalview/schemes/FeatureColour.java
src/jalview/schemes/RNAHelicesColourChooser.java
src/jalview/schemes/ResidueProperties.java
src/jalview/structure/StructureMapping.java
src/jalview/structure/StructureSelectionManager.java
src/jalview/urls/CustomUrlProvider.java
src/jalview/util/ColorUtils.java
src/jalview/util/IntRangeComparator.java
src/jalview/util/MapList.java
src/jalview/util/MappingUtils.java
src/jalview/util/MathUtils.java [new file with mode: 0644]
src/jalview/util/Platform.java
src/jalview/util/StringUtils.java
src/jalview/util/UrlConstants.java
src/jalview/util/matcher/Condition.java [new file with mode: 0644]
src/jalview/util/matcher/Matcher.java [new file with mode: 0644]
src/jalview/util/matcher/MatcherI.java [new file with mode: 0644]
src/jalview/viewmodel/AlignmentViewport.java
src/jalview/viewmodel/OverviewDimensions.java
src/jalview/viewmodel/OverviewDimensionsHideHidden.java
src/jalview/viewmodel/OverviewDimensionsShowHidden.java
src/jalview/viewmodel/ViewportRanges.java
src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java
src/jalview/viewmodel/seqfeatures/FeatureRendererSettings.java
src/jalview/workers/AnnotationWorker.java
src/jalview/workers/ColumnCounterSetWorker.java
src/jalview/workers/ConsensusThread.java
src/jalview/workers/ConservationThread.java
src/jalview/workers/StrucConsensusThread.java
src/jalview/ws/DBRefFetcher.java
src/jalview/ws/DasSequenceFeatureFetcher.java
src/jalview/ws/dbsources/Pfam.java
src/jalview/ws/dbsources/PfamFull.java
src/jalview/ws/dbsources/PfamSeed.java
src/jalview/ws/dbsources/Rfam.java
src/jalview/ws/dbsources/RfamFull.java
src/jalview/ws/dbsources/RfamSeed.java
src/jalview/ws/dbsources/Uniprot.java
src/jalview/ws/dbsources/Xfam.java
src/jalview/ws/ebi/EBIFetchClient.java
src/jalview/ws/jws1/JPredThread.java
src/jalview/ws/jws2/AADisorderClient.java
src/jalview/ws/jws2/AbstractJabaCalcWorker.java
src/jalview/ws/jws2/jabaws2/Jws2Instance.java
src/jalview/ws/sifts/SiftsClient.java
src/org/stackoverflowusers/file/WindowsShortcut.java [new file with mode: 0644]
test/MCview/PDBChainTest.java
test/jalview/analysis/AlignmentGenerator.java
test/jalview/analysis/AlignmentUtilsTests.java
test/jalview/analysis/CrossRefTest.java
test/jalview/analysis/DnaTest.java
test/jalview/analysis/TestAlignSeq.java
test/jalview/controller/AlignViewControllerTest.java
test/jalview/datamodel/AlignmentAnnotationTests.java
test/jalview/datamodel/AlignmentTest.java
test/jalview/datamodel/CigarArrayTest.java [new file with mode: 0644]
test/jalview/datamodel/ColumnSelectionTest.java
test/jalview/datamodel/HiddenColumnsCursorTest.java [new file with mode: 0644]
test/jalview/datamodel/HiddenColumnsTest.java
test/jalview/datamodel/RangeElementsIteratorTest.java [moved from test/jalview/datamodel/VisibleColsIteratorTest.java with 84% similarity]
test/jalview/datamodel/SequenceFeatureTest.java
test/jalview/datamodel/SequenceTest.java
test/jalview/datamodel/StartRegionIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/VisibleContigsIteratorTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureAttributesTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureMatcherSetTest.java [new file with mode: 0644]
test/jalview/datamodel/features/FeatureMatcherTest.java [new file with mode: 0644]
test/jalview/datamodel/features/SequenceFeaturesTest.java
test/jalview/ext/ensembl/EnsemblCdnaTest.java
test/jalview/ext/ensembl/EnsemblGeneTest.java
test/jalview/ext/ensembl/EnsemblRestClientTest.java
test/jalview/ext/ensembl/EnsemblSeqProxyTest.java
test/jalview/ext/ensembl/SpeciesTest.java [new file with mode: 0644]
test/jalview/ext/htsjdk/TestHtsContigDb.java
test/jalview/ext/htsjdk/VCFReaderTest.java [new file with mode: 0644]
test/jalview/ext/jmol/JmolCommandsTest.java
test/jalview/ext/jmol/JmolViewerTest.java
test/jalview/ext/rbvi/chimera/AtomSpecModelTest.java
test/jalview/ext/so/SequenceOntologyTest.java
test/jalview/gui/AlignFrameTest.java
test/jalview/gui/AlignViewportTest.java
test/jalview/gui/AlignmentPanelTest.java
test/jalview/gui/AnnotationColumnChooserTest.java [new file with mode: 0644]
test/jalview/gui/FeatureSettingsTest.java [new file with mode: 0644]
test/jalview/gui/FreeUpMemoryTest.java [new file with mode: 0644]
test/jalview/gui/PairwiseAlignmentPanelTest.java [new file with mode: 0644]
test/jalview/gui/PopupMenuTest.java
test/jalview/gui/ProgressBarTest.java
test/jalview/gui/SeqCanvasTest.java [new file with mode: 0644]
test/jalview/gui/StructureChooserTest.java
test/jalview/gui/StructureViewerTest.java
test/jalview/io/AnnotatedPDBFileInputTest.java
test/jalview/io/ClustalFileTest.java [new file with mode: 0644]
test/jalview/io/CrossRef2xmlTests.java
test/jalview/io/FeaturesFileTest.java
test/jalview/io/JSONFileTest.java
test/jalview/io/Jalview2xmlBase.java
test/jalview/io/Jalview2xmlTests.java
test/jalview/io/SequenceAnnotationReportTest.java
test/jalview/io/gff/SequenceOntologyLiteTest.java [new file with mode: 0644]
test/jalview/io/vcf/VCFLoaderTest.java [new file with mode: 0644]
test/jalview/io/vcf/contigs.fasta [new file with mode: 0644]
test/jalview/io/vcf/contigs.fasta.fai [new file with mode: 0644]
test/jalview/io/vcf/testVcf.dat [new file with mode: 0644]
test/jalview/io/vcf/testVcf.vcf [new file with mode: 0644]
test/jalview/io/vcf/testVcf2.vcf [new file with mode: 0644]
test/jalview/renderer/OverviewRendererTest.java [new file with mode: 0644]
test/jalview/renderer/ScaleRendererTest.java
test/jalview/renderer/seqfeatures/FeatureColourFinderTest.java
test/jalview/renderer/seqfeatures/FeatureRendererTest.java
test/jalview/schemes/Blosum62ColourSchemeTest.java
test/jalview/schemes/FeatureColourTest.java
test/jalview/structure/Mapping.java
test/jalview/structure/StructureMappingTest.java
test/jalview/structure/StructureSelectionManagerTest.java
test/jalview/structures/models/AAStructureBindingModelTest.java
test/jalview/util/MapListTest.java
test/jalview/util/MappingUtilsTest.java
test/jalview/util/MathUtilsTest.java [new file with mode: 0644]
test/jalview/util/StringUtilsTest.java
test/jalview/util/matcher/ConditionTest.java [new file with mode: 0644]
test/jalview/util/matcher/MatcherTest.java [new file with mode: 0644]
test/jalview/viewmodel/ViewportRangesTest.java
test/jalview/ws/PDBSequenceFetcherTest.java
test/jalview/ws/dbsources/PfamFullTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/PfamSeedTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/RfamFullTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/RfamSeedTest.java [new file with mode: 0644]
test/jalview/ws/dbsources/UniprotTest.java
test/jalview/ws/dbsources/XfamFetcherTest.java
test/jalview/ws/ebi/EBIFetchClientTest.java
test/jalview/ws/sifts/SiftsClientTest.java
utils/InstallAnywhere/Jalview.iap_xml
utils/InstallAnywhere/jalview_buildinstaller.xml
utils/MessageBundleChecker.java
utils/checkstyle/checkstyle-suppress.xml
utils/checkstyle/import-control.xml
utils/proguard.jar [deleted file]
utils/proguard_5.3.3.jar [new file with mode: 0755]

index 15432a1..7ef21f1 100644 (file)
@@ -1,31 +1,2 @@
-build
-buildPropertiesFile
-buildTests
-buildextclients
-buildindices
-castorbinding
-clean
-compileApplet
-distclean
 help
-init
-linkcheck
-makeApplet
-makedist
-makefulldist
-obfuscate
-packageApplet
-prepare
-prepareTests
-preparejnlp
-prepubapplet_1
-pubapplet
-runenv
-signApplet
-sourcedist
-sourcedoc
-sourcescrub
-testclean
-testng
 usage
-writejnlpf
index 0329bb7..a87fa04 100644 (file)
@@ -8,5 +8,4 @@
     <file-match-pattern match-pattern="src/.*.java" include-pattern="true"/>
     <file-match-pattern match-pattern="resources/.*.properties" include-pattern="true"/>
   </fileset>
-  <filter name="NonSrcDirs" enabled="false"/>
 </fileset-config>
index c4a2832..0cdc4b9 100644 (file)
        <classpathentry kind="lib" path="lib/VARNAv3-93.jar"/>
        <classpathentry kind="lib" path="lib/jfreesvg-2.1.jar"/>
        <classpathentry kind="lib" path="lib/quaqua-filechooser-only-8.0.jar"/>
-       <classpathentry kind="lib" path="lib/htsjdk-1.133.jar"/>
+       <classpathentry kind="lib" path="lib/VAqua4.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/plugin"/>
        <classpathentry kind="lib" path="lib/xml-apis.jar"/>
        <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
-       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
        <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/Plugin.jar"/>
        <classpathentry kind="lib" path="lib/jersey-client-1.19.jar"/>
        <classpathentry kind="lib" path="lib/jersey-core-1.19.jar"/>
@@ -68,6 +67,8 @@
        <classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
        <classpathentry kind="lib" path="lib/biojava-core-4.1.0.jar"/>
        <classpathentry kind="lib" path="lib/biojava-ontology-4.1.0.jar"/>
-       <classpathentry kind="lib" path="lib/groovy-all-2.4.6-indy.jar"/>
+       <classpathentry kind="lib" path="lib/htsjdk-2.12.0.jar"/>
+       <classpathentry kind="lib" path="lib/groovy-all-2.4.12-indy.jar"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
        <classpathentry kind="output" path="classes"/>
 </classpath>
index bfd5da9..2a55560 100644 (file)
@@ -11,3 +11,5 @@
 .gitattributes
 TESTNG
 /jalviewApplet.jar
+/benchmarking/lib
+*.class
\ No newline at end of file
index 8a5e7a7..5908bb2 100644 (file)
@@ -1,15 +1,15 @@
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.compliance=1.8
 org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
-org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.jdt.core.compiler.source=1.8
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=52
diff --git a/README b/README
index cbc93b1..eaf226b 100755 (executable)
--- a/README
+++ b/README
@@ -25,7 +25,10 @@ To run application:
 
 java -Djava.ext.dirs=JALVIEW_HOME/lib -cp JALVIEW_HOME/jalview.jar jalview.bin.Jalview
 
-Replace JALVIEW_HOME with the full path to Jalview Installation Directory.
+Replace JALVIEW_HOME with the full path to Jalview Installation Directory. If building from source:
+
+java -Djava.ext.dirs=JALVIEW_BUILD/dist -cp JALVIEW_BUILD/dist/jalview.jar jalview.bin.Jalview
+
 
 ##################
 
diff --git a/RELEASE b/RELEASE
index e6b0cf8..c61d86c 100644 (file)
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-jalview.release=releases/Release_2_10_2b1_Branch
-jalview.version=2.10.2b1
+jalview.release=releases/Release_2_10_4_Branch
+jalview.version=2.10.4b1
index e0be904..d0d9125 100644 (file)
@@ -9,6 +9,7 @@ ext.edu.ucsf.rbvi.strucviz2 includes sources originally developed by Scooter Mor
  
  jalview.ext.android includes code taken from the Android Open Source Project (https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util).
  The Apache 2.0 Licence (http://www.apache.org/licenses/LICENSE-2.0) is acknowledged in the source code.
+ org.stackoverflowusers.file.WindowsShortcuts was downloaded from http://github.com/codebling/WindowsShortcuts via https://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java 
 
 Licensing information for each library is given below:
 
@@ -49,6 +50,8 @@ jfreesvg-2.1.jar : GPL v3 licensed library from the JFree suite: http://www.jfre
 
 quaqua: v.8.0 (latest stable) by Randel S Hofer. LGPL and BSD Modified license: downloaded from http://www.randelshofer.ch/quaqua/ 
 
+vaqua: v4 (latest stable) by Alan Snyder et al. GPLv2 with Classpathe xception, also includes contributions from Quaqua: ownloaded from http://violetlib.org/vaqua/overview.html
+
 lib/htsjdk-1.120-SNAPSHOT.jar: (currently not required for 2.10) built from maven master at https://github.com/samtools/htsjdk MIT License to Broad Institute
 
 lib/biojava-core-4.1.0.jar LGPLv2.1 - latest license at https://github.com/biojava/biojava/blob/master/LICENSE
diff --git a/benchmarking/.classpath b/benchmarking/.classpath
new file mode 100644 (file)
index 0000000..131ff24
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" output="target/classes" path="src/main/java">
+               <attributes>
+                       <attribute name="optional" value="true"/>
+                       <attribute name="maven.pomderived" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="src" output="target/test-classes" path="src/test/java">
+               <attributes>
+                       <attribute name="optional" value="true"/>
+                       <attribute name="maven.pomderived" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+               <attributes>
+                       <attribute name="maven.pomderived" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
+               <attributes>
+                       <attribute name="maven.pomderived" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry combineaccessrules="false" kind="src" path="/Jalview Release 2.7"/>
+       <classpathentry kind="lib" path="/Jalview Release 2.7/jalviewApplet.jar"/>
+       <classpathentry kind="output" path="target/classes"/>
+</classpath>
diff --git a/benchmarking/.gitignore b/benchmarking/.gitignore
new file mode 100644 (file)
index 0000000..f4da43d
--- /dev/null
@@ -0,0 +1,6 @@
+/target/
+/bin
+/results
+*.log
+*.json
+*~
diff --git a/benchmarking/.settings/org.eclipse.core.resources.prefs b/benchmarking/.settings/org.eclipse.core.resources.prefs
new file mode 100644 (file)
index 0000000..e9441bb
--- /dev/null
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/benchmarking/.settings/org.eclipse.jdt.core.prefs b/benchmarking/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..714351a
--- /dev/null
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/benchmarking/.settings/org.eclipse.m2e.core.prefs b/benchmarking/.settings/org.eclipse.m2e.core.prefs
new file mode 100644 (file)
index 0000000..f897a7f
--- /dev/null
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/benchmarking/README b/benchmarking/README
new file mode 100644 (file)
index 0000000..d1ec146
--- /dev/null
@@ -0,0 +1,44 @@
+To set up benchmarking:
+
+You will need to install Maven: https://maven.apache.org/install.html
+
+1. Run the makedist target of build.xml in Eclipse, or in the jalview directory run 
+  ant makedist
+
+This builds a jalview.jar file and puts it into dist/
+
+
+2. Make a lib directory in benchmarking/ if not already present and cd into this directory.
+
+
+3. Purge any previous maven dependencies:
+   mvn dependency:purge-local-repository -DactTransitively=false -DreResolve=false
+
+
+4. Run
+  mvn install:install-file -Dfile=../dist/jalview.jar -DgroupId=jalview.org -DartifactId=jalview -Dversion=1.0 -Dpackaging=jar -DlocalRepositoryPath=lib
+to install the jalview.jar file in the local maven repository. The pom.xml in the benchmarking references this installation, so if you change the names the pom.xml file will also need to be updated.
+
+
+5. Build and run jmh benchmarking. In the benchmarking directory:
+  mvn clean install
+  java -jar target/benchmarks.jar
+  
+  To get JSON output instead use:
+  java -jar target/benchmarks.jar -rf json
+  
+  To run a specific benchmark file use:
+  java -jar target/benchmarks.jar <Benchmark class name> 
+  
+  JSON output can be viewed quickly by drag-dropping on http://jmh.morethan.io/
+  
+  To get help use the standard -h option:
+       java -jar target/benchmarks.jar -h
+       
+  More information here:
+       http://openjdk.java.net/projects/code-tools/jmh/
+       http://java-performance.info/jmh/
+  
+  
+ 6. If you make changes to the Jalview code everything will need to be refreshed, by performing steps 3-5 again.
+
diff --git a/benchmarking/pom.xml b/benchmarking/pom.xml
new file mode 100644 (file)
index 0000000..3bced18
--- /dev/null
@@ -0,0 +1,183 @@
+<!--
+Copyright (c) 2014, Oracle America, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+ * Neither the name of Oracle nor the names of its contributors may be used
+   to endorse or promote products derived from this software without
+   specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.jalview</groupId>
+    <artifactId>benchmarking</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>JMH benchmark sample: Java</name>
+
+    <!--
+       This is the demo/sample template build script for building Java benchmarks with JMH.
+       Edit as needed.
+    -->
+
+       <repositories>
+               <repository>
+               <id>lib</id>
+               <url>file://${project.basedir}/lib</url>
+               </repository>
+       </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.openjdk.jmh</groupId>
+            <artifactId>jmh-core</artifactId>
+            <version>${jmh.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.openjdk.jmh</groupId>
+            <artifactId>jmh-generator-annprocess</artifactId>
+            <version>${jmh.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+               <groupId>jalview.org</groupId>
+               <artifactId>jalview</artifactId>
+               <version>1.0</version>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+        <!--
+            JMH version to use with this project.
+          -->
+        <jmh.version>1.19</jmh.version>
+
+        <!--
+            Java source/target to use for compilation.
+          -->
+        <javac.target>1.8</javac.target>
+
+        <!--
+            Name of the benchmark Uber-JAR to generate.
+          -->
+        <uberjar.name>benchmarks</uberjar.name>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <compilerVersion>${javac.target}</compilerVersion>
+                    <source>${javac.target}</source>
+                    <target>${javac.target}</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.2</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <finalName>${uberjar.name}</finalName>
+                            <transformers>
+                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>org.openjdk.jmh.Main</mainClass>
+                                </transformer>
+                            </transformers>
+                            <filters>
+                                <filter>
+                                    <!--
+                                        Shading signed JARs will fail without this.
+                                        http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
+                                    -->
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <artifactId>maven-clean-plugin</artifactId>
+                    <version>2.5</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-deploy-plugin</artifactId>
+                    <version>2.8.1</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-install-plugin</artifactId>
+                    <version>2.5.1</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-jar-plugin</artifactId>
+                    <version>2.4</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-javadoc-plugin</artifactId>
+                    <version>2.9.1</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-resources-plugin</artifactId>
+                    <version>2.6</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-site-plugin</artifactId>
+                    <version>3.3</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-source-plugin</artifactId>
+                    <version>2.2.1</version>
+                </plugin>
+                <plugin>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>2.17</version>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+    </build>
+
+</project>
diff --git a/benchmarking/src/main/java/org/jalview/HiddenColsIteratorsBenchmark.java b/benchmarking/src/main/java/org/jalview/HiddenColsIteratorsBenchmark.java
new file mode 100644 (file)
index 0000000..477bfad
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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 org.jalview;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Param;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+
+/*
+ * A class to benchmark hidden columns performance
+ */
+@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(1)
+public class HiddenColsIteratorsBenchmark {
+       /*
+        * State with multiple hidden columns and a start position set
+        */
+       @State(Scope.Thread)
+       public static class HiddenColsAndStartState
+       {
+               @Param({"300", "10000", "100000"})
+               public int maxcols;
+               
+               @Param({"1", "50", "90"})
+               public int startpcnt; // position as percentage of maxcols
+               
+               @Param({"1","15","100"})
+               public int hide;
+               
+               HiddenColumns h = new HiddenColumns();
+               Random rand = new Random();
+               
+               public int hiddenColumn;
+               public int visibleColumn;
+       
+               @Setup
+               public void setup()
+               {
+                       rand.setSeed(1234);
+                       int lastcol = 0;
+               while (lastcol < maxcols)
+               {
+                       int count = rand.nextInt(100); 
+                       lastcol += count;
+                       h.hideColumns(lastcol, lastcol+hide);
+                       lastcol+=hide;
+               }
+               
+               // make sure column at start is hidden
+               hiddenColumn = (int)(maxcols * startpcnt/100.0);
+               h.hideColumns(hiddenColumn, hiddenColumn);
+               
+               // and column <hide> after start is visible
+               ColumnSelection sel = new ColumnSelection();
+               h.revealHiddenColumns(hiddenColumn+hide, sel);
+               visibleColumn = hiddenColumn+hide;
+               
+               System.out.println("Maxcols: " + maxcols + " HiddenCol: " + hiddenColumn + " Hide: " + hide);
+               System.out.println("Number of hidden columns: " + h.getSize());
+               }
+       }
+       
+       /* Convention: functions in alphabetical order */
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+       public int benchStartIterator(HiddenColsAndStartState tstate)
+       {
+               int res = 0;
+               int startx = tstate.visibleColumn;
+               Iterator<Integer> it = tstate.h.getStartRegionIterator(startx,
+                               startx+60);
+        while (it.hasNext())
+        {
+          res = it.next() - startx;
+          Blackhole.consumeCPU(5);
+        }
+        return res;
+       }
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+       public int benchBoundedIterator(HiddenColsAndStartState tstate)
+       {
+               int startx = tstate.visibleColumn;
+               int blockStart = startx;
+               int blockEnd;
+               int screenY = 0;
+               Iterator<int[]> it = tstate.h.getBoundedIterator(startx,
+                               startx+60);
+        while (it.hasNext())
+        {
+               int[] region = it.next();
+          
+               blockEnd = Math.min(region[0] - 1, blockStart + 60 - screenY);
+             
+               screenY += blockEnd - blockStart + 1;
+               blockStart = region[1] + 1;
+               
+               Blackhole.consumeCPU(5);
+        }
+        return blockStart;
+       }
+}
diff --git a/benchmarking/src/main/java/org/jalview/HiddenColumnsBenchmark.java b/benchmarking/src/main/java/org/jalview/HiddenColumnsBenchmark.java
new file mode 100644 (file)
index 0000000..07b76f9
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * 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 org.jalview;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Param;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import jalview.datamodel.ColumnSelection;
+import jalview.datamodel.HiddenColumns;
+
+/*
+ * A class to benchmark hidden columns performance
+ */
+@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(1)
+public class HiddenColumnsBenchmark 
+{      
+       /*
+        * State with multiple hidden columns and a start position set
+        */
+       @State(Scope.Thread)
+       public static class HiddenColsAndStartState
+       {
+               @Param({"100", "1000", "10000", "100000", "1000000"})
+               public int maxcols;
+               
+               @Param({"1", "50", "90"})
+               public int startpcnt; // position as percentage of maxcols
+               
+               @Param({"1","15","100"})
+               public int hide;
+               
+               HiddenColumns h = new HiddenColumns();
+               Random rand = new Random();
+               
+               public int hiddenColumn;
+               public int visibleColumn;
+       
+               @Setup
+               public void setup()
+               {
+                       rand.setSeed(1234);
+                       int lastcol = 0;
+               while (lastcol < maxcols)
+               {
+                       int count = rand.nextInt(100); 
+                       lastcol += count;
+                       h.hideColumns(lastcol, lastcol+hide);
+                       lastcol+=hide;
+               }
+               
+               // make sure column at start is hidden
+               hiddenColumn = (int)(maxcols * startpcnt/100.0);
+               h.hideColumns(hiddenColumn, hiddenColumn);
+               
+               // and column <hide> after start is visible
+               ColumnSelection sel = new ColumnSelection();
+               h.revealHiddenColumns(hiddenColumn+hide, sel);
+               visibleColumn = hiddenColumn+hide;
+               
+               System.out.println("Maxcols: " + maxcols + " HiddenCol: " + hiddenColumn + " Hide: " + hide);
+               System.out.println("Number of hidden columns: " + h.getSize());
+               }
+       }
+       
+       /* Convention: functions in alphabetical order */
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+       public int benchAdjustForHiddenColumns(HiddenColsAndStartState tstate)
+       {
+               return tstate.h.visibleToAbsoluteColumn(tstate.visibleColumn);
+       }
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+       public int benchFindColumnPosition(HiddenColsAndStartState tstate)
+       {
+               return tstate.h.absoluteToVisibleColumn(tstate.visibleColumn);
+       }
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+    public int benchGetSize(HiddenColsAndStartState tstate)
+    {
+               return tstate.h.getSize();
+    }
+
+   @Benchmark
+    @BenchmarkMode({Mode.Throughput})
+    public HiddenColumns benchHideCols(HiddenColsAndStartState tstate) 
+    {
+       tstate.h.hideColumns(tstate.visibleColumn,
+                       tstate.visibleColumn+2000); 
+       return tstate.h;
+    }
+   
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+    public boolean benchIsVisible(HiddenColsAndStartState tstate) 
+    {
+       return tstate.h.isVisible(tstate.hiddenColumn); 
+    }
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+    public HiddenColumns benchReveal(HiddenColsAndStartState tstate) 
+    {
+               ColumnSelection sel = new ColumnSelection();
+       tstate.h.revealHiddenColumns(tstate.hiddenColumn, sel);
+       return tstate.h;
+    }
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+    public HiddenColumns benchRevealAll(HiddenColsAndStartState tstate) 
+    {
+               ColumnSelection sel = new ColumnSelection();
+       tstate.h.revealAllHiddenColumns(sel);
+       return tstate.h;
+    }
+       
+       
+}
\ No newline at end of file
diff --git a/benchmarking/src/main/java/org/jalview/SeqWidthBenchmark.java b/benchmarking/src/main/java/org/jalview/SeqWidthBenchmark.java
new file mode 100644 (file)
index 0000000..a92d4f0
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 org.jalview;
+
+import org.jalview.HiddenColumnsBenchmark.HiddenColsAndStartState;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Param;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+/*
+ * A class to benchmark hidden columns performance
+ */
+@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
+@Fork(1)
+public class SeqWidthBenchmark {
+
+       /*
+        * State with multiple hidden columns and a start position set
+        */
+       @State(Scope.Thread)
+       public static class AlignmentState
+       {
+               @Param({"100", "1000", "10000", "100000"})
+               public int numSeqs;
+               
+               Random rand = new Random();
+               
+               AlignmentI al;
+               
+               @Setup
+               public void setup()
+               {
+                       rand.setSeed(1234);
+                       
+                       SequenceI[] seqs = new Sequence[numSeqs];
+                   for (int i = 0; i < numSeqs; i++)
+                   {
+                     int count = rand.nextInt(10000); 
+                     StringBuilder aas = new StringBuilder();
+                     for (int j=0; j<count; j++)
+                     {
+                        aas.append("a");
+                     }
+
+                     seqs[i] = new Sequence("Sequence" + i, aas.toString());
+                   }
+                       al = new Alignment(seqs);
+               }
+       }
+       
+       
+       @Benchmark
+       @BenchmarkMode({Mode.Throughput})
+       public int benchSeqGetWidth(AlignmentState tstate)
+       {
+               return tstate.al.getWidth();
+       }
+       
+}
index eb30ef0..d826d83 100755 (executable)
--- a/build.xml
+++ b/build.xml
@@ -17,7 +17,9 @@
  * 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.
 -->
-<project name="jalviewX" default="usage" basedir=".">
+<project name="jalviewX" default="usage" basedir="."
+ xmlns:if="ant:if"
+    xmlns:unless="ant:unless">
   <target name="help" depends="usage" />
   <target name="usage" depends="init">
     <echo message="~~~Jalview Ant build.xml Usage~~~~" />
@@ -36,7 +38,7 @@
     <echo message="testng - run jalview's tests via testNG. Default group is '${testng-groups}'" />
     <echo message="      you can specify particular test groups as a list via -Dtestng-groups=" />
     <echo message="See docs/building.html and the comments in build file for other targets." />
-    <echo message="note: compile and makeApplet require the property java118.home to be set to point to a java 1.1.8 jdk." />
+    <echo message="note: compile and makeApplet optionally compile/obfuscate applet against a different Java version by specifying -Djava118.home=PathtoJDK/lib which is the lib directory in the JDK install that contains rt.jar " />
     <echo message="Useful -D flags: -Ddonotobfuscate will prevent applet obfuscation" />
   </target>
 
 
     <!-- default TestNG groups to run -->
     <property name="testng-groups" value="Functional" />
+    <!-- Java 9 JVM args -->
+    <condition property="java9">
+      <equals arg1="${ant.java.version}" arg2="9"/>
+    </condition>
 
     <!-- Don't change anything below here unless you know what you are doing! -->
     <!-- Url path for WebStart in JNLP file -->
     <!-- Anne's version needs 1.7 - should rebuild VARNA to java 1.6 for release -->
     <property name="j2sev" value="1.7+" />
     <!-- Java Compilation settings - source and target javac version -->
-    <property name="javac.source" value="1.7" />
-    <property name="javac.target" value="1.7" />
+    <property name="javac.source" value="1.8" />
+    <property name="javac.target" value="1.8" />
 
     <!-- Permissions for running Java applets and applications. -->
     <!-- Defaults are those suitable for deploying jalview webstart www.jalview.org -->
     </path>
     <property name="source.dist.name" value="${basedir}/jalview-src.tar.gz" />
     <!-- The Location of the java 1.1.8 jdk -->
-    <!--<property name="java118.home" value="C:\Sun\jdk1.1.8" /> -->
     <property name="java118.home" value="${java.home}" />
-    <!-- <property name="applet.jre.tools" value="${java118.home}/lib/classes.zip" />
-               -->
     <!-- jre for 1.4 version -->
-    <property name="applet.jre.tools" value="${java.home}/lib/rt.jar" />
+    <property name="applet.jre.tools" value="${java118.home}/lib/rt.jar" />
 
     <!-- the classpath for building the 1.1 applet -->
     <path id="jalviewlite.deps">
       <fileset dir="${java118.home}">
-        <include name="lib/classes.zip" />
+        <include name="lib/rt.jar" />
       </fileset>
       <fileset dir="${java.home}/lib">
         <include name="plugin.jar" />
         <pathelement location="${testOutputDir}" />
         <path refid="test.classpath" />
       </classpath>
+      <jvmarg value="--add-modules=java.se.ee" if:set="java9"/>
+      <jvmarg value="--illegal-access=warn" if:set="java9"/>
       <classfileset dir="${testOutputDir}" includes="**/*.class" />
     </testng>
   </target>
 
   <target name="buildindices" depends="init, prepare" unless="help.uptodate">
+    <replace value="${JALVIEW_VERSION}">
+      <replacetoken><![CDATA[$$Version-Rel$$]]></replacetoken>
+      <fileset dir="${outputDir}/${helpDir}">
+        <include name="help.jhm" />
+      </fileset>
+    </replace>
+
     <java classname="com.sun.java.help.search.Indexer" classpathref="build.classpath" fork="true" dir="${outputDir}/${helpDir}">
       <arg line="html" />
     </java>
           <offline_allowed />
         </information>
         <resources>
-          <j2se version="9+" />
+          <j2se version="1.8+" />
           <jar main="true" href="jalview.jar"/>
           <fileset dir="${packageDir}">
             <exclude name="jalview.jar" />
             <include name="*.jar" />
             <include name="*_*.jar" />
-            <exclude name="*jnilib.jar" />
+            <exclude name="*quaqua*.jar" />
           </fileset>
           <property name="jalview.version" value="${JALVIEW_VERSION}" />
         </resources>
         <resources os="Mac OS X">
           <fileset dir="${packageDir}">
-            <include name="*quaqua*.jnilib.jar" />
+                <include name="quaqua-filechooser-only-8.0.jar"/>
+            <include name="*quaqua64*.jnilib.jar" />
           </fileset>
         </resources>
 
     </presetdef>
 
     <jnlpf toFile="${jnlpFile}" />
-    <!-- add a j2se entry for java 9 -->
-    <replace file="${jnlpFile}" value="j2se version=&quot;1.7+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee&quot;">
-          <replacetoken>j2se version="1.9+"</replacetoken>
-           
-        </replace>
+    <!-- add the add-modules j2se attribute for java 9 -->
+    <replace file="${jnlpFile}" value="j2se version=&quot;1.8+&quot; initial-heap-size=&quot;${inih}&quot; max-heap-size=&quot;${maxh}&quot; java-vm-args=&quot;--add-modules=java.se.ee --illegal-access=warn&quot;">
+          <replacetoken>j2se version="1.8+"</replacetoken>
+    </replace>
   </target>
 
   <target name="-dofakejnlpfileassoc" depends="-generatejnlpf" if="nojnlpfileassocs">
 </target>
 
 <target name="packageApplet" depends="compileApplet, buildPropertiesFile">
-  <copy file="${resourceDir}/images/idwidth.gif" toFile="${outputDir}/images/idwidth.gif" />
   <copy file="${resourceDir}/images/link.gif" toFile="${outputDir}/images/link.gif" />
   <copy todir="${outputDir}/lang">
     <fileset dir="${resourceDir}/lang">
       <include name="MCview/**" />
       <include name="jalview/**" />
       <include name=".build_properties" />
-      <include name="images/idwidth.gif" />
       <include name="images/link.gif" />
       <include name="lang/**" />
     </fileset>
       <include name="plugin.jar" />
     </fileset>
   </path>
-  <taskdef resource="proguard/ant/task.properties" classpath="utils/proguard.jar" />
+  <taskdef resource="proguard/ant/task.properties" classpath="utils/proguard_5.3.3.jar" />
 
   <proguard verbose="true" >
     <injar file="in.jar" />
index 9e65534..99af214 100755 (executable)
@@ -26,6 +26,11 @@ BETA-TURN-IIL        8b5b50
 ST-MOTIF       ac25a1
 kdHydrophobicity       ccffcc|333300|-3.9|4.5|above|-2.0
 
+STARTFILTERS
+GAMMA-TURN-INVERSE     Label Contains PDB
+kdHydrophobicity       (Score LT 1.5) OR (Score GE 2.8)
+ENDFILTERS
+
 STARTGROUP     uniprot
 <html><a href="http://pfam.xfam.org/family/PF00111">Pfam family</a></html>     FER_CAPAA       -1      0       0       Pfam
 Iron-sulfur (2Fe-2S)   FER_CAPAA       -1      39      39      METAL
diff --git a/examples/groovy/PIDmatrix.groovy b/examples/groovy/PIDmatrix.groovy
new file mode 100644 (file)
index 0000000..b97abcc
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+import jalview.analysis.scoremodels.ScoreModels
+import jalview.analysis.scoremodels.SimilarityParams
+
+// generate matrix for current selection using standard Jalview PID
+
+printSimilarityMatrix(true,true,SimilarityParams.Jalview)
+
+/** 
+ * this function prints a sequence similarity matrix in PHYLIP format. 
+ * printSimilarityMatrix(selected-only, include-ids, pidMethod)
+ * 
+ * Allowed values for pidMethod:
+ * 
+ * Jalview's Comparison.PID method includes matching gaps 
+ * and counts over the length of the shorter gapped sequence
+ * SimilarityParams.Jalview;
+ *
+ * 'SeqSpace' mode PCA calculation does not count matching 
+ * gaps but uses longest gapped sequence length
+ *  SimilarityParams.SeqSpace;
+ *
+ * PID calcs from the Raghava-Barton paper
+ * SimilarityParams.PID1: ignores gap-gap, does not score gap-residue,
+ * includes gap-residue in lengths, matches on longer of two sequences.
+ * 
+ * SimilarityParams.PID2: ignores gap-gap,ignores gap-residue, 
+ * matches on longer of two sequences
+ * 
+ * SimilarityParams.PID3: ignores gap-gap,ignores gap-residue, 
+ * matches on shorter of sequences only
+ * 
+ * SimilarityParams.PID4: ignores gap-gap,does not score gap-residue,
+ * includes gap-residue in lengths,matches on shorter of sequences only.
+ */
+
+void printSimilarityMatrix(boolean selview=false, boolean includeids=true, SimilarityParams pidMethod) {
+
+  def currentAlignFrame = jalview.bin.Jalview.getCurrentAlignFrame()
+
+  jalview.gui.AlignViewport av = currentAlignFrame.getCurrentView()
+
+  jalview.datamodel.AlignmentView seqStrings = av.getAlignmentView(selview)
+
+  if (!selview || av.getSelectionGroup()==null) {
+    start = 0
+    end = av.getAlignment().getWidth()
+    seqs = av.getAlignment().getSequencesArray()
+  } else {
+    start = av.getSelectionGroup().getStartRes()
+    end = av.getSelectionGroup().getEndRes() + 1
+    seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment())
+  }
+
+  distanceCalc = ScoreModels.getInstance().getScoreModel("PID",
+      (jalview.api.AlignmentViewPanel) currentAlignFrame.alignPanel)
+
+  def distance=distanceCalc.findSimilarities(
+      seqStrings.getSequenceStrings(jalview.util.Comparison.GAP_DASH),pidMethod)
+
+  // output the PHYLIP Matrix
+
+  print distance.width()+" "+distance.height()+"\n"
+
+  p = 0
+
+  for (v in 1..distance.height()) {
+
+    if (includeids) {
+      print seqs[p++].getDisplayId(false)+" "
+    }
+
+    for (r in 1..distance.width()) {
+      print distance.getValue(v-1,r-1)+" "
+    }
+
+    print "\n"
+  }
+}
\ No newline at end of file
diff --git a/examples/testdata/4IM2_missing.pdb b/examples/testdata/4IM2_missing.pdb
new file mode 100644 (file)
index 0000000..35f94b2
--- /dev/null
@@ -0,0 +1,525 @@
+HEADER    TRANSFERASE/TRANSFERASE INHIBITOR       01-JAN-13   4IM2              
+TITLE     STRUCTURE OF TANK-BINDING KINASE 1  (Truncated test file)
+ATOM   3510  N   LEU A 468      76.337  90.332  -7.628  1.00168.53           N  
+ANISOU 3510  N   LEU A 468    19760  15191  29082   5189   1248  -1702       N  
+ATOM   3511  CA  LEU A 468      75.576  90.788  -6.476  1.00181.60           C  
+ANISOU 3511  CA  LEU A 468    21073  16927  30998   5158   1421  -2051       C  
+ATOM   3512  C   LEU A 468      76.285  91.971  -5.836  1.00196.57           C  
+ANISOU 3512  C   LEU A 468    23029  18636  33021   5053   1667  -2229       C  
+ATOM   3513  O   LEU A 468      75.727  93.067  -5.733  1.00197.97           O  
+ANISOU 3513  O   LEU A 468    23115  18581  33523   5166   1662  -2304       O  
+ATOM   3514  CB  LEU A 468      75.409  89.671  -5.449  1.00173.72           C  
+ANISOU 3514  CB  LEU A 468    19829  16340  29836   4981   1593  -2304       C  
+ATOM   3515  CG  LEU A 468      74.099  88.882  -5.420  1.00170.58           C  
+ANISOU 3515  CG  LEU A 468    19142  16142  29529   5074   1449  -2345       C  
+ATOM   3516  CD1 LEU A 468      74.023  87.940  -6.581  1.00167.98           C  
+ANISOU 3516  CD1 LEU A 468    18951  15858  29015   5180   1156  -2025       C  
+ATOM   3517  CD2 LEU A 468      73.988  88.096  -4.145  1.00165.16           C  
+ANISOU 3517  CD2 LEU A 468    18217  15818  28720   4865   1706  -2656       C  
+ATOM   3518  N   ASP A 469      77.529  91.753  -5.429  1.00208.80           N  
+ANISOU 3518  N   ASP A 469    24731  20282  34322   4836   1875  -2296       N  
+ATOM   3519  CA  ASP A 469      78.267  92.785  -4.720  1.00223.11           C  
+ANISOU 3519  CA  ASP A 469    26577  21960  36233   4701   2123  -2500       C  
+ATOM   3520  C   ASP A 469      78.653  93.973  -5.599  1.00231.96           C  
+ANISOU 3520  C   ASP A 469    27954  22654  37525   4817   2037  -2283       C  
+ATOM   3521  O   ASP A 469      78.718  95.100  -5.118  1.00235.71           O  
+ANISOU 3521  O   ASP A 469    28377  22940  38242   4798   2175  -2449       O  
+ATOM   3522  CB  ASP A 469      79.506  92.197  -4.057  1.00224.50           C  
+ANISOU 3522  CB  ASP A 469    26841  22370  36089   4433   2350  -2633       C  
+ATOM   3523  CG  ASP A 469      79.748  92.767  -2.679  1.00229.51           C  
+ANISOU 3523  CG  ASP A 469    27303  23107  36794   4254   2637  -3018       C  
+ATOM   3524  OD1 ASP A 469      79.958  93.992  -2.564  1.00232.36           O  
+ANISOU 3524  OD1 ASP A 469    27695  23203  37387   4262   2718  -3093       O  
+ATOM   3525  OD2 ASP A 469      79.751  91.979  -1.709  1.00229.48           O  
+ANISOU 3525  OD2 ASP A 469    27145  23450  36598   4098   2779  -3242       O  
+ATOM   3526  N   PHE A 470      78.904  93.735  -6.882  1.00233.88           N  
+ANISOU 3526  N   PHE A 470    28481  22742  37641   4931   1813  -1912       N  
+ATOM   3527  CA  PHE A 470      79.292  94.831  -7.766  1.00237.73           C  
+ANISOU 3527  CA  PHE A 470    29252  22819  38256   5033   1733  -1677       C  
+ATOM   3528  C   PHE A 470      78.103  95.470  -8.467  1.00232.15           C  
+ANISOU 3528  C   PHE A 470    28512  21868  37828   5322   1470  -1512       C  
+ATOM   3529  O   PHE A 470      78.021  96.691  -8.532  1.00230.69           O  
+ANISOU 3529  O   PHE A 470    28371  21372  37909   5402   1493  -1517       O  
+ATOM   3530  CB  PHE A 470      80.344  94.379  -8.784  1.00242.99           C  
+ANISOU 3530  CB  PHE A 470    30297  23410  38616   4976   1661  -1357       C  
+ATOM   3531  CG  PHE A 470      80.663  95.388  -9.839  1.00251.53           C  
+ANISOU 3531  CG  PHE A 470    31709  24074  39788   5089   1550  -1064       C  
+ATOM   3532  CD1 PHE A 470      81.658  96.315  -9.620  1.00252.90           C  
+ANISOU 3532  CD1 PHE A 470    32021  24036  40032   4942   1771  -1133       C  
+ATOM   3533  CD2 PHE A 470      80.005  95.389 -11.059  1.00255.02           C  
+ANISOU 3533  CD2 PHE A 470    32335  24339  40221   5330   1225   -715       C  
+ATOM   3534  CE1 PHE A 470      81.981  97.242 -10.575  1.00254.75           C  
+ANISOU 3534  CE1 PHE A 470    32569  23879  40344   5028   1688   -862       C  
+ATOM   3535  CE2 PHE A 470      80.321  96.320 -12.027  1.00256.76           C  
+ANISOU 3535  CE2 PHE A 470    32894  24173  40491   5427   1126   -433       C  
+ATOM   3536  CZ  PHE A 470      81.313  97.248 -11.784  1.00256.57           C  
+ANISOU 3536  CZ  PHE A 470    33006  23929  40550   5272   1365   -504       C  
+ATOM   3537  N   CYS A 471      77.190  94.665  -8.997  1.00176.88           N  
+ANISOU 3537  N   CYS A 471    20955  21812  24441   2225  -3821  -3711       N  
+ATOM   3538  CA  CYS A 471      76.124  95.233  -9.815  1.00176.41           C  
+ANISOU 3538  CA  CYS A 471    20820  21906  24301   2124  -3576  -3614       C  
+ATOM   3539  C   CYS A 471      74.868  95.631  -9.050  1.00176.82           C  
+ANISOU 3539  C   CYS A 471    21026  22206  23952   2247  -3588  -3734       C  
+ATOM   3540  O   CYS A 471      74.632  96.811  -8.854  1.00178.66           O  
+ANISOU 3540  O   CYS A 471    21175  22393  24314   2353  -3749  -3873       O  
+ATOM   3541  CB  CYS A 471      75.805  94.346 -11.019  1.00173.95           C  
+ANISOU 3541  CB  CYS A 471    20521  21694  23877   1925  -3217  -3359       C  
+ATOM   3542  SG  CYS A 471      76.954  94.571 -12.398  1.00273.04           S  
+ANISOU 3542  SG  CYS A 471    32850  33934  36957   1863  -3075  -3146       S  
+ATOM   3543  N   ILE A 472      74.068  94.670  -8.603  1.00173.50           N  
+ANISOU 3543  N   ILE A 472    20813  22023  23085   2253  -3402  -3662       N  
+ATOM   3544  CA  ILE A 472      72.771  95.033  -8.022  1.00171.09           C  
+ANISOU 3544  CA  ILE A 472    20616  21936  22453   2374  -3307  -3694       C  
+ATOM   3545  C   ILE A 472      72.832  96.022  -6.874  1.00177.82           C  
+ANISOU 3545  C   ILE A 472    21591  22750  23222   2698  -3611  -3931       C  
+ATOM   3546  O   ILE A 472      71.992  96.914  -6.776  1.00182.92           O  
+ANISOU 3546  O   ILE A 472    22220  23491  23792   2778  -3598  -3985       O  
+ATOM   3547  CB  ILE A 472      71.979  93.843  -7.518  1.00159.41           C  
+ANISOU 3547  CB  ILE A 472    19318  20653  20599   2393  -3038  -3546       C  
+ATOM   3548  CG1 ILE A 472      70.708  93.667  -8.341  1.00147.27           C  
+ANISOU 3548  CG1 ILE A 472    17653  19253  19051   2196  -2745  -3384       C  
+ATOM   3549  CG2 ILE A 472      71.526  94.090  -6.105  1.00155.95           C  
+ANISOU 3549  CG2 ILE A 472    19129  20312  19812   2752  -3085  -3637       C  
+ATOM   3550  CD1 ILE A 472      70.927  92.915  -9.606  1.00135.92           C  
+ANISOU 3550  CD1 ILE A 472    16097  17752  17794   1929  -2643  -3254       C  
+ATOM   3551  N   ARG A 473      73.812  95.865  -5.996  1.00180.33           N  
+ANISOU 3551  N   ARG A 473    22055  22917  23544   2920  -3920  -4092       N  
+ATOM   3552  CA  ARG A 473      73.860  96.762  -4.877  1.00189.65           C  
+ANISOU 3552  CA  ARG A 473    23415  24034  24610   3307  -4283  -4357       C  
+ATOM   3553  C   ARG A 473      74.328  98.021  -5.558  1.00185.98           C  
+ANISOU 3553  C   ARG A 473    22628  23344  24692   3195  -4530  -4477       C  
+ATOM   3554  O   ARG A 473      73.519  98.885  -5.866  1.00181.76           O  
+ANISOU 3554  O   ARG A 473    22002  22904  24153   3165  -4446  -4474       O  
+ATOM   3555  CB  ARG A 473      74.803  96.287  -3.768  1.00205.24           C  
+ANISOU 3555  CB  ARG A 473    25661  25862  26460   3635  -4629  -4545       C  
+ATOM   3556  CG  ARG A 473      74.468  96.921  -2.433  1.00220.50           C  
+ANISOU 3556  CG  ARG A 473    27955  27814  28011   4171  -4923  -4787       C  
+ATOM   3557  CD  ARG A 473      75.337  96.433  -1.295  1.00232.58           C  
+ANISOU 3557  CD  ARG A 473    29837  29192  29341   4591  -5300  -5003       C  
+ATOM   3558  NE  ARG A 473      76.763  96.602  -1.549  1.00237.46           N  
+ANISOU 3558  NE  ARG A 473    30220  29440  30564   4489  -5773  -5205       N  
+ATOM   3559  CZ  ARG A 473      77.716  95.991  -0.861  1.00240.32           C  
+ANISOU 3559  CZ  ARG A 473    30786  29623  30903   4725  -6098  -5371       C  
+ATOM   3560  NH1 ARG A 473      77.388  95.172   0.115  1.00241.12           N  
+ANISOU 3560  NH1 ARG A 473    31370  29900  30344   5100  -5978  -5355       N  
+ATOM   3561  NH2 ARG A 473      78.988  96.195  -1.159  1.00241.54           N  
+ANISOU 3561  NH2 ARG A 473    30651  29398  31724   4605  -6518  -5533       N  
+ATOM   3562  N   ASN A 474      75.608  98.073  -5.901  1.00188.74           N  
+ANISOU 3562  N   ASN A 474    22768  23381  25563   3107  -4772  -4534       N  
+ATOM   3563  CA  ASN A 474      76.168  99.264  -6.526  1.00188.61           C  
+ANISOU 3563  CA  ASN A 474    22395  23075  26194   3026  -4977  -4605       C  
+ATOM   3564  C   ASN A 474      75.420  99.892  -7.729  1.00173.98           C  
+ANISOU 3564  C   ASN A 474    20312  21326  24467   2781  -4623  -4414       C  
+ATOM   3565  O   ASN A 474      75.482 101.107  -7.891  1.00176.08           O  
+ANISOU 3565  O   ASN A 474    20374  21426  25104   2827  -4803  -4518       O  
+ATOM   3566  CB  ASN A 474      77.650  99.066  -6.857  1.00199.45           C  
+ANISOU 3566  CB  ASN A 474    23514  24060  28207   2939  -5170  -4595       C  
+ATOM   3567  CG  ASN A 474      78.504  98.809  -5.625  1.00210.23           C  
+ANISOU 3567  CG  ASN A 474    25062  25219  29597   3243  -5689  -4883       C  
+ATOM   3568  OD1 ASN A 474      78.174  99.240  -4.527  1.00215.26           O  
+ANISOU 3568  OD1 ASN A 474    25969  25891  29928   3596  -6043  -5160       O  
+ATOM   3569  ND2 ASN A 474      79.613  98.103  -5.810  1.00212.07           N  
+ANISOU 3569  ND2 ASN A 474    25176  25222  30179   3148  -5748  -4822       N  
+ATOM   3570  N   ILE A 475      74.722  99.114  -8.562  1.00161.86           N  
+ANISOU 3570  N   ILE A 475    18813  20033  22652   2556  -4165  -4158       N  
+ATOM   3571  CA  ILE A 475      74.022  99.727  -9.709  1.00154.84           C  
+ANISOU 3571  CA  ILE A 475    17755  19216  21861   2390  -3885  -4015       C  
+ATOM   3572  C   ILE A 475      72.785 100.520  -9.312  1.00159.27           C  
+ANISOU 3572  C   ILE A 475    18401  19995  22121   2490  -3891  -4125       C  
+ATOM   3573  O   ILE A 475      72.662 101.687  -9.663  1.00162.60           O  
+ANISOU 3573  O   ILE A 475    18651  20327  22804   2505  -3962  -4186       O  
+ATOM   3574  CB  ILE A 475      73.600  98.727 -10.818  1.00214.93           C  
+ANISOU 3574  CB  ILE A 475    25397  26975  29293   2173  -3475  -3753       C  
+ATOM   3575  CG1 ILE A 475      74.808  98.227 -11.613  1.00218.74           C  
+ANISOU 3575  CG1 ILE A 475    25753  27211  30145   2083  -3390  -3585       C  
+ATOM   3576  CG2 ILE A 475      72.618  99.379 -11.789  1.00210.37           C  
+ANISOU 3576  CG2 ILE A 475    24743  26509  28681   2097  -3256  -3677       C  
+ATOM   3577  CD1 ILE A 475      74.446  97.201 -12.664  1.00218.15           C  
+ANISOU 3577  CD1 ILE A 475    25777  27260  29850   1946  -3056  -3359       C  
+ATOM   3578  N   GLU A 476      71.856  99.893  -8.600  1.00156.45           N  
+ANISOU 3578  N   GLU A 476    18293  19907  21245   2571  -3778  -4120       N  
+ATOM   3579  CA  GLU A 476      70.623 100.594  -8.252  1.00142.72           C  
+ANISOU 3579  CA  GLU A 476    16627  18370  19231   2677  -3720  -4176       C  
+ATOM   3580  C   GLU A 476      70.845 101.560  -7.094  1.00144.41           C  
+ANISOU 3580  C   GLU A 476    16959  18500  19412   3012  -4114  -4440       C  
+ATOM   3581  O   GLU A 476      70.012 102.417  -6.811  1.00142.58           O  
+ANISOU 3581  O   GLU A 476    16768  18380  19024   3141  -4136  -4517       O  
+ATOM   3582  CB  GLU A 476      69.486  99.616  -7.958  1.00128.68           C  
+ANISOU 3582  CB  GLU A 476    15020  16860  17012   2665  -3395  -4018       C  
+ATOM   3583  CG  GLU A 476      69.562  98.905  -6.628  1.00128.73           C  
+ANISOU 3583  CG  GLU A 476    15321  16930  16662   2941  -3433  -4041       C  
+ATOM   3584  CD  GLU A 476      68.465  97.866  -6.484  1.00133.35           C  
+ANISOU 3584  CD  GLU A 476    15992  17719  16955   2900  -3026  -3806       C  
+ATOM   3585  OE1 GLU A 476      68.285  97.061  -7.422  1.00132.30           O  
+ANISOU 3585  OE1 GLU A 476    15710  17598  16962   2603  -2816  -3643       O  
+ATOM   3586  OE2 GLU A 476      67.768  97.860  -5.448  1.00136.37           O  
+ANISOU 3586  OE2 GLU A 476    16588  18223  17003   3196  -2912  -3773       O  
+ATOM   3587  N   LYS A 477      71.990 101.424  -6.439  1.00148.46           N  
+ANISOU 3587  N   LYS A 477    17534  18788  20087   3179  -4467  -4596       N  
+ATOM   3588  CA  LYS A 477      72.399 102.365  -5.405  1.00154.25           C  
+ANISOU 3588  CA  LYS A 477    18378  19348  20882   3544  -4976  -4907       C  
+ATOM   3589  C   LYS A 477      72.634 103.732  -5.994  1.00159.76           C  
+ANISOU 3589  C   LYS A 477    18754  19833  22117   3467  -5183  -5014       C  
+ATOM   3590  O   LYS A 477      72.568 104.742  -5.292  1.00158.01           O  
+ANISOU 3590  O   LYS A 477    18598  19516  21921   3749  -5568  -5266       O  
+ATOM   3591  CB  LYS A 477      73.667 101.883  -4.706  1.00154.03           C  
+ANISOU 3591  CB  LYS A 477    18446  19057  21022   3730  -5372  -5076       C  
+ATOM   3592  CG  LYS A 477      73.367 100.787  -3.750  1.00149.76           C  
+ANISOU 3592  CG  LYS A 477    18324  18717  19861   3975  -5258  -5042       C  
+ATOM   3593  CD  LYS A 477      74.437 100.554  -2.740  1.00143.39           C  
+ANISOU 3593  CD  LYS A 477    17737  17664  19082   4327  -5762  -5306       C  
+ATOM   3594  CE  LYS A 477      73.739 100.087  -1.488  1.00145.72           C  
+ANISOU 3594  CE  LYS A 477    18569  18186  18613   4805  -5697  -5337       C  
+ATOM   3595  NZ  LYS A 477      74.661  99.556  -0.469  1.00149.42           N  
+ANISOU 3595  NZ  LYS A 477    19366  18470  18936   5206  -6108  -5561       N  
+ATOM   3596  N   THR A 478      72.908 103.757  -7.293  1.00166.45           N  
+ANISOU 3596  N   THR A 478    19271  20589  23384   3124  -4918  -4810       N  
+ATOM   3597  CA  THR A 478      73.186 105.012  -7.968  1.00170.99           C  
+ANISOU 3597  CA  THR A 478    19509  20929  24531   3052  -5023  -4844       C  
+ATOM   3598  C   THR A 478      71.905 105.733  -8.391  1.00172.33           C  
+ANISOU 3598  C   THR A 478    19686  21358  24435   2999  -4780  -4796       C  
+ATOM   3599  O   THR A 478      71.021 105.170  -9.046  1.00163.10           O  
+ANISOU 3599  O   THR A 478    18581  20461  22928   2823  -4356  -4594       O  
+ATOM   3600  CB  THR A 478      74.217 104.856  -9.128  1.00146.93           C  
+ANISOU 3600  CB  THR A 478    16115  17589  22125   2815  -4843  -4626       C  
+ATOM   3601  OG1 THR A 478      74.873 106.106  -9.369  1.00147.15           O  
+ANISOU 3601  OG1 THR A 478    15790  17239  22881   2857  -5085  -4705       O  
+ATOM   3602  CG2 THR A 478      73.562 104.391 -10.425  1.00144.32           C  
+ANISOU 3602  CG2 THR A 478    15764  17473  21597   2570  -4282  -4318       C  
+ATOM   3603  N   VAL A 479      71.809 106.979  -7.944  1.00177.73           N  
+ANISOU 3603  N   VAL A 479    20309  21929  25291   3181  -5110  -5014       N  
+ATOM   3604  CA  VAL A 479      70.674 107.838  -8.218  1.00175.65           C  
+ANISOU 3604  CA  VAL A 479    20044  21875  24819   3171  -4959  -5013       C  
+ATOM   3605  C   VAL A 479      71.176 109.077  -8.939  1.00177.67           C  
+ANISOU 3605  C   VAL A 479    19928  21830  25749   3101  -5069  -5035       C  
+ATOM   3606  O   VAL A 479      72.353 109.417  -8.840  1.00179.02           O  
+ANISOU 3606  O   VAL A 479    19865  21600  26555   3150  -5387  -5121       O  
+ATOM   3607  CB  VAL A 479      69.983 108.266  -6.909  1.00168.37           C  
+ANISOU 3607  CB  VAL A 479    19442  21113  23418   3524  -5243  -5249       C  
+ATOM   3608  CG1 VAL A 479      69.008 109.403  -7.169  1.00162.23           C  
+ANISOU 3608  CG1 VAL A 479    18603  20470  22565   3531  -5176  -5285       C  
+ATOM   3609  CG2 VAL A 479      69.288 107.080  -6.256  1.00163.89           C  
+ANISOU 3609  CG2 VAL A 479    19236  20856  22180   3624  -4991  -5142       C  
+ATOM   3610  N   MET A 486      57.931 103.433  -6.150  1.00 92.00           N  
+ANISOU 3610  N   MET A 486    10154  13425  11375   3371  -1523  -3283       N  
+ATOM   3611  CA  MET A 486      56.824 104.104  -5.482  1.00110.61           C  
+ANISOU 3611  CA  MET A 486    12558  15882  13585   3644  -1287  -3161       C  
+ATOM   3612  C   MET A 486      56.182 105.150  -6.389  1.00111.38           C  
+ANISOU 3612  C   MET A 486    12428  16016  13877   3424  -1416  -3308       C  
+ATOM   3613  O   MET A 486      56.000 106.305  -6.002  1.00120.35           O  
+ANISOU 3613  O   MET A 486    13691  17243  14794   3639  -1526  -3430       O  
+ATOM   3614  CB  MET A 486      57.288 104.743  -4.170  1.00120.60           C  
+ANISOU 3614  CB  MET A 486    14253  17221  14347   4174  -1395  -3243       C  
+ATOM   3615  CG  MET A 486      58.405 105.768  -4.316  1.00126.87           C  
+ANISOU 3615  CG  MET A 486    15171  17987  15047   4196  -1944  -3629       C  
+ATOM   3616  SD  MET A 486      58.150 107.169  -3.210  1.00146.78           S  
+ANISOU 3616  SD  MET A 486    18044  20597  17128   4757  -2124  -3782       S  
+ATOM   3617  CE  MET A 486      58.003 106.326  -1.640  1.00102.35           C  
+ANISOU 3617  CE  MET A 486    12883  15002  11003   5391  -1797  -3524       C  
+ATOM   3618  N   GLY A 496      57.154  99.412  -2.065  1.00157.62           N  
+ANISOU 3618  N   GLY A 496    19150  21656  19083   4616     82  -2024       N  
+ATOM   3619  CA  GLY A 496      57.031  99.217  -3.498  1.00158.07           C  
+ANISOU 3619  CA  GLY A 496    18749  21636  19677   3991   -116  -2141       C  
+ATOM   3620  C   GLY A 496      57.664  97.914  -3.942  1.00163.71           C  
+ANISOU 3620  C   GLY A 496    19349  22235  20619   3709   -140  -2093       C  
+ATOM   3621  O   GLY A 496      58.040  97.090  -3.111  1.00167.17           O  
+ANISOU 3621  O   GLY A 496    20009  22652  20858   3970     79  -1915       O  
+ATOM   3622  N   GLU A 497      57.778  97.722  -5.254  1.00166.67           N  
+ANISOU 3622  N   GLU A 497    19413  22530  21383   3222   -403  -2250       N  
+ATOM   3623  CA  GLU A 497      58.431  96.534  -5.792  1.00170.73           C  
+ANISOU 3623  CA  GLU A 497    19840  22933  22098   2955   -485  -2239       C  
+ATOM   3624  C   GLU A 497      59.931  96.621  -5.556  1.00172.64           C  
+ANISOU 3624  C   GLU A 497    20403  23228  21965   3043   -809  -2472       C  
+ATOM   3625  O   GLU A 497      60.589  95.609  -5.311  1.00173.01           O  
+ANISOU 3625  O   GLU A 497    20547  23223  21965   3047   -764  -2395       O  
+ATOM   3626  CB  GLU A 497      58.173  96.394  -7.290  1.00169.62           C  
+ANISOU 3626  CB  GLU A 497    19361  22681  22408   2505   -727  -2374       C  
+ATOM   3627  CG  GLU A 497      56.882  97.012  -7.777  1.00173.11           C  
+ANISOU 3627  CG  GLU A 497    19518  23086  23171   2413   -663  -2350       C  
+ATOM   3628  CD  GLU A 497      56.703  96.840  -9.270  1.00173.61           C  
+ANISOU 3628  CD  GLU A 497    19329  23013  23621   2054   -974  -2531       C  
+ATOM   3629  OE1 GLU A 497      57.597  96.241  -9.908  1.00170.78           O  
+ANISOU 3629  OE1 GLU A 497    19040  22600  23250   1901  -1202  -2647       O  
+ATOM   3630  OE2 GLU A 497      55.672  97.300  -9.805  1.00174.73           O  
+ANISOU 3630  OE2 GLU A 497    19233  23093  24064   1967   -999  -2559       O  
+ATOM   3631  N   ILE A 498      60.466  97.836  -5.652  1.00171.44           N  
+ANISOU 3631  N   ILE A 498    20381  23149  21608   3107  -1147  -2757       N  
+ATOM   3632  CA  ILE A 498      61.883  98.078  -5.403  1.00167.93           C  
+ANISOU 3632  CA  ILE A 498    20186  22695  20926   3210  -1503  -2995       C  
+ATOM   3633  C   ILE A 498      62.251  97.620  -4.004  1.00173.82           C  
+ANISOU 3633  C   ILE A 498    21298  23465  21280   3659  -1379  -2903       C  
+ATOM   3634  O   ILE A 498      63.315  97.034  -3.789  1.00168.12           O  
+ANISOU 3634  O   ILE A 498    20732  22684  20462   3692  -1544  -2980       O  
+ATOM   3635  CB  ILE A 498      62.227  99.564  -5.520  1.00165.53           C  
+ANISOU 3635  CB  ILE A 498    19934  22420  20540   3287  -1855  -3283       C  
+ATOM   3636  CG1 ILE A 498      61.848 100.100  -6.901  1.00171.37           C  
+ANISOU 3636  CG1 ILE A 498    20360  23137  21618   2911  -1947  -3365       C  
+ATOM   3637  CG2 ILE A 498      63.706  99.798  -5.239  1.00153.61           C  
+ANISOU 3637  CG2 ILE A 498    18613  20820  18931   3398  -2256  -3525       C  
+ATOM   3638  CD1 ILE A 498      61.891 101.605  -6.996  1.00174.73           C  
+ANISOU 3638  CD1 ILE A 498    20793  23594  22004   2996  -2195  -3587       C  
+ATOM   3639  N   SER A 499      61.362  97.890  -3.054  1.00187.32           N  
+ANISOU 3639  N   SER A 499    23169  25252  22753   4049  -1074  -2727       N  
+ATOM   3640  CA  SER A 499      61.558  97.444  -1.683  1.00198.47           C  
+ANISOU 3640  CA  SER A 499    24999  26682  23729   4594   -880  -2593       C  
+ATOM   3641  C   SER A 499      61.611  95.926  -1.651  1.00206.95           C  
+ANISOU 3641  C   SER A 499    26003  27687  24943   4481   -548  -2315       C  
+ATOM   3642  O   SER A 499      62.302  95.334  -0.823  1.00211.11           O  
+ANISOU 3642  O   SER A 499    26867  28195  25151   4806   -533  -2293       O  
+ATOM   3643  CB  SER A 499      60.435  97.952  -0.780  1.00200.16           C  
+ANISOU 3643  CB  SER A 499    25385  26976  23690   5064   -498  -2368       C  
+ATOM   3644  OG  SER A 499      60.635  97.541   0.556  1.00203.12           O  
+ANISOU 3644  OG  SER A 499    26244  27359  23574   5697   -286  -2224       O  
+ATOM   3645  N   ASP A 500      60.877  95.300  -2.564  1.00209.43           N  
+ANISOU 3645  N   ASP A 500    25883  27938  25751   4040   -320  -2123       N  
+ATOM   3646  CA  ASP A 500      60.862  93.851  -2.655  1.00212.36           C  
+ANISOU 3646  CA  ASP A 500    26120  28207  26361   3883    -39  -1865       C  
+ATOM   3647  C   ASP A 500      62.097  93.307  -3.366  1.00203.21           C  
+ANISOU 3647  C   ASP A 500    24946  26999  25265   3557   -426  -2093       C  
+ATOM   3648  O   ASP A 500      62.511  92.195  -3.092  1.00204.96           O  
+ANISOU 3648  O   ASP A 500    25238  27165  25472   3573   -286  -1954       O  
+ATOM   3649  CB  ASP A 500      59.580  93.355  -3.335  1.00221.23           C  
+ANISOU 3649  CB  ASP A 500    26765  29217  28075   3573    287  -1593       C  
+ATOM   3650  CG  ASP A 500      58.346  93.583  -2.486  1.00233.55           C  
+ANISOU 3650  CG  ASP A 500    28310  30773  29657   3938    816  -1243       C  
+ATOM   3651  OD1 ASP A 500      58.502  93.807  -1.272  1.00239.09           O  
+ANISOU 3651  OD1 ASP A 500    29432  31558  29852   4492   1029  -1137       O  
+ATOM   3652  OD2 ASP A 500      57.226  93.532  -3.034  1.00237.24           O  
+ANISOU 3652  OD2 ASP A 500    28355  31128  30656   3709   1011  -1072       O  
+ATOM   3653  N   ILE A 501      62.692  94.094  -4.258  1.00192.67           N  
+ANISOU 3653  N   ILE A 501    23525  25673  24008   3292   -875  -2413       N  
+ATOM   3654  CA  ILE A 501      63.864  93.633  -5.007  1.00181.71           C  
+ANISOU 3654  CA  ILE A 501    22108  24215  22719   3006  -1195  -2587       C  
+ATOM   3655  C   ILE A 501      65.165  93.699  -4.196  1.00170.68           C  
+ANISOU 3655  C   ILE A 501    21064  22814  20972   3287  -1450  -2764       C  
+ATOM   3656  O   ILE A 501      65.940  92.735  -4.173  1.00164.70           O  
+ANISOU 3656  O   ILE A 501    20376  21999  20202   3222  -1476  -2736       O  
+ATOM   3657  CB  ILE A 501      64.023  94.385  -6.353  1.00138.03           C  
+ANISOU 3657  CB  ILE A 501    16341  18649  17453   2657  -1507  -2802       C  
+ATOM   3658  CG1 ILE A 501      63.385  93.589  -7.491  1.00130.33           C  
+ANISOU 3658  CG1 ILE A 501    15061  17587  16872   2296  -1411  -2683       C  
+ATOM   3659  CG2 ILE A 501      65.491  94.642  -6.682  1.00137.98           C  
+ANISOU 3659  CG2 ILE A 501    16437  18581  17410   2596  -1883  -3037       C  
+ATOM   3660  CD1 ILE A 501      61.877  93.516  -7.426  1.00127.50           C  
+ANISOU 3660  CD1 ILE A 501    14480  17217  16748   2292  -1111  -2482       C  
+ATOM   3661  N   HIS A 502      65.396  94.829  -3.532  1.00164.00           N  
+ANISOU 3661  N   HIS A 502    20439  22007  19869   3613  -1676  -2964       N  
+ATOM   3662  CA  HIS A 502      66.603  95.022  -2.737  1.00157.69           C  
+ANISOU 3662  CA  HIS A 502    19968  21147  18800   3931  -2029  -3195       C  
+ATOM   3663  C   HIS A 502      66.627  94.018  -1.594  1.00162.64           C  
+ANISOU 3663  C   HIS A 502    20933  21795  19069   4322  -1761  -3012       C  
+ATOM   3664  O   HIS A 502      67.677  93.496  -1.229  1.00164.35           O  
+ANISOU 3664  O   HIS A 502    21348  21935  19163   4433  -1971  -3125       O  
+ATOM   3665  CB  HIS A 502      66.654  96.452  -2.195  1.00150.83           C  
+ANISOU 3665  CB  HIS A 502    19272  20282  17755   4264  -2351  -3452       C  
+ATOM   3666  CG  HIS A 502      67.989  97.117  -2.348  1.00143.78           C  
+ANISOU 3666  CG  HIS A 502    18387  19223  17019   4248  -2929  -3804       C  
+ATOM   3667  ND1 HIS A 502      68.672  97.155  -3.544  1.00137.00           N  
+ANISOU 3667  ND1 HIS A 502    17193  18253  16609   3785  -3096  -3872       N  
+ATOM   3668  CD2 HIS A 502      68.759  97.787  -1.457  1.00140.15           C  
+ANISOU 3668  CD2 HIS A 502    18217  18647  16387   4674  -3387  -4099       C  
+ATOM   3669  CE1 HIS A 502      69.807  97.814  -3.383  1.00131.33           C  
+ANISOU 3669  CE1 HIS A 502    16503  17343  16055   3893  -3583  -4155       C  
+ATOM   3670  NE2 HIS A 502      69.883  98.208  -2.125  1.00130.34           N  
+ANISOU 3670  NE2 HIS A 502    16741  17200  15583   4416  -3815  -4326       N  
+ATOM   3671  N   THR A 503      65.450  93.751  -1.039  1.00163.37           N  
+ANISOU 3671  N   THR A 503    21080  21971  19022   4552  -1268  -2705       N  
+ATOM   3672  CA  THR A 503      65.295  92.771   0.033  1.00163.31           C  
+ANISOU 3672  CA  THR A 503    21384  21972  18696   4974   -872  -2437       C  
+ATOM   3673  C   THR A 503      65.393  91.327  -0.475  1.00151.49           C  
+ANISOU 3673  C   THR A 503    19659  20415  17484   4610   -614  -2206       C  
+ATOM   3674  O   THR A 503      65.955  90.464   0.198  1.00146.11           O  
+ANISOU 3674  O   THR A 503    19247  19705  16563   4853   -520  -2130       O  
+ATOM   3675  CB  THR A 503      63.956  92.972   0.783  1.00170.09           C  
+ANISOU 3675  CB  THR A 503    22342  22899  19387   5379   -334  -2106       C  
+ATOM   3676  OG1 THR A 503      64.026  94.162   1.576  1.00176.61           O  
+ANISOU 3676  OG1 THR A 503    23551  23775  19778   5902   -585  -2325       O  
+ATOM   3677  CG2 THR A 503      63.645  91.790   1.693  1.00171.53           C  
+ANISOU 3677  CG2 THR A 503    22747  23054  19374   5760    235  -1707       C  
+ATOM   3678  N   LYS A 504      64.865  91.071  -1.669  1.00141.41           N  
+ANISOU 3678  N   LYS A 504    17911  19106  16711   4058   -541  -2117       N  
+ATOM   3679  CA  LYS A 504      64.918  89.730  -2.251  1.00138.96           C  
+ANISOU 3679  CA  LYS A 504    17368  18711  16719   3706   -369  -1930       C  
+ATOM   3680  C   LYS A 504      66.335  89.382  -2.646  1.00142.04           C  
+ANISOU 3680  C   LYS A 504    17848  19062  17058   3520   -779  -2178       C  
+ATOM   3681  O   LYS A 504      66.762  88.230  -2.559  1.00134.46           O  
+ANISOU 3681  O   LYS A 504    16927  18053  16108   3463   -666  -2057       O  
+ATOM   3682  CB  LYS A 504      64.010  89.614  -3.477  1.00129.82           C  
+ANISOU 3682  CB  LYS A 504    15721  17488  16116   3223   -303  -1839       C  
+ATOM   3683  CG  LYS A 504      62.585  89.222  -3.164  1.00130.75           C  
+ANISOU 3683  CG  LYS A 504    15616  17544  16518   3307    225  -1458       C  
+ATOM   3684  N   LEU A 505      67.053  90.395  -3.100  1.00138.41           N  
+ANISOU 3684  N   LEU A 505    17394  18602  16593   3427  -1237  -2505       N  
+ATOM   3685  CA  LEU A 505      68.435  90.224  -3.458  1.00120.29           C  
+ANISOU 3685  CA  LEU A 505    15154  16230  14322   3285  -1619  -2726       C  
+ATOM   3686  C   LEU A 505      69.260  89.889  -2.223  1.00124.70           C  
+ANISOU 3686  C   LEU A 505    16126  16770  14484   3724  -1710  -2800       C  
+ATOM   3687  O   LEU A 505      70.166  89.052  -2.259  1.00124.02           O  
+ANISOU 3687  O   LEU A 505    16106  16619  14396   3643  -1808  -2823       O  
+ATOM   3688  CB  LEU A 505      68.966  91.499  -4.066  1.00109.42           C  
+ANISOU 3688  CB  LEU A 505    13673  14807  13094   3171  -2036  -3016       C  
+ATOM   3689  CG  LEU A 505      70.386  91.097  -4.404  1.00128.61           C  
+ANISOU 3689  CG  LEU A 505    16120  17111  15635   3034  -2336  -3157       C  
+ATOM   3690  CD1 LEU A 505      70.509  91.048  -5.920  1.00141.22           C  
+ANISOU 3690  CD1 LEU A 505    17393  18648  17616   2580  -2370  -3131       C  
+ATOM   3691  CD2 LEU A 505      71.441  91.946  -3.655  1.00114.58           C  
+ANISOU 3691  CD2 LEU A 505    14554  15224  13758   3350  -2784  -3462       C  
+ATOM   3692  N   LEU A 506      68.951  90.579  -1.132  1.00130.40           N  
+ANISOU 3692  N   LEU A 506    17159  17539  14847   4233  -1705  -2854       N  
+ATOM   3693  CA  LEU A 506      69.612  90.336   0.139  1.00131.63           C  
+ANISOU 3693  CA  LEU A 506    17798  17665  14549   4786  -1814  -2947       C  
+ATOM   3694  C   LEU A 506      69.501  88.865   0.507  1.00144.65           C  
+ANISOU 3694  C   LEU A 506    19548  19330  16080   4841  -1380  -2638       C  
+ATOM   3695  O   LEU A 506      70.448  88.287   1.024  1.00156.36           O  
+ANISOU 3695  O   LEU A 506    21304  20754  17351   5038  -1550  -2741       O  
+ATOM   3696  CB  LEU A 506      69.008  91.206   1.240  1.00120.17           C  
+ANISOU 3696  CB  LEU A 506    16713  16270  12675   5409  -1772  -2980       C  
+ATOM   3697  N   ARG A 507      68.351  88.260   0.214  1.00146.01           N  
+ANISOU 3697  N   ARG A 507    19470  19552  16453   4658   -839  -2263       N  
+ATOM   3698  CA  ARG A 507      68.159  86.831   0.443  1.00152.26           C  
+ANISOU 3698  CA  ARG A 507    20261  20320  17271   4649   -394  -1930       C  
+ATOM   3699  C   ARG A 507      69.164  86.004  -0.360  1.00145.14           C  
+ANISOU 3699  C   ARG A 507    19199  19352  16597   4200   -653  -2042       C  
+ATOM   3700  O   ARG A 507      69.471  84.873   0.007  1.00142.95           O  
+ANISOU 3700  O   ARG A 507    19044  19045  16224   4273   -450  -1882       O  
+ATOM   3701  CB  ARG A 507      66.724  86.401   0.111  1.00161.16           C  
+ANISOU 3701  CB  ARG A 507    21023  21434  18777   4463    165  -1523       C  
+ATOM   3702  CG  ARG A 507      65.661  86.975   1.046  1.00177.63           C  
+ANISOU 3702  CG  ARG A 507    23283  23565  20645   4978    585  -1290       C  
+ATOM   3703  CD  ARG A 507      64.263  86.468   0.696  1.00187.68           C  
+ANISOU 3703  CD  ARG A 507    24111  24757  22442   4769   1148   -857       C  
+ATOM   3704  NE  ARG A 507      63.229  87.082   1.527  1.00198.88           N  
+ANISOU 3704  NE  ARG A 507    25661  26202  23704   5259   1588   -600       N  
+ATOM   3705  CZ  ARG A 507      61.923  86.883   1.368  1.00205.71           C  
+ANISOU 3705  CZ  ARG A 507    26137  26967  25058   5169   2095   -210       C  
+ATOM   3706  NH1 ARG A 507      61.484  86.084   0.406  1.00207.33           N  
+ANISOU 3706  NH1 ARG A 507    25797  27020  25959   4610   2159    -75       N  
+ATOM   3707  NH2 ARG A 507      61.055  87.485   2.171  1.00208.11           N  
+ANISOU 3707  NH2 ARG A 507    26596  27293  25185   5668   2517     41       N  
+ATOM   3708  N   LEU A 508      69.681  86.579  -1.445  1.00134.19           N  
+ANISOU 3708  N   LEU A 508    17555  17933  15496   3776  -1072  -2294       N  
+ATOM   3709  CA  LEU A 508      70.654  85.888  -2.287  1.00125.06           C  
+ANISOU 3709  CA  LEU A 508    16259  16704  14555   3385  -1304  -2382       C  
+ATOM   3710  C   LEU A 508      72.070  85.983  -1.730  1.00133.75           C  
+ANISOU 3710  C   LEU A 508    17660  17739  15419   3609  -1705  -2655       C  
+ATOM   3711  O   LEU A 508      72.775  84.975  -1.650  1.00132.42           O  
+ANISOU 3711  O   LEU A 508    17581  17530  15202   3562  -1700  -2615       O  
+ATOM   3712  CB  LEU A 508      70.606  86.404  -3.726  1.00112.64           C  
+ANISOU 3712  CB  LEU A 508    14312  15095  13392   2907  -1513  -2482       C  
+ATOM   3713  CG  LEU A 508      69.271  86.186  -4.436  1.00113.17           C  
+ANISOU 3713  CG  LEU A 508    14056  15174  13769   2653  -1215  -2260       C  
+ATOM   3714  CD1 LEU A 508      69.459  86.184  -5.945  1.00106.22           C  
+ANISOU 3714  CD1 LEU A 508    12898  14225  13237   2216  -1432  -2343       C  
+ATOM   3715  CD2 LEU A 508      68.613  84.894  -3.968  1.00117.32           C  
+ANISOU 3715  CD2 LEU A 508    14560  15678  14341   2703   -779  -1932       C  
+ATOM   3716  N   SER A 509      72.491  87.188  -1.355  1.00130.72           N  
+ANISOU 3716  N   SER A 509    17414  17317  14936   3856  -2085  -2944       N  
+ATOM   3717  CA  SER A 509      73.767  87.347  -0.663  1.00136.66           C  
+ANISOU 3717  CA  SER A 509    18457  17948  15519   4156  -2531  -3236       C  
+ATOM   3718  C   SER A 509      73.698  86.583   0.646  1.00143.40           C  
+ANISOU 3718  C   SER A 509    19773  18845  15866   4687  -2319  -3138       C  
+ATOM   3719  O   SER A 509      74.680  85.977   1.082  1.00138.35           O  
+ANISOU 3719  O   SER A 509    19358  18121  15090   4839  -2521  -3256       O  
+ATOM   3720  CB  SER A 509      74.089  88.820  -0.410  1.00130.15           C  
+ANISOU 3720  CB  SER A 509    17685  17028  14738   4382  -3008  -3572       C  
+ATOM   3721  OG  SER A 509      74.991  89.319  -1.384  1.00125.27           O  
+ANISOU 3721  OG  SER A 509    16739  16248  14609   4003  -3376  -3749       O  
+ATOM   3722  N   SER A 510      72.520  86.615   1.263  1.00144.65           N  
+ANISOU 3722  N   SER A 510    20078  19123  15761   4999  -1878  -2899       N  
+ATOM   3723  CA  SER A 510      72.236  85.777   2.417  1.00148.97           C  
+ANISOU 3723  CA  SER A 510    21044  19713  15846   5526  -1487  -2677       C  
+ATOM   3724  C   SER A 510      72.424  84.315   2.046  1.00145.48           C  
+ANISOU 3724  C   SER A 510    20455  19266  15554   5204  -1182  -2426       C  
+ATOM   3725  O   SER A 510      73.001  83.552   2.811  1.00140.49           O  
+ANISOU 3725  O   SER A 510    20177  18606  14598   5539  -1145  -2415       O  
+ATOM   3726  CB  SER A 510      70.807  85.999   2.911  1.00149.05           C  
+ANISOU 3726  CB  SER A 510    21111  19824  15699   5832   -930  -2348       C  
+ATOM   3727  OG  SER A 510      70.346  84.882   3.643  1.00147.27           O  
+ANISOU 3727  OG  SER A 510    21097  19617  15240   6154   -337  -1962       O  
+ATOM   3728  N   SER A 511      71.946  83.946   0.858  1.00139.39           N  
+ANISOU 3728  N   SER A 511    19184  18508  15269   4583  -1004  -2249       N  
+ATOM   3729  CA  SER A 511      72.011  82.573   0.365  1.00129.68           C  
+ANISOU 3729  CA  SER A 511    17763  17254  14255   4236   -745  -2013       C  
+ATOM   3730  C   SER A 511      73.437  82.171   0.006  1.00129.30           C  
+ANISOU 3730  C   SER A 511    17762  17135  14232   4035  -1175  -2258       C  
+ATOM   3731  O   SER A 511      73.792  80.991   0.047  1.00109.17           O  
+ANISOU 3731  O   SER A 511    15254  14565  11659   3957  -1019  -2119       O  
+ATOM   3732  CB  SER A 511      71.108  82.416  -0.862  1.00125.40           C  
+ANISOU 3732  CB  SER A 511    16701  16703  14243   3684   -564  -1828       C  
+ATOM   3733  OG  SER A 511      71.167  81.106  -1.394  1.00137.08           O  
+ANISOU 3733  OG  SER A 511    17990  18125  15969   3359   -392  -1635       O  
+ATOM   3734  N   GLN A 512      74.247  83.164  -0.347  1.00131.12           N  
+ANISOU 3734  N   GLN A 512    17958  17304  14560   3957  -1699  -2603       N  
+ATOM   3735  CA  GLN A 512      75.621  82.941  -0.774  1.00120.52           C  
+ANISOU 3735  CA  GLN A 512    16587  15845  13361   3755  -2110  -2821       C  
+ATOM   3736  C   GLN A 512      76.508  82.796   0.431  1.00123.24           C  
+ANISOU 3736  C   GLN A 512    17385  16120  13322   4264  -2359  -3023       C  
+ATOM   3737  O   GLN A 512      77.569  82.174   0.385  1.00128.94           O  
+ANISOU 3737  O   GLN A 512    18162  16749  14081   4188  -2573  -3124       O  
+ATOM   3738  CB  GLN A 512      76.096  84.150  -1.546  1.00118.44           C  
+ANISOU 3738  CB  GLN A 512    16077  15483  13440   3537  -2531  -3073       C  
+ATOM   3739  CG  GLN A 512      76.798  83.822  -2.807  1.00125.91           C  
+ANISOU 3739  CG  GLN A 512    16711  16343  14787   3048  -2644  -3057       C  
+ATOM   3740  CD  GLN A 512      76.915  85.020  -3.690  1.00137.22           C  
+ANISOU 3740  CD  GLN A 512    17859  17690  16589   2843  -2877  -3186       C  
+ATOM   3741  OE1 GLN A 512      77.456  86.052  -3.281  1.00153.03           O  
+ANISOU 3741  OE1 GLN A 512    19904  19568  18672   3053  -3245  -3442       O  
+ATOM   3742  NE2 GLN A 512      76.353  84.922  -4.897  1.00128.62           N  
+ANISOU 3742  NE2 GLN A 512    16487  16643  15739   2470  -2678  -3017       N  
+ATOM   3743  N   GLY A 513      76.073  83.422   1.510  1.00128.93           N  
+ANISOU 3743  N   GLY A 513    18453  16869  13665   4826  -2362  -3100       N  
+ATOM   3744  CA  GLY A 513      76.767  83.309   2.761  1.00135.31           C  
+ANISOU 3744  CA  GLY A 513    19790  17602  14021   5449  -2610  -3308       C  
+ATOM   3745  C   GLY A 513      76.597  81.908   3.295  1.00133.10           C  
+ANISOU 3745  C   GLY A 513    19748  17398  13425   5620  -2132  -3010       C  
+ATOM   3746  O   GLY A 513      77.464  81.407   3.988  1.00135.48           O  
+ANISOU 3746  O   GLY A 513    20407  17622  13448   5952  -2344  -3163       O  
+ATOM   3747  N   THR A 514      75.478  81.270   2.971  1.00130.00           N  
+ANISOU 3747  N   THR A 514    19139  17128  13128   5402  -1499  -2584       N  
+ATOM   3748  CA  THR A 514      75.252  79.899   3.409  1.00136.05           C  
+ANISOU 3748  CA  THR A 514    20053  17932  13707   5525   -990  -2248       C  
+ATOM   3749  C   THR A 514      76.125  78.980   2.571  1.00136.91           C  
+ANISOU 3749  C   THR A 514    19914  17993  14111   4991  -1151  -2279       C  
+ATOM   3750  O   THR A 514      76.451  77.859   2.971  1.00136.85           O  
+ANISOU 3750  O   THR A 514    20093  17981  13923   5102   -944  -2139       O  
+ATOM   3751  CB  THR A 514      73.784  79.475   3.234  1.00141.83           C  
+ANISOU 3751  CB  THR A 514    20522  18734  14631   5408   -287  -1768       C  
+ATOM   3752  OG1 THR A 514      73.591  78.921   1.927  1.00142.93           O  
+ANISOU 3752  OG1 THR A 514    20113  18858  15336   4676   -233  -1640       O  
+ATOM   3753  CG2 THR A 514      72.861  80.659   3.413  1.00145.09           C  
+ANISOU 3753  CG2 THR A 514    20916  19192  15018   5620   -214  -1761       C  
+ATOM   3754  N   ILE A 515      76.491  79.473   1.394  1.00129.08           N  
+ANISOU 3754  N   ILE A 515    18518  16960  13565   4442  -1494  -2442       N  
+ATOM   3755  CA  ILE A 515      77.327  78.735   0.468  1.00119.39           C  
+ANISOU 3755  CA  ILE A 515    17053  15677  12633   3951  -1657  -2465       C  
+ATOM   3756  C   ILE A 515      78.787  78.796   0.895  1.00127.05           C  
+ANISOU 3756  C   ILE A 515    18275  16524  13475   4143  -2161  -2798       C  
+ATOM   3757  O   ILE A 515      79.482  77.778   0.906  1.00128.44           O  
+ANISOU 3757  O   ILE A 515    18530  16671  13600   4065  -2153  -2760       O  
+ATOM   3758  CB  ILE A 515      77.220  79.308  -0.945  1.00110.42           C  
+ANISOU 3758  CB  ILE A 515    15452  14516  11987   3401  -1815  -2500       C  
+ATOM   3759  CG1 ILE A 515      75.801  79.150  -1.481  1.00109.39           C  
+ANISOU 3759  CG1 ILE A 515    15038  14466  12060   3177  -1388  -2203       C  
+ATOM   3760  CG2 ILE A 515      78.168  78.587  -1.861  1.00112.80           C  
+ANISOU 3760  CG2 ILE A 515    15581  14743  12533   2997  -1979  -2516       C  
+ATOM   3761  CD1 ILE A 515      75.570  79.879  -2.780  1.00110.01           C  
+ANISOU 3761  CD1 ILE A 515    14740  14519  12538   2759  -1561  -2268       C  
+ATOM   3762  N   GLU A 516      79.240  79.996   1.247  1.00122.76           N  
+ANISOU 3762  N   GLU A 516    17837  15878  12927   4400  -2627  -3132       N  
+ATOM   3763  CA  GLU A 516      80.636  80.218   1.589  1.00124.18           C  
+ANISOU 3763  CA  GLU A 516    18172  15860  13149   4571  -3208  -3493       C  
+ATOM   3764  C   GLU A 516      81.099  79.274   2.681  1.00126.09           C  
+ANISOU 3764  C   GLU A 516    18894  16095  12918   5029  -3183  -3522       C  
+ATOM   3765  O   GLU A 516      82.117  78.595   2.539  1.00116.84           O  
+ANISOU 3765  O   GLU A 516    17721  14825  11848   4893  -3381  -3605       O  
+ATOM   3766  CB  GLU A 516      80.863  81.656   2.046  1.00135.21           C  
+ANISOU 3766  CB  GLU A 516    19658  17112  14603   4907  -3722  -3854       C  
+ATOM   3767  CG  GLU A 516      82.315  82.045   1.962  1.00146.41           C  
+ANISOU 3767  CG  GLU A 516    20992  18238  16400   4885  -4381  -4215       C  
+ATOM   3768  CD  GLU A 516      82.865  81.775   0.582  1.00151.28           C  
+ANISOU 3768  CD  GLU A 516    21100  18789  17590   4220  -4329  -4073       C  
+ATOM   3769  OE1 GLU A 516      82.373  82.413  -0.367  1.00146.52           O  
+ANISOU 3769  OE1 GLU A 516    20133  18221  17316   3867  -4198  -3957       O  
+ATOM   3770  OE2 GLU A 516      83.763  80.917   0.441  1.00154.24           O  
+ANISOU 3770  OE2 GLU A 516    21471  19078  18055   4089  -4397  -4065       O  
+END                                                                             
diff --git a/examples/testdata/4IM2_missing_noid.pdb b/examples/testdata/4IM2_missing_noid.pdb
new file mode 100644 (file)
index 0000000..5a5ef94
--- /dev/null
@@ -0,0 +1,524 @@
+TITLE     STRUCTURE OF TANK-BINDING KINASE 1  (Truncated test file)
+ATOM   3510  N   LEU A 468      76.337  90.332  -7.628  1.00168.53           N  
+ANISOU 3510  N   LEU A 468    19760  15191  29082   5189   1248  -1702       N  
+ATOM   3511  CA  LEU A 468      75.576  90.788  -6.476  1.00181.60           C  
+ANISOU 3511  CA  LEU A 468    21073  16927  30998   5158   1421  -2051       C  
+ATOM   3512  C   LEU A 468      76.285  91.971  -5.836  1.00196.57           C  
+ANISOU 3512  C   LEU A 468    23029  18636  33021   5053   1667  -2229       C  
+ATOM   3513  O   LEU A 468      75.727  93.067  -5.733  1.00197.97           O  
+ANISOU 3513  O   LEU A 468    23115  18581  33523   5166   1662  -2304       O  
+ATOM   3514  CB  LEU A 468      75.409  89.671  -5.449  1.00173.72           C  
+ANISOU 3514  CB  LEU A 468    19829  16340  29836   4981   1593  -2304       C  
+ATOM   3515  CG  LEU A 468      74.099  88.882  -5.420  1.00170.58           C  
+ANISOU 3515  CG  LEU A 468    19142  16142  29529   5074   1449  -2345       C  
+ATOM   3516  CD1 LEU A 468      74.023  87.940  -6.581  1.00167.98           C  
+ANISOU 3516  CD1 LEU A 468    18951  15858  29015   5180   1156  -2025       C  
+ATOM   3517  CD2 LEU A 468      73.988  88.096  -4.145  1.00165.16           C  
+ANISOU 3517  CD2 LEU A 468    18217  15818  28720   4865   1706  -2656       C  
+ATOM   3518  N   ASP A 469      77.529  91.753  -5.429  1.00208.80           N  
+ANISOU 3518  N   ASP A 469    24731  20282  34322   4836   1875  -2296       N  
+ATOM   3519  CA  ASP A 469      78.267  92.785  -4.720  1.00223.11           C  
+ANISOU 3519  CA  ASP A 469    26577  21960  36233   4701   2123  -2500       C  
+ATOM   3520  C   ASP A 469      78.653  93.973  -5.599  1.00231.96           C  
+ANISOU 3520  C   ASP A 469    27954  22654  37525   4817   2037  -2283       C  
+ATOM   3521  O   ASP A 469      78.718  95.100  -5.118  1.00235.71           O  
+ANISOU 3521  O   ASP A 469    28377  22940  38242   4798   2175  -2449       O  
+ATOM   3522  CB  ASP A 469      79.506  92.197  -4.057  1.00224.50           C  
+ANISOU 3522  CB  ASP A 469    26841  22370  36089   4433   2350  -2633       C  
+ATOM   3523  CG  ASP A 469      79.748  92.767  -2.679  1.00229.51           C  
+ANISOU 3523  CG  ASP A 469    27303  23107  36794   4254   2637  -3018       C  
+ATOM   3524  OD1 ASP A 469      79.958  93.992  -2.564  1.00232.36           O  
+ANISOU 3524  OD1 ASP A 469    27695  23203  37387   4262   2718  -3093       O  
+ATOM   3525  OD2 ASP A 469      79.751  91.979  -1.709  1.00229.48           O  
+ANISOU 3525  OD2 ASP A 469    27145  23450  36598   4098   2779  -3242       O  
+ATOM   3526  N   PHE A 470      78.904  93.735  -6.882  1.00233.88           N  
+ANISOU 3526  N   PHE A 470    28481  22742  37641   4931   1813  -1912       N  
+ATOM   3527  CA  PHE A 470      79.292  94.831  -7.766  1.00237.73           C  
+ANISOU 3527  CA  PHE A 470    29252  22819  38256   5033   1733  -1677       C  
+ATOM   3528  C   PHE A 470      78.103  95.470  -8.467  1.00232.15           C  
+ANISOU 3528  C   PHE A 470    28512  21868  37828   5322   1470  -1512       C  
+ATOM   3529  O   PHE A 470      78.021  96.691  -8.532  1.00230.69           O  
+ANISOU 3529  O   PHE A 470    28371  21372  37909   5402   1493  -1517       O  
+ATOM   3530  CB  PHE A 470      80.344  94.379  -8.784  1.00242.99           C  
+ANISOU 3530  CB  PHE A 470    30297  23410  38616   4976   1661  -1357       C  
+ATOM   3531  CG  PHE A 470      80.663  95.388  -9.839  1.00251.53           C  
+ANISOU 3531  CG  PHE A 470    31709  24074  39788   5089   1550  -1064       C  
+ATOM   3532  CD1 PHE A 470      81.658  96.315  -9.620  1.00252.90           C  
+ANISOU 3532  CD1 PHE A 470    32021  24036  40032   4942   1771  -1133       C  
+ATOM   3533  CD2 PHE A 470      80.005  95.389 -11.059  1.00255.02           C  
+ANISOU 3533  CD2 PHE A 470    32335  24339  40221   5330   1225   -715       C  
+ATOM   3534  CE1 PHE A 470      81.981  97.242 -10.575  1.00254.75           C  
+ANISOU 3534  CE1 PHE A 470    32569  23879  40344   5028   1688   -862       C  
+ATOM   3535  CE2 PHE A 470      80.321  96.320 -12.027  1.00256.76           C  
+ANISOU 3535  CE2 PHE A 470    32894  24173  40491   5427   1126   -433       C  
+ATOM   3536  CZ  PHE A 470      81.313  97.248 -11.784  1.00256.57           C  
+ANISOU 3536  CZ  PHE A 470    33006  23929  40550   5272   1365   -504       C  
+ATOM   3537  N   CYS A 471      77.190  94.665  -8.997  1.00176.88           N  
+ANISOU 3537  N   CYS A 471    20955  21812  24441   2225  -3821  -3711       N  
+ATOM   3538  CA  CYS A 471      76.124  95.233  -9.815  1.00176.41           C  
+ANISOU 3538  CA  CYS A 471    20820  21906  24301   2124  -3576  -3614       C  
+ATOM   3539  C   CYS A 471      74.868  95.631  -9.050  1.00176.82           C  
+ANISOU 3539  C   CYS A 471    21026  22206  23952   2247  -3588  -3734       C  
+ATOM   3540  O   CYS A 471      74.632  96.811  -8.854  1.00178.66           O  
+ANISOU 3540  O   CYS A 471    21175  22393  24314   2353  -3749  -3873       O  
+ATOM   3541  CB  CYS A 471      75.805  94.346 -11.019  1.00173.95           C  
+ANISOU 3541  CB  CYS A 471    20521  21694  23877   1925  -3217  -3359       C  
+ATOM   3542  SG  CYS A 471      76.954  94.571 -12.398  1.00273.04           S  
+ANISOU 3542  SG  CYS A 471    32850  33934  36957   1863  -3075  -3146       S  
+ATOM   3543  N   ILE A 472      74.068  94.670  -8.603  1.00173.50           N  
+ANISOU 3543  N   ILE A 472    20813  22023  23085   2253  -3402  -3662       N  
+ATOM   3544  CA  ILE A 472      72.771  95.033  -8.022  1.00171.09           C  
+ANISOU 3544  CA  ILE A 472    20616  21936  22453   2374  -3307  -3694       C  
+ATOM   3545  C   ILE A 472      72.832  96.022  -6.874  1.00177.82           C  
+ANISOU 3545  C   ILE A 472    21591  22750  23222   2698  -3611  -3931       C  
+ATOM   3546  O   ILE A 472      71.992  96.914  -6.776  1.00182.92           O  
+ANISOU 3546  O   ILE A 472    22220  23491  23792   2778  -3598  -3985       O  
+ATOM   3547  CB  ILE A 472      71.979  93.843  -7.518  1.00159.41           C  
+ANISOU 3547  CB  ILE A 472    19318  20653  20599   2393  -3038  -3546       C  
+ATOM   3548  CG1 ILE A 472      70.708  93.667  -8.341  1.00147.27           C  
+ANISOU 3548  CG1 ILE A 472    17653  19253  19051   2196  -2745  -3384       C  
+ATOM   3549  CG2 ILE A 472      71.526  94.090  -6.105  1.00155.95           C  
+ANISOU 3549  CG2 ILE A 472    19129  20312  19812   2752  -3085  -3637       C  
+ATOM   3550  CD1 ILE A 472      70.927  92.915  -9.606  1.00135.92           C  
+ANISOU 3550  CD1 ILE A 472    16097  17752  17794   1929  -2643  -3254       C  
+ATOM   3551  N   ARG A 473      73.812  95.865  -5.996  1.00180.33           N  
+ANISOU 3551  N   ARG A 473    22055  22917  23544   2920  -3920  -4092       N  
+ATOM   3552  CA  ARG A 473      73.860  96.762  -4.877  1.00189.65           C  
+ANISOU 3552  CA  ARG A 473    23415  24034  24610   3307  -4283  -4357       C  
+ATOM   3553  C   ARG A 473      74.328  98.021  -5.558  1.00185.98           C  
+ANISOU 3553  C   ARG A 473    22628  23344  24692   3195  -4530  -4477       C  
+ATOM   3554  O   ARG A 473      73.519  98.885  -5.866  1.00181.76           O  
+ANISOU 3554  O   ARG A 473    22002  22904  24153   3165  -4446  -4474       O  
+ATOM   3555  CB  ARG A 473      74.803  96.287  -3.768  1.00205.24           C  
+ANISOU 3555  CB  ARG A 473    25661  25862  26460   3635  -4629  -4545       C  
+ATOM   3556  CG  ARG A 473      74.468  96.921  -2.433  1.00220.50           C  
+ANISOU 3556  CG  ARG A 473    27955  27814  28011   4171  -4923  -4787       C  
+ATOM   3557  CD  ARG A 473      75.337  96.433  -1.295  1.00232.58           C  
+ANISOU 3557  CD  ARG A 473    29837  29192  29341   4591  -5300  -5003       C  
+ATOM   3558  NE  ARG A 473      76.763  96.602  -1.549  1.00237.46           N  
+ANISOU 3558  NE  ARG A 473    30220  29440  30564   4489  -5773  -5205       N  
+ATOM   3559  CZ  ARG A 473      77.716  95.991  -0.861  1.00240.32           C  
+ANISOU 3559  CZ  ARG A 473    30786  29623  30903   4725  -6098  -5371       C  
+ATOM   3560  NH1 ARG A 473      77.388  95.172   0.115  1.00241.12           N  
+ANISOU 3560  NH1 ARG A 473    31370  29900  30344   5100  -5978  -5355       N  
+ATOM   3561  NH2 ARG A 473      78.988  96.195  -1.159  1.00241.54           N  
+ANISOU 3561  NH2 ARG A 473    30651  29398  31724   4605  -6518  -5533       N  
+ATOM   3562  N   ASN A 474      75.608  98.073  -5.901  1.00188.74           N  
+ANISOU 3562  N   ASN A 474    22768  23381  25563   3107  -4772  -4534       N  
+ATOM   3563  CA  ASN A 474      76.168  99.264  -6.526  1.00188.61           C  
+ANISOU 3563  CA  ASN A 474    22395  23075  26194   3026  -4977  -4605       C  
+ATOM   3564  C   ASN A 474      75.420  99.892  -7.729  1.00173.98           C  
+ANISOU 3564  C   ASN A 474    20312  21326  24467   2781  -4623  -4414       C  
+ATOM   3565  O   ASN A 474      75.482 101.107  -7.891  1.00176.08           O  
+ANISOU 3565  O   ASN A 474    20374  21426  25104   2827  -4803  -4518       O  
+ATOM   3566  CB  ASN A 474      77.650  99.066  -6.857  1.00199.45           C  
+ANISOU 3566  CB  ASN A 474    23514  24060  28207   2939  -5170  -4595       C  
+ATOM   3567  CG  ASN A 474      78.504  98.809  -5.625  1.00210.23           C  
+ANISOU 3567  CG  ASN A 474    25062  25219  29597   3243  -5689  -4883       C  
+ATOM   3568  OD1 ASN A 474      78.174  99.240  -4.527  1.00215.26           O  
+ANISOU 3568  OD1 ASN A 474    25969  25891  29928   3596  -6043  -5160       O  
+ATOM   3569  ND2 ASN A 474      79.613  98.103  -5.810  1.00212.07           N  
+ANISOU 3569  ND2 ASN A 474    25176  25222  30179   3148  -5748  -4822       N  
+ATOM   3570  N   ILE A 475      74.722  99.114  -8.562  1.00161.86           N  
+ANISOU 3570  N   ILE A 475    18813  20033  22652   2556  -4165  -4158       N  
+ATOM   3571  CA  ILE A 475      74.022  99.727  -9.709  1.00154.84           C  
+ANISOU 3571  CA  ILE A 475    17755  19216  21861   2390  -3885  -4015       C  
+ATOM   3572  C   ILE A 475      72.785 100.520  -9.312  1.00159.27           C  
+ANISOU 3572  C   ILE A 475    18401  19995  22121   2490  -3891  -4125       C  
+ATOM   3573  O   ILE A 475      72.662 101.687  -9.663  1.00162.60           O  
+ANISOU 3573  O   ILE A 475    18651  20327  22804   2505  -3962  -4186       O  
+ATOM   3574  CB  ILE A 475      73.600  98.727 -10.818  1.00214.93           C  
+ANISOU 3574  CB  ILE A 475    25397  26975  29293   2173  -3475  -3753       C  
+ATOM   3575  CG1 ILE A 475      74.808  98.227 -11.613  1.00218.74           C  
+ANISOU 3575  CG1 ILE A 475    25753  27211  30145   2083  -3390  -3585       C  
+ATOM   3576  CG2 ILE A 475      72.618  99.379 -11.789  1.00210.37           C  
+ANISOU 3576  CG2 ILE A 475    24743  26509  28681   2097  -3256  -3677       C  
+ATOM   3577  CD1 ILE A 475      74.446  97.201 -12.664  1.00218.15           C  
+ANISOU 3577  CD1 ILE A 475    25777  27260  29850   1946  -3056  -3359       C  
+ATOM   3578  N   GLU A 476      71.856  99.893  -8.600  1.00156.45           N  
+ANISOU 3578  N   GLU A 476    18293  19907  21245   2571  -3778  -4120       N  
+ATOM   3579  CA  GLU A 476      70.623 100.594  -8.252  1.00142.72           C  
+ANISOU 3579  CA  GLU A 476    16627  18370  19231   2677  -3720  -4176       C  
+ATOM   3580  C   GLU A 476      70.845 101.560  -7.094  1.00144.41           C  
+ANISOU 3580  C   GLU A 476    16959  18500  19412   3012  -4114  -4440       C  
+ATOM   3581  O   GLU A 476      70.012 102.417  -6.811  1.00142.58           O  
+ANISOU 3581  O   GLU A 476    16768  18380  19024   3141  -4136  -4517       O  
+ATOM   3582  CB  GLU A 476      69.486  99.616  -7.958  1.00128.68           C  
+ANISOU 3582  CB  GLU A 476    15020  16860  17012   2665  -3395  -4018       C  
+ATOM   3583  CG  GLU A 476      69.562  98.905  -6.628  1.00128.73           C  
+ANISOU 3583  CG  GLU A 476    15321  16930  16662   2941  -3433  -4041       C  
+ATOM   3584  CD  GLU A 476      68.465  97.866  -6.484  1.00133.35           C  
+ANISOU 3584  CD  GLU A 476    15992  17719  16955   2900  -3026  -3806       C  
+ATOM   3585  OE1 GLU A 476      68.285  97.061  -7.422  1.00132.30           O  
+ANISOU 3585  OE1 GLU A 476    15710  17598  16962   2603  -2816  -3643       O  
+ATOM   3586  OE2 GLU A 476      67.768  97.860  -5.448  1.00136.37           O  
+ANISOU 3586  OE2 GLU A 476    16588  18223  17003   3196  -2912  -3773       O  
+ATOM   3587  N   LYS A 477      71.990 101.424  -6.439  1.00148.46           N  
+ANISOU 3587  N   LYS A 477    17534  18788  20087   3179  -4467  -4596       N  
+ATOM   3588  CA  LYS A 477      72.399 102.365  -5.405  1.00154.25           C  
+ANISOU 3588  CA  LYS A 477    18378  19348  20882   3544  -4976  -4907       C  
+ATOM   3589  C   LYS A 477      72.634 103.732  -5.994  1.00159.76           C  
+ANISOU 3589  C   LYS A 477    18754  19833  22117   3467  -5183  -5014       C  
+ATOM   3590  O   LYS A 477      72.568 104.742  -5.292  1.00158.01           O  
+ANISOU 3590  O   LYS A 477    18598  19516  21921   3749  -5568  -5266       O  
+ATOM   3591  CB  LYS A 477      73.667 101.883  -4.706  1.00154.03           C  
+ANISOU 3591  CB  LYS A 477    18446  19057  21022   3730  -5372  -5076       C  
+ATOM   3592  CG  LYS A 477      73.367 100.787  -3.750  1.00149.76           C  
+ANISOU 3592  CG  LYS A 477    18324  18717  19861   3975  -5258  -5042       C  
+ATOM   3593  CD  LYS A 477      74.437 100.554  -2.740  1.00143.39           C  
+ANISOU 3593  CD  LYS A 477    17737  17664  19082   4327  -5762  -5306       C  
+ATOM   3594  CE  LYS A 477      73.739 100.087  -1.488  1.00145.72           C  
+ANISOU 3594  CE  LYS A 477    18569  18186  18613   4805  -5697  -5337       C  
+ATOM   3595  NZ  LYS A 477      74.661  99.556  -0.469  1.00149.42           N  
+ANISOU 3595  NZ  LYS A 477    19366  18470  18936   5206  -6108  -5561       N  
+ATOM   3596  N   THR A 478      72.908 103.757  -7.293  1.00166.45           N  
+ANISOU 3596  N   THR A 478    19271  20589  23384   3124  -4918  -4810       N  
+ATOM   3597  CA  THR A 478      73.186 105.012  -7.968  1.00170.99           C  
+ANISOU 3597  CA  THR A 478    19509  20929  24531   3052  -5023  -4844       C  
+ATOM   3598  C   THR A 478      71.905 105.733  -8.391  1.00172.33           C  
+ANISOU 3598  C   THR A 478    19686  21358  24435   2999  -4780  -4796       C  
+ATOM   3599  O   THR A 478      71.021 105.170  -9.046  1.00163.10           O  
+ANISOU 3599  O   THR A 478    18581  20461  22928   2823  -4356  -4594       O  
+ATOM   3600  CB  THR A 478      74.217 104.856  -9.128  1.00146.93           C  
+ANISOU 3600  CB  THR A 478    16115  17589  22125   2815  -4843  -4626       C  
+ATOM   3601  OG1 THR A 478      74.873 106.106  -9.369  1.00147.15           O  
+ANISOU 3601  OG1 THR A 478    15790  17239  22881   2857  -5085  -4705       O  
+ATOM   3602  CG2 THR A 478      73.562 104.391 -10.425  1.00144.32           C  
+ANISOU 3602  CG2 THR A 478    15764  17473  21597   2570  -4282  -4318       C  
+ATOM   3603  N   VAL A 479      71.809 106.979  -7.944  1.00177.73           N  
+ANISOU 3603  N   VAL A 479    20309  21929  25291   3181  -5110  -5014       N  
+ATOM   3604  CA  VAL A 479      70.674 107.838  -8.218  1.00175.65           C  
+ANISOU 3604  CA  VAL A 479    20044  21875  24819   3171  -4959  -5013       C  
+ATOM   3605  C   VAL A 479      71.176 109.077  -8.939  1.00177.67           C  
+ANISOU 3605  C   VAL A 479    19928  21830  25749   3101  -5069  -5035       C  
+ATOM   3606  O   VAL A 479      72.353 109.417  -8.840  1.00179.02           O  
+ANISOU 3606  O   VAL A 479    19865  21600  26555   3150  -5387  -5121       O  
+ATOM   3607  CB  VAL A 479      69.983 108.266  -6.909  1.00168.37           C  
+ANISOU 3607  CB  VAL A 479    19442  21113  23418   3524  -5243  -5249       C  
+ATOM   3608  CG1 VAL A 479      69.008 109.403  -7.169  1.00162.23           C  
+ANISOU 3608  CG1 VAL A 479    18603  20470  22565   3531  -5176  -5285       C  
+ATOM   3609  CG2 VAL A 479      69.288 107.080  -6.256  1.00163.89           C  
+ANISOU 3609  CG2 VAL A 479    19236  20856  22180   3624  -4991  -5142       C  
+ATOM   3610  N   MET A 486      57.931 103.433  -6.150  1.00 92.00           N  
+ANISOU 3610  N   MET A 486    10154  13425  11375   3371  -1523  -3283       N  
+ATOM   3611  CA  MET A 486      56.824 104.104  -5.482  1.00110.61           C  
+ANISOU 3611  CA  MET A 486    12558  15882  13585   3644  -1287  -3161       C  
+ATOM   3612  C   MET A 486      56.182 105.150  -6.389  1.00111.38           C  
+ANISOU 3612  C   MET A 486    12428  16016  13877   3424  -1416  -3308       C  
+ATOM   3613  O   MET A 486      56.000 106.305  -6.002  1.00120.35           O  
+ANISOU 3613  O   MET A 486    13691  17243  14794   3639  -1526  -3430       O  
+ATOM   3614  CB  MET A 486      57.288 104.743  -4.170  1.00120.60           C  
+ANISOU 3614  CB  MET A 486    14253  17221  14347   4174  -1395  -3243       C  
+ATOM   3615  CG  MET A 486      58.405 105.768  -4.316  1.00126.87           C  
+ANISOU 3615  CG  MET A 486    15171  17987  15047   4196  -1944  -3629       C  
+ATOM   3616  SD  MET A 486      58.150 107.169  -3.210  1.00146.78           S  
+ANISOU 3616  SD  MET A 486    18044  20597  17128   4757  -2124  -3782       S  
+ATOM   3617  CE  MET A 486      58.003 106.326  -1.640  1.00102.35           C  
+ANISOU 3617  CE  MET A 486    12883  15002  11003   5391  -1797  -3524       C  
+ATOM   3618  N   GLY A 496      57.154  99.412  -2.065  1.00157.62           N  
+ANISOU 3618  N   GLY A 496    19150  21656  19083   4616     82  -2024       N  
+ATOM   3619  CA  GLY A 496      57.031  99.217  -3.498  1.00158.07           C  
+ANISOU 3619  CA  GLY A 496    18749  21636  19677   3991   -116  -2141       C  
+ATOM   3620  C   GLY A 496      57.664  97.914  -3.942  1.00163.71           C  
+ANISOU 3620  C   GLY A 496    19349  22235  20619   3709   -140  -2093       C  
+ATOM   3621  O   GLY A 496      58.040  97.090  -3.111  1.00167.17           O  
+ANISOU 3621  O   GLY A 496    20009  22652  20858   3970     79  -1915       O  
+ATOM   3622  N   GLU A 497      57.778  97.722  -5.254  1.00166.67           N  
+ANISOU 3622  N   GLU A 497    19413  22530  21383   3222   -403  -2250       N  
+ATOM   3623  CA  GLU A 497      58.431  96.534  -5.792  1.00170.73           C  
+ANISOU 3623  CA  GLU A 497    19840  22933  22098   2955   -485  -2239       C  
+ATOM   3624  C   GLU A 497      59.931  96.621  -5.556  1.00172.64           C  
+ANISOU 3624  C   GLU A 497    20403  23228  21965   3043   -809  -2472       C  
+ATOM   3625  O   GLU A 497      60.589  95.609  -5.311  1.00173.01           O  
+ANISOU 3625  O   GLU A 497    20547  23223  21965   3047   -764  -2395       O  
+ATOM   3626  CB  GLU A 497      58.173  96.394  -7.290  1.00169.62           C  
+ANISOU 3626  CB  GLU A 497    19361  22681  22408   2505   -727  -2374       C  
+ATOM   3627  CG  GLU A 497      56.882  97.012  -7.777  1.00173.11           C  
+ANISOU 3627  CG  GLU A 497    19518  23086  23171   2413   -663  -2350       C  
+ATOM   3628  CD  GLU A 497      56.703  96.840  -9.270  1.00173.61           C  
+ANISOU 3628  CD  GLU A 497    19329  23013  23621   2054   -974  -2531       C  
+ATOM   3629  OE1 GLU A 497      57.597  96.241  -9.908  1.00170.78           O  
+ANISOU 3629  OE1 GLU A 497    19040  22600  23250   1901  -1202  -2647       O  
+ATOM   3630  OE2 GLU A 497      55.672  97.300  -9.805  1.00174.73           O  
+ANISOU 3630  OE2 GLU A 497    19233  23093  24064   1967   -999  -2559       O  
+ATOM   3631  N   ILE A 498      60.466  97.836  -5.652  1.00171.44           N  
+ANISOU 3631  N   ILE A 498    20381  23149  21608   3107  -1147  -2757       N  
+ATOM   3632  CA  ILE A 498      61.883  98.078  -5.403  1.00167.93           C  
+ANISOU 3632  CA  ILE A 498    20186  22695  20926   3210  -1503  -2995       C  
+ATOM   3633  C   ILE A 498      62.251  97.620  -4.004  1.00173.82           C  
+ANISOU 3633  C   ILE A 498    21298  23465  21280   3659  -1379  -2903       C  
+ATOM   3634  O   ILE A 498      63.315  97.034  -3.789  1.00168.12           O  
+ANISOU 3634  O   ILE A 498    20732  22684  20462   3692  -1544  -2980       O  
+ATOM   3635  CB  ILE A 498      62.227  99.564  -5.520  1.00165.53           C  
+ANISOU 3635  CB  ILE A 498    19934  22420  20540   3287  -1855  -3283       C  
+ATOM   3636  CG1 ILE A 498      61.848 100.100  -6.901  1.00171.37           C  
+ANISOU 3636  CG1 ILE A 498    20360  23137  21618   2911  -1947  -3365       C  
+ATOM   3637  CG2 ILE A 498      63.706  99.798  -5.239  1.00153.61           C  
+ANISOU 3637  CG2 ILE A 498    18613  20820  18931   3398  -2256  -3525       C  
+ATOM   3638  CD1 ILE A 498      61.891 101.605  -6.996  1.00174.73           C  
+ANISOU 3638  CD1 ILE A 498    20793  23594  22004   2996  -2195  -3587       C  
+ATOM   3639  N   SER A 499      61.362  97.890  -3.054  1.00187.32           N  
+ANISOU 3639  N   SER A 499    23169  25252  22753   4049  -1074  -2727       N  
+ATOM   3640  CA  SER A 499      61.558  97.444  -1.683  1.00198.47           C  
+ANISOU 3640  CA  SER A 499    24999  26682  23729   4594   -880  -2593       C  
+ATOM   3641  C   SER A 499      61.611  95.926  -1.651  1.00206.95           C  
+ANISOU 3641  C   SER A 499    26003  27687  24943   4481   -548  -2315       C  
+ATOM   3642  O   SER A 499      62.302  95.334  -0.823  1.00211.11           O  
+ANISOU 3642  O   SER A 499    26867  28195  25151   4806   -533  -2293       O  
+ATOM   3643  CB  SER A 499      60.435  97.952  -0.780  1.00200.16           C  
+ANISOU 3643  CB  SER A 499    25385  26976  23690   5064   -498  -2368       C  
+ATOM   3644  OG  SER A 499      60.635  97.541   0.556  1.00203.12           O  
+ANISOU 3644  OG  SER A 499    26244  27359  23574   5697   -286  -2224       O  
+ATOM   3645  N   ASP A 500      60.877  95.300  -2.564  1.00209.43           N  
+ANISOU 3645  N   ASP A 500    25883  27938  25751   4040   -320  -2123       N  
+ATOM   3646  CA  ASP A 500      60.862  93.851  -2.655  1.00212.36           C  
+ANISOU 3646  CA  ASP A 500    26120  28207  26361   3883    -39  -1865       C  
+ATOM   3647  C   ASP A 500      62.097  93.307  -3.366  1.00203.21           C  
+ANISOU 3647  C   ASP A 500    24946  26999  25265   3557   -426  -2093       C  
+ATOM   3648  O   ASP A 500      62.511  92.195  -3.092  1.00204.96           O  
+ANISOU 3648  O   ASP A 500    25238  27165  25472   3573   -286  -1954       O  
+ATOM   3649  CB  ASP A 500      59.580  93.355  -3.335  1.00221.23           C  
+ANISOU 3649  CB  ASP A 500    26765  29217  28075   3573    287  -1593       C  
+ATOM   3650  CG  ASP A 500      58.346  93.583  -2.486  1.00233.55           C  
+ANISOU 3650  CG  ASP A 500    28310  30773  29657   3938    816  -1243       C  
+ATOM   3651  OD1 ASP A 500      58.502  93.807  -1.272  1.00239.09           O  
+ANISOU 3651  OD1 ASP A 500    29432  31558  29852   4492   1029  -1137       O  
+ATOM   3652  OD2 ASP A 500      57.226  93.532  -3.034  1.00237.24           O  
+ANISOU 3652  OD2 ASP A 500    28355  31128  30656   3709   1011  -1072       O  
+ATOM   3653  N   ILE A 501      62.692  94.094  -4.258  1.00192.67           N  
+ANISOU 3653  N   ILE A 501    23525  25673  24008   3292   -875  -2413       N  
+ATOM   3654  CA  ILE A 501      63.864  93.633  -5.007  1.00181.71           C  
+ANISOU 3654  CA  ILE A 501    22108  24215  22719   3006  -1195  -2587       C  
+ATOM   3655  C   ILE A 501      65.165  93.699  -4.196  1.00170.68           C  
+ANISOU 3655  C   ILE A 501    21064  22814  20972   3287  -1450  -2764       C  
+ATOM   3656  O   ILE A 501      65.940  92.735  -4.173  1.00164.70           O  
+ANISOU 3656  O   ILE A 501    20376  21999  20202   3222  -1476  -2736       O  
+ATOM   3657  CB  ILE A 501      64.023  94.385  -6.353  1.00138.03           C  
+ANISOU 3657  CB  ILE A 501    16341  18649  17453   2657  -1507  -2802       C  
+ATOM   3658  CG1 ILE A 501      63.385  93.589  -7.491  1.00130.33           C  
+ANISOU 3658  CG1 ILE A 501    15061  17587  16872   2296  -1411  -2683       C  
+ATOM   3659  CG2 ILE A 501      65.491  94.642  -6.682  1.00137.98           C  
+ANISOU 3659  CG2 ILE A 501    16437  18581  17410   2596  -1883  -3037       C  
+ATOM   3660  CD1 ILE A 501      61.877  93.516  -7.426  1.00127.50           C  
+ANISOU 3660  CD1 ILE A 501    14480  17217  16748   2292  -1111  -2482       C  
+ATOM   3661  N   HIS A 502      65.396  94.829  -3.532  1.00164.00           N  
+ANISOU 3661  N   HIS A 502    20439  22007  19869   3613  -1676  -2964       N  
+ATOM   3662  CA  HIS A 502      66.603  95.022  -2.737  1.00157.69           C  
+ANISOU 3662  CA  HIS A 502    19968  21147  18800   3931  -2029  -3195       C  
+ATOM   3663  C   HIS A 502      66.627  94.018  -1.594  1.00162.64           C  
+ANISOU 3663  C   HIS A 502    20933  21795  19069   4322  -1761  -3012       C  
+ATOM   3664  O   HIS A 502      67.677  93.496  -1.229  1.00164.35           O  
+ANISOU 3664  O   HIS A 502    21348  21935  19163   4433  -1971  -3125       O  
+ATOM   3665  CB  HIS A 502      66.654  96.452  -2.195  1.00150.83           C  
+ANISOU 3665  CB  HIS A 502    19272  20282  17755   4264  -2351  -3452       C  
+ATOM   3666  CG  HIS A 502      67.989  97.117  -2.348  1.00143.78           C  
+ANISOU 3666  CG  HIS A 502    18387  19223  17019   4248  -2929  -3804       C  
+ATOM   3667  ND1 HIS A 502      68.672  97.155  -3.544  1.00137.00           N  
+ANISOU 3667  ND1 HIS A 502    17193  18253  16609   3785  -3096  -3872       N  
+ATOM   3668  CD2 HIS A 502      68.759  97.787  -1.457  1.00140.15           C  
+ANISOU 3668  CD2 HIS A 502    18217  18647  16387   4674  -3387  -4099       C  
+ATOM   3669  CE1 HIS A 502      69.807  97.814  -3.383  1.00131.33           C  
+ANISOU 3669  CE1 HIS A 502    16503  17343  16055   3893  -3583  -4155       C  
+ATOM   3670  NE2 HIS A 502      69.883  98.208  -2.125  1.00130.34           N  
+ANISOU 3670  NE2 HIS A 502    16741  17200  15583   4416  -3815  -4326       N  
+ATOM   3671  N   THR A 503      65.450  93.751  -1.039  1.00163.37           N  
+ANISOU 3671  N   THR A 503    21080  21971  19022   4552  -1268  -2705       N  
+ATOM   3672  CA  THR A 503      65.295  92.771   0.033  1.00163.31           C  
+ANISOU 3672  CA  THR A 503    21384  21972  18696   4974   -872  -2437       C  
+ATOM   3673  C   THR A 503      65.393  91.327  -0.475  1.00151.49           C  
+ANISOU 3673  C   THR A 503    19659  20415  17484   4610   -614  -2206       C  
+ATOM   3674  O   THR A 503      65.955  90.464   0.198  1.00146.11           O  
+ANISOU 3674  O   THR A 503    19247  19705  16563   4853   -520  -2130       O  
+ATOM   3675  CB  THR A 503      63.956  92.972   0.783  1.00170.09           C  
+ANISOU 3675  CB  THR A 503    22342  22899  19387   5379   -334  -2106       C  
+ATOM   3676  OG1 THR A 503      64.026  94.162   1.576  1.00176.61           O  
+ANISOU 3676  OG1 THR A 503    23551  23775  19778   5902   -585  -2325       O  
+ATOM   3677  CG2 THR A 503      63.645  91.790   1.693  1.00171.53           C  
+ANISOU 3677  CG2 THR A 503    22747  23054  19374   5760    235  -1707       C  
+ATOM   3678  N   LYS A 504      64.865  91.071  -1.669  1.00141.41           N  
+ANISOU 3678  N   LYS A 504    17911  19106  16711   4058   -541  -2117       N  
+ATOM   3679  CA  LYS A 504      64.918  89.730  -2.251  1.00138.96           C  
+ANISOU 3679  CA  LYS A 504    17368  18711  16719   3706   -369  -1930       C  
+ATOM   3680  C   LYS A 504      66.335  89.382  -2.646  1.00142.04           C  
+ANISOU 3680  C   LYS A 504    17848  19062  17058   3520   -779  -2178       C  
+ATOM   3681  O   LYS A 504      66.762  88.230  -2.559  1.00134.46           O  
+ANISOU 3681  O   LYS A 504    16927  18053  16108   3463   -666  -2057       O  
+ATOM   3682  CB  LYS A 504      64.010  89.614  -3.477  1.00129.82           C  
+ANISOU 3682  CB  LYS A 504    15721  17488  16116   3223   -303  -1839       C  
+ATOM   3683  CG  LYS A 504      62.585  89.222  -3.164  1.00130.75           C  
+ANISOU 3683  CG  LYS A 504    15616  17544  16518   3307    225  -1458       C  
+ATOM   3684  N   LEU A 505      67.053  90.395  -3.100  1.00138.41           N  
+ANISOU 3684  N   LEU A 505    17394  18602  16593   3427  -1237  -2505       N  
+ATOM   3685  CA  LEU A 505      68.435  90.224  -3.458  1.00120.29           C  
+ANISOU 3685  CA  LEU A 505    15154  16230  14322   3285  -1619  -2726       C  
+ATOM   3686  C   LEU A 505      69.260  89.889  -2.223  1.00124.70           C  
+ANISOU 3686  C   LEU A 505    16126  16770  14484   3724  -1710  -2800       C  
+ATOM   3687  O   LEU A 505      70.166  89.052  -2.259  1.00124.02           O  
+ANISOU 3687  O   LEU A 505    16106  16619  14396   3643  -1808  -2823       O  
+ATOM   3688  CB  LEU A 505      68.966  91.499  -4.066  1.00109.42           C  
+ANISOU 3688  CB  LEU A 505    13673  14807  13094   3171  -2036  -3016       C  
+ATOM   3689  CG  LEU A 505      70.386  91.097  -4.404  1.00128.61           C  
+ANISOU 3689  CG  LEU A 505    16120  17111  15635   3034  -2336  -3157       C  
+ATOM   3690  CD1 LEU A 505      70.509  91.048  -5.920  1.00141.22           C  
+ANISOU 3690  CD1 LEU A 505    17393  18648  17616   2580  -2370  -3131       C  
+ATOM   3691  CD2 LEU A 505      71.441  91.946  -3.655  1.00114.58           C  
+ANISOU 3691  CD2 LEU A 505    14554  15224  13758   3350  -2784  -3462       C  
+ATOM   3692  N   LEU A 506      68.951  90.579  -1.132  1.00130.40           N  
+ANISOU 3692  N   LEU A 506    17159  17539  14847   4233  -1705  -2854       N  
+ATOM   3693  CA  LEU A 506      69.612  90.336   0.139  1.00131.63           C  
+ANISOU 3693  CA  LEU A 506    17798  17665  14549   4786  -1814  -2947       C  
+ATOM   3694  C   LEU A 506      69.501  88.865   0.507  1.00144.65           C  
+ANISOU 3694  C   LEU A 506    19548  19330  16080   4841  -1380  -2638       C  
+ATOM   3695  O   LEU A 506      70.448  88.287   1.024  1.00156.36           O  
+ANISOU 3695  O   LEU A 506    21304  20754  17351   5038  -1550  -2741       O  
+ATOM   3696  CB  LEU A 506      69.008  91.206   1.240  1.00120.17           C  
+ANISOU 3696  CB  LEU A 506    16713  16270  12675   5409  -1772  -2980       C  
+ATOM   3697  N   ARG A 507      68.351  88.260   0.214  1.00146.01           N  
+ANISOU 3697  N   ARG A 507    19470  19552  16453   4658   -839  -2263       N  
+ATOM   3698  CA  ARG A 507      68.159  86.831   0.443  1.00152.26           C  
+ANISOU 3698  CA  ARG A 507    20261  20320  17271   4649   -394  -1930       C  
+ATOM   3699  C   ARG A 507      69.164  86.004  -0.360  1.00145.14           C  
+ANISOU 3699  C   ARG A 507    19199  19352  16597   4200   -653  -2042       C  
+ATOM   3700  O   ARG A 507      69.471  84.873   0.007  1.00142.95           O  
+ANISOU 3700  O   ARG A 507    19044  19045  16224   4273   -450  -1882       O  
+ATOM   3701  CB  ARG A 507      66.724  86.401   0.111  1.00161.16           C  
+ANISOU 3701  CB  ARG A 507    21023  21434  18777   4463    165  -1523       C  
+ATOM   3702  CG  ARG A 507      65.661  86.975   1.046  1.00177.63           C  
+ANISOU 3702  CG  ARG A 507    23283  23565  20645   4978    585  -1290       C  
+ATOM   3703  CD  ARG A 507      64.263  86.468   0.696  1.00187.68           C  
+ANISOU 3703  CD  ARG A 507    24111  24757  22442   4769   1148   -857       C  
+ATOM   3704  NE  ARG A 507      63.229  87.082   1.527  1.00198.88           N  
+ANISOU 3704  NE  ARG A 507    25661  26202  23704   5259   1588   -600       N  
+ATOM   3705  CZ  ARG A 507      61.923  86.883   1.368  1.00205.71           C  
+ANISOU 3705  CZ  ARG A 507    26137  26967  25058   5169   2095   -210       C  
+ATOM   3706  NH1 ARG A 507      61.484  86.084   0.406  1.00207.33           N  
+ANISOU 3706  NH1 ARG A 507    25797  27020  25959   4610   2159    -75       N  
+ATOM   3707  NH2 ARG A 507      61.055  87.485   2.171  1.00208.11           N  
+ANISOU 3707  NH2 ARG A 507    26596  27293  25185   5668   2517     41       N  
+ATOM   3708  N   LEU A 508      69.681  86.579  -1.445  1.00134.19           N  
+ANISOU 3708  N   LEU A 508    17555  17933  15496   3776  -1072  -2294       N  
+ATOM   3709  CA  LEU A 508      70.654  85.888  -2.287  1.00125.06           C  
+ANISOU 3709  CA  LEU A 508    16259  16704  14555   3385  -1304  -2382       C  
+ATOM   3710  C   LEU A 508      72.070  85.983  -1.730  1.00133.75           C  
+ANISOU 3710  C   LEU A 508    17660  17739  15419   3609  -1705  -2655       C  
+ATOM   3711  O   LEU A 508      72.775  84.975  -1.650  1.00132.42           O  
+ANISOU 3711  O   LEU A 508    17581  17530  15202   3562  -1700  -2615       O  
+ATOM   3712  CB  LEU A 508      70.606  86.404  -3.726  1.00112.64           C  
+ANISOU 3712  CB  LEU A 508    14312  15095  13392   2907  -1513  -2482       C  
+ATOM   3713  CG  LEU A 508      69.271  86.186  -4.436  1.00113.17           C  
+ANISOU 3713  CG  LEU A 508    14056  15174  13769   2653  -1215  -2260       C  
+ATOM   3714  CD1 LEU A 508      69.459  86.184  -5.945  1.00106.22           C  
+ANISOU 3714  CD1 LEU A 508    12898  14225  13237   2216  -1432  -2343       C  
+ATOM   3715  CD2 LEU A 508      68.613  84.894  -3.968  1.00117.32           C  
+ANISOU 3715  CD2 LEU A 508    14560  15678  14341   2703   -779  -1932       C  
+ATOM   3716  N   SER A 509      72.491  87.188  -1.355  1.00130.72           N  
+ANISOU 3716  N   SER A 509    17414  17317  14936   3856  -2085  -2944       N  
+ATOM   3717  CA  SER A 509      73.767  87.347  -0.663  1.00136.66           C  
+ANISOU 3717  CA  SER A 509    18457  17948  15519   4156  -2531  -3236       C  
+ATOM   3718  C   SER A 509      73.698  86.583   0.646  1.00143.40           C  
+ANISOU 3718  C   SER A 509    19773  18845  15866   4687  -2319  -3138       C  
+ATOM   3719  O   SER A 509      74.680  85.977   1.082  1.00138.35           O  
+ANISOU 3719  O   SER A 509    19358  18121  15090   4839  -2521  -3256       O  
+ATOM   3720  CB  SER A 509      74.089  88.820  -0.410  1.00130.15           C  
+ANISOU 3720  CB  SER A 509    17685  17028  14738   4382  -3008  -3572       C  
+ATOM   3721  OG  SER A 509      74.991  89.319  -1.384  1.00125.27           O  
+ANISOU 3721  OG  SER A 509    16739  16248  14609   4003  -3376  -3749       O  
+ATOM   3722  N   SER A 510      72.520  86.615   1.263  1.00144.65           N  
+ANISOU 3722  N   SER A 510    20078  19123  15761   4999  -1878  -2899       N  
+ATOM   3723  CA  SER A 510      72.236  85.777   2.417  1.00148.97           C  
+ANISOU 3723  CA  SER A 510    21044  19713  15846   5526  -1487  -2677       C  
+ATOM   3724  C   SER A 510      72.424  84.315   2.046  1.00145.48           C  
+ANISOU 3724  C   SER A 510    20455  19266  15554   5204  -1182  -2426       C  
+ATOM   3725  O   SER A 510      73.001  83.552   2.811  1.00140.49           O  
+ANISOU 3725  O   SER A 510    20177  18606  14598   5539  -1145  -2415       O  
+ATOM   3726  CB  SER A 510      70.807  85.999   2.911  1.00149.05           C  
+ANISOU 3726  CB  SER A 510    21111  19824  15699   5832   -930  -2348       C  
+ATOM   3727  OG  SER A 510      70.346  84.882   3.643  1.00147.27           O  
+ANISOU 3727  OG  SER A 510    21097  19617  15240   6154   -337  -1962       O  
+ATOM   3728  N   SER A 511      71.946  83.946   0.858  1.00139.39           N  
+ANISOU 3728  N   SER A 511    19184  18508  15269   4583  -1004  -2249       N  
+ATOM   3729  CA  SER A 511      72.011  82.573   0.365  1.00129.68           C  
+ANISOU 3729  CA  SER A 511    17763  17254  14255   4236   -745  -2013       C  
+ATOM   3730  C   SER A 511      73.437  82.171   0.006  1.00129.30           C  
+ANISOU 3730  C   SER A 511    17762  17135  14232   4035  -1175  -2258       C  
+ATOM   3731  O   SER A 511      73.792  80.991   0.047  1.00109.17           O  
+ANISOU 3731  O   SER A 511    15254  14565  11659   3957  -1019  -2119       O  
+ATOM   3732  CB  SER A 511      71.108  82.416  -0.862  1.00125.40           C  
+ANISOU 3732  CB  SER A 511    16701  16703  14243   3684   -564  -1828       C  
+ATOM   3733  OG  SER A 511      71.167  81.106  -1.394  1.00137.08           O  
+ANISOU 3733  OG  SER A 511    17990  18125  15969   3359   -392  -1635       O  
+ATOM   3734  N   GLN A 512      74.247  83.164  -0.347  1.00131.12           N  
+ANISOU 3734  N   GLN A 512    17958  17304  14560   3957  -1699  -2603       N  
+ATOM   3735  CA  GLN A 512      75.621  82.941  -0.774  1.00120.52           C  
+ANISOU 3735  CA  GLN A 512    16587  15845  13361   3755  -2110  -2821       C  
+ATOM   3736  C   GLN A 512      76.508  82.796   0.431  1.00123.24           C  
+ANISOU 3736  C   GLN A 512    17385  16120  13322   4264  -2359  -3023       C  
+ATOM   3737  O   GLN A 512      77.569  82.174   0.385  1.00128.94           O  
+ANISOU 3737  O   GLN A 512    18162  16749  14081   4188  -2573  -3124       O  
+ATOM   3738  CB  GLN A 512      76.096  84.150  -1.546  1.00118.44           C  
+ANISOU 3738  CB  GLN A 512    16077  15483  13440   3537  -2531  -3073       C  
+ATOM   3739  CG  GLN A 512      76.798  83.822  -2.807  1.00125.91           C  
+ANISOU 3739  CG  GLN A 512    16711  16343  14787   3048  -2644  -3057       C  
+ATOM   3740  CD  GLN A 512      76.915  85.020  -3.690  1.00137.22           C  
+ANISOU 3740  CD  GLN A 512    17859  17690  16589   2843  -2877  -3186       C  
+ATOM   3741  OE1 GLN A 512      77.456  86.052  -3.281  1.00153.03           O  
+ANISOU 3741  OE1 GLN A 512    19904  19568  18672   3053  -3245  -3442       O  
+ATOM   3742  NE2 GLN A 512      76.353  84.922  -4.897  1.00128.62           N  
+ANISOU 3742  NE2 GLN A 512    16487  16643  15739   2470  -2678  -3017       N  
+ATOM   3743  N   GLY A 513      76.073  83.422   1.510  1.00128.93           N  
+ANISOU 3743  N   GLY A 513    18453  16869  13665   4826  -2362  -3100       N  
+ATOM   3744  CA  GLY A 513      76.767  83.309   2.761  1.00135.31           C  
+ANISOU 3744  CA  GLY A 513    19790  17602  14021   5449  -2610  -3308       C  
+ATOM   3745  C   GLY A 513      76.597  81.908   3.295  1.00133.10           C  
+ANISOU 3745  C   GLY A 513    19748  17398  13425   5620  -2132  -3010       C  
+ATOM   3746  O   GLY A 513      77.464  81.407   3.988  1.00135.48           O  
+ANISOU 3746  O   GLY A 513    20407  17622  13448   5952  -2344  -3163       O  
+ATOM   3747  N   THR A 514      75.478  81.270   2.971  1.00130.00           N  
+ANISOU 3747  N   THR A 514    19139  17128  13128   5402  -1499  -2584       N  
+ATOM   3748  CA  THR A 514      75.252  79.899   3.409  1.00136.05           C  
+ANISOU 3748  CA  THR A 514    20053  17932  13707   5525   -990  -2248       C  
+ATOM   3749  C   THR A 514      76.125  78.980   2.571  1.00136.91           C  
+ANISOU 3749  C   THR A 514    19914  17993  14111   4991  -1151  -2279       C  
+ATOM   3750  O   THR A 514      76.451  77.859   2.971  1.00136.85           O  
+ANISOU 3750  O   THR A 514    20093  17981  13923   5102   -944  -2139       O  
+ATOM   3751  CB  THR A 514      73.784  79.475   3.234  1.00141.83           C  
+ANISOU 3751  CB  THR A 514    20522  18734  14631   5408   -287  -1768       C  
+ATOM   3752  OG1 THR A 514      73.591  78.921   1.927  1.00142.93           O  
+ANISOU 3752  OG1 THR A 514    20113  18858  15336   4676   -233  -1640       O  
+ATOM   3753  CG2 THR A 514      72.861  80.659   3.413  1.00145.09           C  
+ANISOU 3753  CG2 THR A 514    20916  19192  15018   5620   -214  -1761       C  
+ATOM   3754  N   ILE A 515      76.491  79.473   1.394  1.00129.08           N  
+ANISOU 3754  N   ILE A 515    18518  16960  13565   4442  -1494  -2442       N  
+ATOM   3755  CA  ILE A 515      77.327  78.735   0.468  1.00119.39           C  
+ANISOU 3755  CA  ILE A 515    17053  15677  12633   3951  -1657  -2465       C  
+ATOM   3756  C   ILE A 515      78.787  78.796   0.895  1.00127.05           C  
+ANISOU 3756  C   ILE A 515    18275  16524  13475   4143  -2161  -2798       C  
+ATOM   3757  O   ILE A 515      79.482  77.778   0.906  1.00128.44           O  
+ANISOU 3757  O   ILE A 515    18530  16671  13600   4065  -2153  -2760       O  
+ATOM   3758  CB  ILE A 515      77.220  79.308  -0.945  1.00110.42           C  
+ANISOU 3758  CB  ILE A 515    15452  14516  11987   3401  -1815  -2500       C  
+ATOM   3759  CG1 ILE A 515      75.801  79.150  -1.481  1.00109.39           C  
+ANISOU 3759  CG1 ILE A 515    15038  14466  12060   3177  -1388  -2203       C  
+ATOM   3760  CG2 ILE A 515      78.168  78.587  -1.861  1.00112.80           C  
+ANISOU 3760  CG2 ILE A 515    15581  14743  12533   2997  -1979  -2516       C  
+ATOM   3761  CD1 ILE A 515      75.570  79.879  -2.780  1.00110.01           C  
+ANISOU 3761  CD1 ILE A 515    14740  14519  12538   2759  -1561  -2268       C  
+ATOM   3762  N   GLU A 516      79.240  79.996   1.247  1.00122.76           N  
+ANISOU 3762  N   GLU A 516    17837  15878  12927   4400  -2627  -3132       N  
+ATOM   3763  CA  GLU A 516      80.636  80.218   1.589  1.00124.18           C  
+ANISOU 3763  CA  GLU A 516    18172  15860  13149   4571  -3208  -3493       C  
+ATOM   3764  C   GLU A 516      81.099  79.274   2.681  1.00126.09           C  
+ANISOU 3764  C   GLU A 516    18894  16095  12918   5029  -3183  -3522       C  
+ATOM   3765  O   GLU A 516      82.117  78.595   2.539  1.00116.84           O  
+ANISOU 3765  O   GLU A 516    17721  14825  11848   4893  -3381  -3605       O  
+ATOM   3766  CB  GLU A 516      80.863  81.656   2.046  1.00135.21           C  
+ANISOU 3766  CB  GLU A 516    19658  17112  14603   4907  -3722  -3854       C  
+ATOM   3767  CG  GLU A 516      82.315  82.045   1.962  1.00146.41           C  
+ANISOU 3767  CG  GLU A 516    20992  18238  16400   4885  -4381  -4215       C  
+ATOM   3768  CD  GLU A 516      82.865  81.775   0.582  1.00151.28           C  
+ANISOU 3768  CD  GLU A 516    21100  18789  17590   4220  -4329  -4073       C  
+ATOM   3769  OE1 GLU A 516      82.373  82.413  -0.367  1.00146.52           O  
+ANISOU 3769  OE1 GLU A 516    20133  18221  17316   3867  -4198  -3957       O  
+ATOM   3770  OE2 GLU A 516      83.763  80.917   0.441  1.00154.24           O  
+ANISOU 3770  OE2 GLU A 516    21471  19078  18055   4089  -4397  -4065       O  
+END                                                                             
diff --git a/examples/testdata/4IM2_nterm.pdb b/examples/testdata/4IM2_nterm.pdb
new file mode 100644 (file)
index 0000000..48f7803
--- /dev/null
@@ -0,0 +1,139 @@
+HEADER    TRANSFERASE/TRANSFERASE INHIBITOR       01-JAN-13   4IM2              
+TITLE     STRUCTURE OF TANK-BINDING KINASE 1  (Truncated test file)
+DBREF  4IM2 A    1   657  UNP    Q9UHD2   TBK1_HUMAN       1    657             
+SEQADV 4IM2 GLY A   -5  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQADV 4IM2 SER A   -4  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQADV 4IM2 GLY A   -3  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQADV 4IM2 SER A   -2  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQADV 4IM2 GLY A   -1  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQADV 4IM2 SER A    0  UNP  Q9UHD2              EXPRESSION TAG                 
+SEQRES   1 A  663  GLY SER GLY SER GLY SER MET GLN SER THR SER ASN HIS
+ATOM      1  N   GLY A  -1     126.784   4.226 -23.353  1.00158.13           N  
+ANISOU    1  N   GLY A  -1    19370  17517  23197   6628   1162   2075       N  
+ATOM      2  CA  GLY A  -1     125.521   4.306 -24.062  1.00150.94           C  
+ANISOU    2  CA  GLY A  -1    18746  16231  22374   6153   1277   1996       C  
+ATOM      3  C   GLY A  -1     125.742   4.361 -25.557  1.00146.29           C  
+ANISOU    3  C   GLY A  -1    18187  15453  21943   5900   1405   1498       C  
+ATOM      4  O   GLY A  -1     126.691   4.980 -26.029  1.00150.85           O  
+ANISOU    4  O   GLY A  -1    18536  16366  22413   5906   1385   1160       O  
+ATOM      5  N   SER A   0     124.869   3.710 -26.313  1.00137.36           N  
+ANISOU    5  N   SER A   0    17328  13796  21068   5675   1550   1432       N  
+ATOM      6  CA  SER A   0     125.052   3.672 -27.755  1.00139.44           C  
+ANISOU    6  CA  SER A   0    17634  13884  21461   5464   1674    953       C  
+ATOM      7  C   SER A   0     123.846   4.104 -28.574  1.00137.43           C  
+ANISOU    7  C   SER A   0    17591  13478  21149   4975   1714    755       C  
+ATOM      8  O   SER A   0     122.737   4.275 -28.071  1.00128.25           O  
+ANISOU    8  O   SER A   0    16566  12244  19921   4780   1667    988       O  
+ATOM      9  CB  SER A   0     125.578   2.312 -28.214  1.00155.09           C  
+ANISOU    9  CB  SER A   0    19671  15395  23862   5752   1827    887       C  
+ATOM     10  OG  SER A   0     126.993   2.281 -28.131  1.00164.59           O  
+ANISOU   10  OG  SER A   0    20608  16866  25062   6113   1802    781       O  
+ATOM     11  N   MET A   1     124.096   4.270 -29.861  1.00134.87           N  
+ANISOU   11  N   MET A   1    17283  13123  20839   4799   1805    311       N  
+ATOM     12  CA  MET A   1     123.214   5.039 -30.698  1.00125.22           C  
+ANISOU   12  CA  MET A   1    16193  11958  19428   4369   1804     70       C  
+ATOM     13  C   MET A   1     122.885   4.351 -32.006  1.00124.69           C  
+ANISOU   13  C   MET A   1    16293  11510  19572   4225   1942   -293       C  
+ATOM     14  O   MET A   1     123.723   3.686 -32.606  1.00129.61           O  
+ANISOU   14  O   MET A   1    16868  11984  20394   4414   2057   -519       O  
+ATOM     15  CB  MET A   1     123.867   6.392 -30.970  1.00122.51           C  
+ANISOU   15  CB  MET A   1    15675  12140  18732   4254   1762   -132       C  
+ATOM     16  CG  MET A   1     125.074   6.368 -31.866  1.00120.74           C  
+ANISOU   16  CG  MET A   1    15318  12013  18544   4369   1884   -495       C  
+ATOM     17  SD  MET A   1     125.834   7.982 -31.850  1.00196.33           S  
+ANISOU   17  SD  MET A   1    24655  22193  27747   4246   1861   -642       S  
+ATOM     18  CE  MET A   1     126.785   7.896 -30.345  1.00132.53           C  
+ANISOU   18  CE  MET A   1    16264  14397  19693   4639   1728   -355       C  
+ATOM     19  N   GLN A   2     121.643   4.496 -32.440  1.00123.78           N  
+ANISOU   19  N   GLN A   2    16363  11253  19416   3898   1925   -369       N  
+ATOM     20  CA  GLN A   2     121.313   4.161 -33.809  1.00128.51           C  
+ANISOU   20  CA  GLN A   2    17093  11643  20094   3714   2020   -799       C  
+ATOM     21  C   GLN A   2     121.639   5.396 -34.617  1.00121.06           C  
+ANISOU   21  C   GLN A   2    16105  11138  18752   3541   2004  -1065       C  
+ATOM     22  O   GLN A   2     121.959   6.442 -34.059  1.00107.32           O  
+ANISOU   22  O   GLN A   2    14247   9790  16739   3528   1928   -906       O  
+ATOM     23  CB  GLN A   2     119.832   3.845 -33.965  1.00136.11           C  
+ANISOU   23  CB  GLN A   2    18240  12314  21161   3439   1994   -809       C  
+ATOM     24  CG  GLN A   2     119.171   3.277 -32.740  1.00148.45           C  
+ANISOU   24  CG  GLN A   2    19841  13613  22949   3500   1973   -376       C  
+ATOM     25  CD  GLN A   2     117.773   2.799 -33.042  1.00158.28           C  
+ANISOU   25  CD  GLN A   2    21243  14511  24385   3221   1993   -473       C  
+ATOM     26  OE1 GLN A   2     116.827   3.097 -32.313  1.00163.28           O  
+ANISOU   26  OE1 GLN A   2    21915  15160  24966   3070   1921   -211       O  
+ATOM     27  NE2 GLN A   2     117.631   2.057 -34.134  1.00155.02           N  
+ANISOU   27  NE2 GLN A   2    20902  13797  24200   3144   2095   -885       N  
+ATOM     28  N   SER A   3     121.547   5.280 -35.931  1.00124.66           N  
+ANISOU   28  N   SER A   3    16661  11530  19175   3410   2090  -1474       N  
+ATOM     29  CA  SER A   3     121.742   6.432 -36.781  1.00120.35           C  
+ANISOU   29  CA  SER A   3    16121  11369  18236   3242   2107  -1703       C  
+ATOM     30  C   SER A   3     121.190   6.186 -38.161  1.00125.27           C  
+ANISOU   30  C   SER A   3    16914  11882  18801   3073   2165  -2108       C  
+ATOM     31  O   SER A   3     120.746   5.090 -38.489  1.00135.86           O  
+ANISOU   31  O   SER A   3    18337  12848  20436   3082   2197  -2268       O  
+ATOM     32  CB  SER A   3     123.217   6.751 -36.923  1.00116.70           C  
+ANISOU   32  CB  SER A   3    15475  11153  17711   3435   2220  -1807       C  
+ATOM     33  OG  SER A   3     123.770   5.974 -37.974  1.00112.78           O  
+ANISOU   33  OG  SER A   3    15005  10478  17367   3528   2375  -2175       O  
+ATOM     34  N   THR A   4     121.244   7.232 -38.972  1.00116.48           N  
+ANISOU   34  N   THR A   4    15849  11109  17301   2928   2191  -2281       N  
+ATOM     35  CA  THR A   4     120.978   7.130 -40.394  1.00113.12           C  
+ANISOU   35  CA  THR A   4    15568  10691  16723   2823   2262  -2692       C  
+ATOM     36  C   THR A   4     122.118   7.839 -41.119  1.00116.32           C  
+ANISOU   36  C   THR A   4    15918  11410  16867   2884   2432  -2867       C  
+ATOM     37  O   THR A   4     123.106   8.236 -40.495  1.00117.17           O  
+ANISOU   37  O   THR A   4    15851  11671  16998   3007   2493  -2706       O  
+ATOM     38  CB  THR A   4     119.615   7.756 -40.775  1.00106.20           C  
+ANISOU   38  CB  THR A   4    14854   9927  15568   2568   2121  -2713       C  
+ATOM     39  OG1 THR A   4     119.774   9.154 -41.043  1.00107.14           O  
+ANISOU   39  OG1 THR A   4    15001  10451  15258   2480   2138  -2644       O  
+ATOM     40  CG2 THR A   4     118.597   7.567 -39.657  1.00 99.42           C  
+ANISOU   40  CG2 THR A   4    13990   8889  14896   2483   1956  -2396       C  
+ATOM     41  N   SER A   5     121.981   7.982 -42.432  1.00116.70           N  
+ANISOU   41  N   SER A   5    16106  11564  16670   2803   2517  -3210       N  
+ATOM     42  CA  SER A   5     122.975   8.671 -43.247  1.00122.60           C  
+ANISOU   42  CA  SER A   5    16837  12604  17144   2839   2721  -3385       C  
+ATOM     43  C   SER A   5     123.317  10.064 -42.713  1.00128.43           C  
+ANISOU   43  C   SER A   5    17501  13666  17630   2768   2748  -3105       C  
+ATOM     44  O   SER A   5     124.481  10.467 -42.712  1.00133.01           O  
+ANISOU   44  O   SER A   5    17934  14410  18193   2852   2925  -3133       O  
+ATOM     45  CB  SER A   5     122.473   8.779 -44.687  1.00127.34           C  
+ANISOU   45  CB  SER A   5    17646  13318  17418   2741   2774  -3729       C  
+ATOM     46  OG  SER A   5     121.108   9.163 -44.712  1.00135.31           O  
+ANISOU   46  OG  SER A   5    18807  14370  18235   2568   2576  -3643       O  
+ATOM     47  N   ASN A   6     122.303  10.783 -42.240  1.00122.78           N  
+ANISOU   47  N   ASN A   6    16868  13033  16750   2608   2581  -2859       N  
+ATOM     48  CA  ASN A   6     122.460  12.194 -41.897  1.00114.44           C  
+ANISOU   48  CA  ASN A   6    15777  12275  15429   2507   2620  -2639       C  
+ATOM     49  C   ASN A   6     122.348  12.537 -40.412  1.00108.55           C  
+ANISOU   49  C   ASN A   6    14870  11544  14832   2498   2474  -2282       C  
+ATOM     50  O   ASN A   6     122.712  13.640 -40.004  1.00110.16           O  
+ANISOU   50  O   ASN A   6    14983  11984  14890   2435   2531  -2135       O  
+ATOM     51  CB  ASN A   6     121.461  13.034 -42.695  1.00113.00           C  
+ANISOU   51  CB  ASN A   6    15832  12254  14850   2339   2583  -2658       C  
+ATOM     52  CG  ASN A   6     121.646  12.886 -44.191  1.00118.14           C  
+ANISOU   52  CG  ASN A   6    16648  12982  15257   2366   2743  -2999       C  
+ATOM     53  OD1 ASN A   6     122.765  12.952 -44.698  1.00122.16           O  
+ANISOU   53  OD1 ASN A   6    17097  13580  15737   2449   2985  -3154       O  
+ATOM     54  ND2 ASN A   6     120.546  12.672 -44.905  1.00118.96           N  
+ANISOU   54  ND2 ASN A   6    16946  13074  15179   2302   2609  -3139       N  
+ATOM     55  N   HIS A   7     121.845  11.605 -39.608  1.00100.48           N  
+ANISOU   55  N   HIS A   7    13810  10269  14098   2560   2305  -2149       N  
+ATOM     56  CA  HIS A   7     121.657  11.871 -38.183  1.00 91.49           C  
+ANISOU   56  CA  HIS A   7    12535   9160  13066   2569   2162  -1799       C  
+ATOM     57  C   HIS A   7     122.134  10.725 -37.300  1.00 97.74           C  
+ANISOU   57  C   HIS A   7    13180   9725  14232   2793   2117  -1685       C  
+ATOM     58  O   HIS A   7     122.456   9.647 -37.791  1.00101.23           O  
+ANISOU   58  O   HIS A   7    13643   9923  14898   2924   2188  -1876       O  
+ATOM     59  CB  HIS A   7     120.189  12.175 -37.884  1.00 86.30           C  
+ANISOU   59  CB  HIS A   7    12021   8464  12306   2383   1977  -1628       C  
+ATOM     60  CG  HIS A   7     119.634  13.303 -38.694  1.00101.46           C  
+ANISOU   60  CG  HIS A   7    14094  10603  13853   2201   2000  -1699       C  
+ATOM     61  ND1 HIS A   7     119.265  13.157 -40.015  1.00 99.83           N  
+ANISOU   61  ND1 HIS A   7    14076  10384  13472   2153   2049  -1976       N  
+ATOM     62  CD2 HIS A   7     119.401  14.600 -38.379  1.00 89.60           C  
+ANISOU   62  CD2 HIS A   7    12588   9342  12114   2078   1988  -1527       C  
+ATOM     63  CE1 HIS A   7     118.823  14.314 -40.476  1.00 97.86           C  
+ANISOU   63  CE1 HIS A   7    13944  10363  12875   2029   2062  -1939       C  
+ATOM     64  NE2 HIS A   7     118.896  15.206 -39.503  1.00 93.14           N  
+ANISOU   64  NE2 HIS A   7    13235   9899  12253   1975   2035  -1667       N  
+END                                                                             
index 08a7ac3..6582b12 100644 (file)
@@ -1,53 +1,13 @@
 CLUSTAL
 
-FER_CAPAA/1-97     -----------------------------------------------------------A
-FER_CAPAN/1-144    MA------SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALFGLKS-A--NGGKVTCMA
-FER1_SOLLC/1-144   MA------SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA
-Q93XJ9_SOLTU/1-144 MA------SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA
-FER1_PEA/1-149     MATT---PALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFLGLKT-SLKRGDLAVAMA
-Q7XA98_TRIPR/1-152 MATT---PALYGTAVSTSFMRRQPVPMSV-ATTTTTKAFPSGFGLKSVSTKRGDLAVAMA
-FER1_MESCR/1-148   MAAT--TAALSGATMSTAFAPK--TPPMTAALPTNVGR--ALFGLKS-SASR-GRVTAMA
-FER1_SPIOL/1-147   MAAT--TTTMMG--MATTFVPKPQAPPMMAALPSNTGR--SLFGLKT-GSR--GGRMTMA
-FER3_RAPSA/1-96    -----------------------------------------------------------A
-FER1_ARATH/1-148   MAST----ALSSAIVGTSFIRRSPAPISLRSLPSANTQ--SLFGLKS-GTARGGRVTAMA
-FER_BRANA/1-96     -----------------------------------------------------------A
-FER2_ARATH/1-148   MAST----ALSSAIVSTSFLRRQQTPISLRSLPFANTQ--SLFGLKS-STARGGRVTAMA
-Q93Z60_ARATH/1-118 MAST----ALSSAIVSTSFLRRQQTPISLRSLPFANTQ--SLFGLKS-STARGGRVTAMA
-FER1_MAIZE/1-150   MATVLGSPRAPAFFFSSSSLRAAPAPTAV--ALPAAKV--GIMGRSA-SSRR--RLRAQA
-O80429_MAIZE/1-140 MAAT---------ALSMSILR---APPPCFSSPLRLRV--AVAKPLA-APMRRQLLRAQA
-1A70|/1-97         -----------------------------------------------------------A
-
-FER_CAPAA/1-97     SYKVKLITPDGPIEFDCPDDVYILDQAEEAGHDLPYSCRAGSCSSCAGKIAGGAVDQTDG
-FER_CAPAN/1-144    SYKVKLITPDGPIEFDCPDNVYILDQAEEAGHDLPYSCRAGSCSSCAGKIAGGAVDQTDG
-FER1_SOLLC/1-144   SYKVKLITPEGPIEFECPDDVYILDQAEEEGHDLPYSCRAGSCSSCAGKVTAGSVDQSDG
-Q93XJ9_SOLTU/1-144 SYKVKLITPDGPIEFECPDDVYILDQAEEEGHDLPYSCRAGSCSSCAGKVTAGTVDQSDG
-FER1_PEA/1-149     SYKVKLVTPDGTQEFECPSDVYILDHAEEVGIDLPYSCRAGSCSSCAGKVVGGEVDQSDG
-Q7XA98_TRIPR/1-152 TYKVKLITPEGPQEFDCPDDVYILDHAEEVGIELPYSCRAGSCSSCAGKVVNGNVNQEDG
-FER1_MESCR/1-148   AYKVTLVTPEGKQELECPDDVYILDAAEEAGIDLPYSCRAGSCSSCAGKVTSGSVNQDDG
-FER1_SPIOL/1-147   AYKVTLVTPTGNVEFQCPDDVYILDAAEEEGIDLPYSCRAGSCSSCAGKLKTGSLNQDDQ
-FER3_RAPSA/1-96    TYKVKFITPEGEQEVECDDDVYVLDAAEEAGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQ
-FER1_ARATH/1-148   TYKVKFITPEGELEVECDDDVYVLDAAEEAGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQ
-FER_BRANA/1-96     TYKVKFITPEGEQEVECDDDVYVLDAAEEAGIDLPYSCRAGSCSSCAGKVVSGFVDQSDE
-FER2_ARATH/1-148   TYKVKFITPEGEQEVECEEDVYVLDAAEEAGLDLPYSCRAGSCSSCAGKVVSGSIDQSDQ
-Q93Z60_ARATH/1-118 TYKVKFITPEGEQEVECEEDVYVLDAAEEAGLDLPYSCRAGSCSSCAGKVVSGSIDQSDQ
-FER1_MAIZE/1-150   TYNVKLITPEGEVELQVPDDVYILDQAEEDGIDLPYSCRAGSCSSCAGKVVSGSVDQSDQ
-O80429_MAIZE/1-140 TYNVKLITPEGEVELQVPDDVYILDFAEEEGIDLPFSCRAGSCSSCAGKVVSGSVDQSDQ
-1A70|/1-97         AYKVTLVTPTGNVEFQCPDDVYILDAAEEEGIDLPYSCRAGSCSSCAGKLKTGSLNQDDQ
-
-FER_CAPAA/1-97     NFLDDDQLEEGWVLTCVAYPQSDVTIETHKEAELVG-
-FER_CAPAN/1-144    NFLDDDQLEEGWVLTCVAYPQSDVTIETHKEAELVG-
-FER1_SOLLC/1-144   NFLDEDQEAAGFVLTCVAYPKGDVTIETHKEEELTA-
-Q93XJ9_SOLTU/1-144 KFLDDDQEAAGFVLTCVAYPKCDVTIETHKEEELTA-
-FER1_PEA/1-149     SFLDDEQIEAGFVLTCVAYPTSDVVIETHKEEDLTA-
-Q7XA98_TRIPR/1-152 SFLDDEQIEGGWVLTCVAFPTSDVTIETHKEEELTA-
-FER1_MESCR/1-148   SFLDDDQIKEGWVLTCVAYPTGDVTIETHKEEELTA-
-FER1_SPIOL/1-147   SFLDDDQIDEGWVLTCAAYPVSDVTIETHKEEELTA-
-FER3_RAPSA/1-96    SFLDDDQIAEGFVLTCAAYPTSDVTIETHREEDMV--
-FER1_ARATH/1-148   SFLDDEQIGEGFVLTCAAYPTSDVTIETHKEEDIV--
-FER_BRANA/1-96     SFLDDDQIAEGFVLTCAAYPTSDVTIETHKEEELV--
-FER2_ARATH/1-148   SFLDDEQMSEGYVLTCVAYPTSDVVIETHKEEAIM--
-Q93Z60_ARATH/1-118 SFLDD--------------------------------
-FER1_MAIZE/1-150   SYLDDGQIADGWVLTCHAYPTSDVVIETHKEEELTGA
-O80429_MAIZE/1-140 SFLNDNQVADGWVLTCAAYPTSDVVIETHKEDDLL--
-1A70|/1-97         SFLDDDQIDEGWVLTCAAYPVSDVTIETHKKEELTA
-
+FER_CAPAA/1-97     -----------------------------------------------------------A 1
+FER_CAPAN/1-144    MA------SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALFGLKS-A--NGGKVTCMA 48
+FER1_SOLLC/1-144   MA------SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA 48
+Q93XJ9_SOLTU/1-144 MA------SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA 48
+FER1_PEA/1-149     MATT---PALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFLGLKT-SLKRGDLAVAMA 53
+
+FER_CAPAA/1-97     SYKVKLI 8
+FER_CAPAN/1-144    SYKVKLI 55
+FER1_SOLLC/1-144   SYKVKLI 55
+Q93XJ9_SOLTU/1-144 SYKVKLI 55
+FER1_PEA/1-149     SYKVKLV 60
index 8e4961f..e4bf12c 100755 (executable)
@@ -22,7 +22,7 @@
    <mapID target="home" url="html/index.html" />
    
    <mapID target="new" url="html/whatsNew.html"/>
-   <mapID target="release" url="html/releases.html#Jalview.2.10.2b1"/>
+   <mapID target="release" url="html/releases.html#Jalview.$$Version-Rel$$"/>
    <mapID target="alannotation" url="html/features/annotation.html"/>
    <mapID target="keys" url="html/keys.html"/>
    <mapID target="newkeys" url="html/features/newkeystrokes.html"/>
@@ -54,6 +54,7 @@
    <mapID target="pdbmcviewer" url="html/features/pdbviewer.html"/>
    <mapID target="pdbjmol" url="html/features/jmol.html"/>
    <mapID target="chimera" url="html/features/chimera.html"/>
+   <mapID target="chimera.annotxfer" url="html/features/chimera.html#annotxfer"/>
    <mapID target="varna" url="html/features/varna.html"/>
    <mapID target="xsspannotation" url="html/features/xsspannotation.html"/>
    <mapID target="preferences" url="html/features/preferences.html"/>     
index 4636ea3..b218b88 100755 (executable)
        <tocitem text="Jalview Documentation" target="home" expand="true">
                        <tocitem text="What's new" target="new" expand="true">
                                <tocitem text="Latest Release Notes" target="release"/>
-        <tocitem text="Calculations Dialog" target="calcs.dialog"/>
-                               <tocitem text="Groovy Features Counter example" target="groovy.featurescounter"/>
-                               <tocitem text="Custom Colourschemes in Groovy" target="groovy.colours"/>
-                               <tocitem text="Omit hidden regions in Overview" target="overview"/>
-                               <tocitem text="Show gaps as grey in overview" target="overviewprefs"/>
-                               <tocitem text="identifers.org for URL Links" target="linksprefs" />
-                               <tocitem text="New features in Split Frame View" target="splitframe.mirrorfonts" />
+                               <tocitem text="Structure Chooser" target="pdbchooser"/>
+                               <tocitem text="Chimera Annotation Exchange" target="chimera.annotxfer"/>
                </tocitem>
                
                <tocitem text="Editing Alignments" target="edit" />
index bb80b84..1090253 100755 (executable)
   <p>
     Gap open : 12 <br> Gap extend : 2
   </p>
-  <p>When you select the pairwise alignment option a new window will
-    come up which will display the alignments in a text format as they
-    are calculated. Also displayed is information about the alignment
-    such as alignment score, length and percentage identity between the
+  <p>When you select the pairwise alignment option, a new window
+    will come up which displays the alignments in a text format, for
+    example:</p>
+  <p>
+  <pre>
+    FER1_SPIOL/5-13 TTMMGMAT<br />
+                    |. .. ||<br />
+    FER1_MESCR/5-15 TAALSGAT
+    </pre>
+  shows the aligned sequences, where '|' links identical residues, and
+  (for peptide) '.' links residues that have a positive PAM250 score.
+  <p>The window also shows information about the alignment such as
+    alignment score, length and percentage identity between the
     sequences.</p>
-  <p>&nbsp;</p>
+  <p>A button is also provided to allow you to view the sequences as
+    an alignment.</p>
 </body>
 </html>
index aeb461a..a0412d0 100755 (executable)
@@ -46,8 +46,7 @@
     <li><p>
         <strong>Sort by Pairwise Identity</strong>
       </p>
-      <p>Places pairs of sequences together that align with the
-        greatest fraction of conserved residues.</p>
+      <p>Sorts sequences in the selection or alignment according to percent identity with respect to the first sequence in the view.</p>
       <p></li>
     <li><p>
         <strong>Sort by Tree Order</strong>
index 68ac465..e1227de 100644 (file)
             structure in the alignment. The regions used to calculate
             the superposition will be highlighted using the 'Cartoon'
             rendering style, and the remaining data shown as a chain
-            trace.<br/><br/>
+            trace.<br />
+          <br />
         </em></li>
-        <li><strong><a name="experimental">EXPERIMENTAL FEATURES</a></strong><br/>
-          <em>
-            These are only available if the <strong>Tools&#8594;Enable
-            Experimental Features</strong> option is enabled. (Since Jalview 2.10.2)</em>
-          <ul>
-            <li><strong>Write Jalview features</strong><br /> <em>Selecting
-                this option will create new residue attributes for any
-                features currently visible in the associated alignment
-                views, allowing those positions to be selected and
-                analysed with via Chimera's 'Render by Attribute' tool
-                (found in the Tools submenu called Structure Analysis).<br />
-                <br />If you use this option, please remember to select
-                the <em>Refresh Menus</em> option in Chimera's Render by
-                Attribute dialog box in order to see the attributes
-                derived from Jalview sequence features.
-            </em><br />
-            <a href="https://issues.jalview.org/browse/JAL-2295">View
-                this function's issue in Jalview's bug tracker</a></li>
-            <li><strong>Fetch Chimera Attributes</strong><br /> <em>This
-                submenu lists available Chimera residue attributes that
-                can be imported as Jalview features on associated
-                sequences.<br />This is particularly useful for
-                transferring quantitative positional annotation. For
-                example, structure similarity for an alignment can be
-                visualised by transferring the local RMSD attributes
-                generated by Chimera's Match->Align tool onto aligned
-                sequences and displayed with a <a
-                href="featureschemes.html">Graduated feature colour
-                  scheme</a>.
-            </em><a href="https://issues.jalview.org/browse/JAL-2296">View
-                this function's issue in Jalview's bug tracker</a></li>
-          </ul></li>
-        <li><strong>Help<br>
+        <li><a name="annotxfer"><strong>Write Jalview
+              features</strong></a><br /> <em>Selecting this option will create
+            new residue attributes for any features currently visible in
+            the associated alignment views, allowing those positions to
+            be selected and analysed with via Chimera's 'Render by
+            Attribute' tool (found in the Tools submenu called Structure
+            Analysis).<br /> <br />If you use this option, please
+            remember to select the <em>Refresh Menus</em> option in
+            Chimera's Render by Attribute dialog box in order to see the
+            attributes derived from Jalview sequence features.
+        </em></li>
+        <li><strong>Fetch Chimera Attributes</strong><br /> <em>This
+            submenu lists available Chimera residue attributes that can
+            be imported as Jalview features on associated sequences.<br />This
+            is particularly useful for transferring quantitative
+            positional annotation. For example, structure similarity for
+            an alignment can be visualised by transferring the local
+            RMSD attributes generated by Chimera's Match->Align tool
+            onto aligned sequences and displayed with a <a
+            href="featureschemes.html">Graduated feature colour
+              scheme</a>. </li>
+      </ul></li>
+    <li><strong>Help<br>
     </strong>
       <ul>
         <li><strong>Chimera Help<br>
index 0cd6168..ac2489b 100644 (file)
 </p> -->
   <p>
     <a name="align"><strong>Superposing structures based on
-        their aligned sequences</strong></a><br> If several structures are
-    available on the alignment, you may add additional structures to an
-    existing Jmol view by selecting their entry in the appropriate
-    pop-up menu. Jalview will ask you if you wish to add the structure
-    to the existing alignment, and if you do, it will import and
-    superimpose the new PDB file using the corresponding positions from
-    the alignment. If the alignment is subsequently edited, you can use
-    the <a href="#sAlign"><em>Jmol&#8594;Align</em></a> menu option from
-    the menu bar of the structure view window to superpose the
-    structures using the updated alignment.<br> <em>Sequence
+        their aligned sequences</strong></a><br> If several structures are shown
+    in a view, you can superimpose them using the corresponding
+    positions from the alignment via the <a href="#sAlign"><em>Jmol&#8594;Align</em></a>
+    menu option from the menu bar of the structure view window. <br> <em>Sequence
       based structure superposition was added in Jalview 2.6</em>
   </p>
   <p>
index 97a779a..2081a3d 100644 (file)
Binary files a/help/html/features/pdbseqfetcher.png and b/help/html/features/pdbseqfetcher.png differ
index 2962ba6..bb63bed 100644 (file)
@@ -37,7 +37,7 @@
   <p>
     To open the PDB Sequence Fetcher, select PDB as the database from
     any <a href="seqfetch.html">Sequence Fetcher</a> dialog (opened <em>via</em>
-    <strong>&quot;File &#8594;Fetch Sequences&quot;</strong>).
+    <strong>&quot;File &#8594;Fetch Sequences&quot;</strong>). 
   </p>
   <img src="pdbseqfetcher.png" align="left"
     alt="PDB sequence fetcher (introduced in Jalview 2.9)" />
   <p>
     <strong>Searching the PDB Database</strong>
   </p>
+  <p>To search the PDB, begin typing in the text box. If the
+    'autosearch' checkbox is enabled, then the results of your query
+    will be automatically updated and shown in the search results tab;
+    otherwise, press return to update the results. To access previous
+    searches, press the down-arrow or click the drop down menu icon at
+    the side of the search box. If you just want to paste in a list of
+    IDs, the 'Retrieve IDs' tab provides a batch-retrieval interface.</p>
   <p>
-    To search the PDB, begin typing in the text box. The results of your
-    query are shown in the search results tab, which updates every time
-    you type in the search text box. You can sort results according to
-    the displayed columns, and select entries with the mouse and
-    keyboard. Once you have selected one or more entries, hit the <strong>OK</strong>
-    button to retrieve and view them in Jalview.
+    You can sort results according to the displayed columns, and select
+    entries with the mouse and keyboard. Once you have selected one or
+    more entries, hit the <strong>OK</strong> button to retrieve and
+    view them in Jalview.
   </p>
   <p>
   <ul>
@@ -64,9 +69,8 @@
       1xyz:A</li>
 
     <li><strong>Bulk PDB retrieval</strong><br>Multiple PDB
-      IDs can be specified by separating them with a semi-colon.<br />
-      e.g. 1xyz;2xyz;3xyz<br />Hitting Return or OK will automatically
-      fetch those IDs, like the default Sequence Fetcher interface.</li>
+      IDs can be specified for retrieval via the 
+      <strong>Retrieve IDs</strong> tab.</li>
 
     <li><strong>Wild card searching</strong><br>The following
       wild cards are supported by the EMBL-EBI PDBe query service:
index b29b66b..50f864b 100755 (executable)
     and PDB file association (if available). The Jalview id/start-end
     option is ignored if Modeller output is selected.
   <p>
-    <a name="editing"><strong>e&quot;Editinge&quot; Preferences tab</strong></a>
+    <a name="editing"><strong>&quot;Editing&quot; Preferences tab</strong></a>
   </p>
   <p>There are currently three options available which can be
     selected / deselected.</p>
index f551c50..4af0d53 100644 (file)
Binary files a/help/html/features/schooser_enter-id.png and b/help/html/features/schooser_enter-id.png differ
index ab69427..ca793fd 100644 (file)
Binary files a/help/html/features/schooser_main.png and b/help/html/features/schooser_main.png differ
index be1bd66..e1c07c1 100644 (file)
@@ -76,7 +76,7 @@
       or <strong>"View&#8594;Nucleotide"</strong> (in the protein panel)
       allows you to show or hide one or other of the linked alignment
       panels.</li>
-    <li>Panel heights are adjusted dragging the divider between
+    <li>Panel heights are adjusted by dragging the divider between
       them using the mouse</li>
     <li><a href="../menus/alwview.html"><strong>"View&#8594;New
           View / Expand Views / Gather Views"</strong></a> behave as for a normal
index fc71826..785c429 100644 (file)
 
 <body>
   <p>
-    <strong>Structure Chooser</strong>
+    <strong>Structure Chooser Dialog Box</strong>
   </p>
 
   <p>
-    The Structure Chooser interface allows you to interactively select
-    which PDB structures to view for the currently selected set of
+    The Structure Chooser allows you to select
+    3D structures to view for the currently selected set of
     sequences. It is opened by selecting the <strong>"3D
-      Structure Data.."</strong> option from the Sequence ID panel's <a
+      Structure Data..."</strong> option from the Sequence ID panel's <a
       href="../menus/popupMenu.html">pop-up menu</a>. The dialog
     provides:
   </p>
   <p>
     <strong>Selecting and Viewing Structures</strong>
   </p>
+  <p>The drop-down menu offers different options for structure
+    discovery; the 'Cached' view is shown automatically if existing
+    structure data has been imported for the selected sequences, and if
+    none is available, the import PDB/mmCIF file options are shown.</p>
   <p>
     Once one or more structures have been selected, pressing the <strong>View</strong>
-    button will import them into <a
+    or <strong>Add</strong> button will import them <a
       href="viewingpdbs.html#afterviewbutton">a new or existing
-      structure view</a>.
+      structure view</a>. When multiple views are available, use the
+    drop-down menu to pick the target viewer for the structures.
   </p>
   <p>
     <strong>Automated discovery of structure data</strong>
     criteria (e.g. worst quality rather than best).</p>
   <p>
 
-    <img src="schooser_main.png" style="width: 464px; height: 369px;">
+    <img src="schooser_main.png" style="width: 499px; height: 437px;">
     <!-- <p><img src="schooser_config.png" style="width: 463px; height: 369px; ">
        <p><img src="schooser_drop-down.png" style="width: 464px; height: 368px; ">
        <p><img src="schooser_enter-id.png" style="width: 467px; height: 373px; ">
        <p><img src="schooser_from-file.png" style="width: 468px; height: 370px; ">
        <p><img src="schooser_cached.png"> -->
-    <br>The screenshot above shows the Structure Chooser interface
-    along with the meta-data of auto-discovered structures for the
-    sample alignment. If no structures were
-    auto-discovered, options for manually associating PDB records will be shown (see below).
-  <p>
+    <br>The screenshot above shows the Structure Chooser displayed after
+    selecting all the sequences in the Jalview example project. If no
+    structures were auto-discovered, options for manually associating
+    PDB records will be shown (see below).<p>
     <strong>Exploration of meta-data for available structures</strong>
   </p>
   <p>Information on each structure available is displayed in columns
     Columns' tab and tick the columns which you want to see.</p>
   <p>
     <img src="schooser_enter-id.png"
-      style="width: 464px; height: 369px;">
+      style="width: 464px; height: 173px;">
       <br/>
     <strong>Manual selection/association of PDB files with
       Sequences</strong>
index a592e8e..23b55fa 100644 (file)
Binary files a/help/html/features/uniprotseqfetcher.png and b/help/html/features/uniprotseqfetcher.png differ
index edd8995..4a64f52 100644 (file)
   <p>
     <strong>Searching the UniProt Database</strong>
   </p>
-  <p>
-    To search UniProt, simply begin typing in the text box. After a
-    short delay (about 1.5 seconds), results will be shown in the table
-    below. You can sort results by clicking on the displayed columns,
+  <p>To search UniProt, simply begin typing in the text box. If the
+    'autosearch' check box is enabled, then after a short delay (about
+    1.5 seconds), results will be shown in the table below. Results are
+    also updated whenever you press Enter, and you can access previous
+    searches by pressing the 'Down' arrow or clicking the drop-down menu
+    icon at the side of the search box.</p>
+  <p>You can sort results by clicking on the displayed columns,
     and select entries with the mouse or keyboard. Once you have
     selected one or more entries, hit the <strong>OK</strong> button to
     retrieve the sequences.
 
 
     <li><strong>Bulk UniProt record retrieval</strong><br> To
-      retrieve several uniprot accessions at once, first select <strong>UniProt
-        ID</strong> from the dropdown menu, then paste in the accession IDs as a
-      semi-colon separated list. (e.g. fila_human; mnt_human;
-      mnt_mouse).<br />Hitting Return or OK will automatically fetch
-      those IDs, like the default Sequence Fetcher interface.</li>
+      retrieve sequences for a list of Uniprot accessions, please enter
+      them via the 'Retrieve IDs' tab.</li>
 
     <li><strong><a name="text-search">Complex queries
           with the UniProt query Syntax</a></strong> The text box also allows complex
index 0fcbbf9..b1ad4ba 100755 (executable)
@@ -56,7 +56,7 @@
         <li><strong>Viewing Cached Structures</strong><br />If
           previously downloaded structures are available for your
           sequences, the structure chooser will automatically offer them
-          via the <strong>Cached PDB Entries</strong> view. If you wish
+          via the <strong>Cached Structures</strong> view. If you wish
           to download new structures, select one of the PDBe selection
           criteria from the drop-down menu.</li>
       </ul></li>
@@ -82,6 +82,7 @@
     provided it is installed and can be launched by Jalview. The default
     viewer can be configured in the <a href="preferences.html#structure">Structure
       tab</a> in the <strong>Tools&rarr;Preferences</strong> dialog box.
+  
   <p>
     Structure data imported into Jalview can also be processed to
     display secondary structure and temperature factor annotation. See
     for more information.
   </p>
   <p>
-    <strong><a name="afterviewbutton">After pressing the
-        'View' button in the Structure Chooser</a></strong><br /> The behaviour of
-    the 'View' button depends on the number of structures selected, and
-    whether structure views already exist for the selected structures or
-    aligned sequences.
+    <img src="schooser_viewbutton.png"
+      style="width: 465px; height: 81px" /><br/> <strong><a
+      name="afterviewbutton">Controlling where the new structures
+        will be shown</a></strong>
+        <br />The Structure Chooser offers several options
+    for viewing a structure. <br/><strong>New View</strong> will open a new
+    structure viewer for the selected structures, but if there are views
+    already open, you can select which one to use, and press the <strong>Add</strong>
+    button. Jalview can automatically superimpose new structures based
+    on the linked alignments - but if this is not desirable, simple
+    un-tick the <strong>Superpose Structures</strong> checkbox.
+
   </p>
-  <p>If multiple structures are selected, then Jalview will always
-    create a new structure view. The selected structures will be
-    imported into this view, and superposed with the matched positions
-    from the aligned sequences. A message in the structure viewer's
-    status bar will be shown if not enough aligned columns were
-    available to perform a superposition.</p>
   <p>
-    If a <strong>single</strong> PDB structure is selected, one of the
-    following will happen:
+    <em>Superposing structures</em><br/>Jalview superposes structures using
+    the visible portions of any associated sequence alignments. A
+    message in the structure viewer's status bar will be shown if not
+    enough aligned columns were available to perform a superposition.
   </p>
-
-  <ul>
-    <li>If no structures are open, then an interactive display of
-      the structure will be opened in a new window.</li>
-
-    <li>If another structure is already shown for the current
-      alignment, then you will be asked if you want to add and <a
-      href="jmol.html#align"></a> to the structure in the existing view.
-      (<em>new feature in Jalview 2.6</em>).
-    </li>
-
-    <li>If the structure is already shown, then you will be
-      prompted to associate the sequence with an existing view of the
-      selected structure. This is useful when working with multi-domain
-      or multi-chain PDB files.</li>
-
-    <li style="list-style: none">See the <a href="jmol.html">Jmol
-    </a> and <a href="chimera.html">Chimera</a> PDB viewer help pages for
-      more information about the display.
-    </li>
-  </ul>
-
+  <p>
+  See the <a href="jmol.html">Jmol
+    </a> and <a href="chimera.html">Chimera</a> help pages for
+      more information about their capabilities.</p>
+  
 
   <p>
     <strong>Retrieving sequences from the PDB</strong><br>You can
index a93ce4b..d716e33 100755 (executable)
@@ -86,7 +86,7 @@
             the <a href="../features/groovy.html">Groovy Console</a> for
             interactive scripting.
         </em><strong><br></strong></li>
-        <li><strong>Enable Experimental Features</strong> <em>Enable or disable <a href="../whatsNew.html#experimental">features still under development</a> in Jalview's user interface. This setting is remembered in your preferences.</em>
+        <!--         <li><strong>Enable Experimental Features</strong> <em>Enable or disable <a href="../whatsNew.html#experimental">features still under development</a> in Jalview's user interface. This setting is remembered in your preferences.</em> -->
 
       </ul></li>
     <li><strong>Vamsas</strong> <em>For more details, read the
index 5afe85c..3c1d73c 100755 (executable)
@@ -70,6 +70,494 @@ li:before {
     <tr>
       <td width="60" nowrap>
         <div align="center">
+          <strong><a name="Jalview.2.10.4b1">2.10.4b1</a><br />
+            <em>27/05/2018</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!-- JAL-1460 -->Windows File Shortcuts can be dragged
+              onto the Jalview Desktop
+            </li>
+          </ul>
+        </div></td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!-- JAL-2997 -->Clustal files with sequence positions in
+              right-hand column parsed correctly
+            </li>
+            <li>
+              <!-- JAL-2993 -->F2/Keyboard mode edits work when Overview
+              window has input focus
+            </li>
+            <li>
+              <!-- JAL-2991 -->Wrap view - export to SVG - IDs shown but
+              not alignment area in exported graphic
+            </li>
+            <li>
+              <!-- JAL-1460 -->Drag URL from chrome, firefox, IE to
+              Jalview desktop on Windows doesn't open file<br />
+            <em>Dragging the currently open URL and links from a page viewed in Firefox or Chrome on
+                Windows is now fully supported. If you are using Edge, only
+                links in the page can be dragged, and with Internet Explorer, only
+                the currently open URL in the browser can be dropped onto
+                Jalview.</em>
+                </li>
+            <li>
+              <!-- JAL-2992 -->
+            </li>
+          </ul>
+        </div></td>
+    </tr>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.10.4">2.10.4</a><br /> <em>10/05/2018</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!-- JAL-1847 JAL-2944 -->New Structure Chooser control
+              for disabling automatic superposition of multiple
+              structures and open structures in existing views
+            </li>
+            <li>
+              <!-- JAL-984 -->Mouse cursor changes to indicate Sequence
+              ID and annotation area margins can be click-dragged to
+              adjust them.
+            </li>
+            <li>
+              <!-- JAL-2885 -->Jalview uses HTTPS for Uniprot, Xfam and
+              Ensembl services
+            </li>
+            <li>
+              <!-- JAL-2759 -->Improved performance for large alignments
+              and lots of hidden columns
+            </li>
+            <li>
+              <!-- JAL-2593 -->Improved performance when rendering lots
+              of features (particularly when transparency is disabled)
+            </li>
+          </ul>
+          </div>
+      </td>
+      <td><div align="left">
+          <ul>
+            <li>
+              <!-- JAL-2899 -->Structure and Overview aren't updated
+              when Colour By Annotation threshold slider is adjusted
+            </li>
+            <li>
+              <!-- JAL-2778 -->Slow redraw when Overview panel shown
+              overlapping alignment panel
+            </li>
+            <li>
+              <!--  JAL-2929 -->Overview doesn't show end of unpadded
+              sequence as gaps
+            </li>
+            <li>
+              <!-- JAL-2789, JAL-2893 -->Cross-reference handling
+              improved: CDS not handled correctly if transcript has no
+              UTR
+            </li>
+            <li>
+              <!-- JAL-2321 -->Secondary structure and temperature
+              factor annotation not added to sequence when local PDB
+              file associated with it by drag'n'drop or structure
+              chooser
+            </li>
+            <li>
+              <!--  JAL-2984 -->Answering 'No' to PDB Autoassociate
+              dialog doesn't import PDB files dropped on an alignment
+            </li>
+            <li>
+              <!-- JAL-2666 -->Linked scrolling via protein horizontal
+              scroll bar doesn't work for some CDS/Protein views
+            </li>
+            <li>
+              <!-- JAL-2930 -->Trackpad scrolling is broken on OSX on
+              Java 1.8u153 onwards and Java 1.9u4+.
+            </li>
+            <li>
+              <!-- JAL-2924 -->Tooltip shouldn't be displayed for empty
+              columns in annotation row
+            </li>
+            <li>
+              <!-- JAL-2913 -->Preferences panel's ID Width control is not
+              honored in batch mode
+            </li>
+            <li>
+              <!-- JAL-2945 -->Linked sequence highlighting doesn't work
+              for structures added to existing Jmol view
+            </li>
+            <li>
+              <!-- JAL-2223 -->'View Mappings' includes duplicate
+              entries after importing project with multiple views
+            </li>
+            <li>
+              <!-- JAL-2781 JAL-2780 -->Viewing or annotating Uniprot
+              protein sequences via SIFTS from associated PDB entries
+              with negative residue numbers or missing residues fails
+            </li>
+            <li>
+              <!-- JAL-2952 -->Exception when shading sequence with negative
+              Temperature Factor values from annotated PDB files (e.g.
+              as generated by CONSURF)
+            </li>
+            <li>
+              <!-- JAL-2920 -->Uniprot 'sequence variant' features
+              tooltip doesn't include a text description of mutation
+            </li>
+            <li>
+              <!-- JAL-2922 -->Invert displayed features very slow when
+              structure and/or overview windows are also shown
+            </li>
+            <li>
+              <!-- JAL-2954 -->Selecting columns from highlighted regions
+              very slow for alignments with large numbers of sequences
+            </li>
+            <li>
+              <!-- JAL-2925 -->Copy Consensus fails for group consensus
+              with 'StringIndexOutOfBounds'
+            </li>
+            <li>
+              <!-- JAL-2976 -->VAqua(4) provided as fallback Look and Feel for OSX
+              platforms running Java 10
+            </li>
+            <li>
+              <!-- JAL-2960 -->Adding a structure to existing structure
+              view appears to do nothing because the view is hidden behind the alignment view
+            </li>
+          </ul>
+          <em>Applet</em>
+          <ul>
+            <li>
+              <!-- JAL-2926 -->Copy consensus sequence option in applet
+              should copy the group consensus when popup is opened on it
+            </li>
+          </ul>
+          <em>Batch Mode</em>
+          <ul>
+          <li>
+            <!-- JAL-2913 -->Fixed ID width preference is not respected
+          </li>
+          </ul>
+          <em>New Known Defects</em>
+          <ul>
+            <li>
+              <!-- JAL-2973 --> Exceptions occasionally raised when
+              editing a large alignment and overview is displayed
+            </li>
+            <li>
+              <!-- JAL-2974 -->'Overview updating' progress bar is shown
+              repeatedly after a series of edits even when the overview
+              is no longer reflecting updates
+            </li>
+            <li>
+              <!-- JAL-2946 -->'SIFTS Mapping Error' when viewing
+              structures for protein subsequence (if 'Trim Retrieved
+              Sequences' enabled) or Ensembl isoforms (Workaround in
+              2.10.4 is to fail back to N&amp;W mapping)
+            </li>
+          </ul>
+        </div>
+          </td>
+    </tr>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.10.3b1">2.10.3b1</a><br /> <em>24/1/2018</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <ul><li>Updated Certum Codesigning Certificate
+              (Valid till 30th November 2018)</li></ul></div></td>
+      <td><div align="left">
+          <em>Desktop</em><ul>
+          <ul>
+            <li><!-- JAL-2859-->Only one structure is loaded when several sequences and structures are selected for viewing/superposing</li>
+            <li><!-- JAL-2851-->Alignment doesn't appear to scroll vertically via trackpad and scrollwheel</li>
+            <li><!-- JAL-2842-->Jalview hangs if up/down arrows pressed in cursor mode when cursor lies in hidden region at start of alignment</li>
+            <li><!-- JAL-2827-->Helix annotation has 'notches' when scrolled into view if columns are hidden</li>
+            <li><!-- JAL-2740-->Annotation column filter can be slow to reset (ie after hitting cancel) for large numbers of hidden columns</li>
+            <li><!-- JAL-2849-->User preference for disabling inclusion of sequence limits when exporting as flat file has no effect</li>             
+            <li><!-- JAL-2679-->Reproducible cross-reference relationships when retrieving sequences from EnsemblGenomes</li>  
+          </ul>
+          </div>
+      </td>
+    </tr>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.10.3">2.10.3</a><br /> <em>17/11/2017</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em></em>
+          <ul>
+            <li>
+              <!-- JAL-2446 -->Faster and more efficient management and
+              rendering of sequence features
+            </li>
+            <li>
+              <!-- JAL 2523-->More reliable Ensembl fetching with HTTP
+              429 rate limit request hander
+            </li>
+            <li>
+              <!-- JAL-2773 -->Structure views don't get updated unless
+              their colours have changed
+            </li>
+            <li>
+              <!-- JAL-2495 -->All linked sequences are highlighted for
+              a structure mousover (Jmol) or selection (Chimera)
+            </li>
+            <li>
+              <!-- JAL-2790 -->'Cancel' button in progress bar for
+              JABAWS AACon, RNAAliFold and Disorder prediction jobs
+            </li>
+            <li>
+              <!-- JAL-2617 -->Stop codons are excluded in CDS/Protein
+              view from Ensembl locus cross-references
+            </li>
+            <li>
+              <!-- JAL-2685 -->Start/End limits are shown in Pairwise
+              Alignment report
+            </li>
+            <li>
+              <!-- JAL-2810 -->Sequence fetcher's Free text 'autosearch'
+              feature can be disabled
+            </li>
+            <li>
+              <!-- JAL-2810 -->Retrieve IDs tab added for UniProt and
+              PDB easier retrieval of sequences for lists of IDs
+            </li>
+            <li>
+              <!-- JAL-2758 -->Short names for sequences retrieved from
+              Uniprot
+            </li>
+          </ul>
+          <em>Scripting</em>
+          <ul>
+            <li>Groovy interpreter updated to 2.4.12</li>
+            <li>Example groovy script for generating a matrix of
+              percent identity scores for current alignment.</li>
+          </ul>
+          <em>Testing and Deployment</em>
+          <ul>
+            <li>
+              <!-- JAL-2727 -->Test to catch memory leaks in Jalview UI
+            </li>
+          </ul>
+        </div></td>
+      <td><div align="left">
+          <em>General</em>
+          <ul>
+            <li>
+              <!-- JAL-2643 -->Pressing tab after updating the colour
+              threshold text field doesn't trigger an update to the
+              alignment view
+            </li>
+            <li>
+              <!-- JAL-2682 -->Race condition when parsing sequence ID
+              strings in parallel
+            </li>
+            <li>
+              <!-- JAL-2608 -->Overview windows are also closed when
+              alignment window is closed
+            </li>
+            <li>
+              <!-- JAL-2548 -->Export of features doesn't always respect
+              group visibility
+            </li>
+            <li>
+              <!-- JAL-2831 -->Jumping from column 1 to column 100,000
+              takes a long time in Cursor mode
+            </li>
+          </ul>
+          <em>Desktop</em>
+          <ul>
+            <li>
+              <!-- JAL-2777 -->Structures with whitespace chainCode
+              cannot be viewed in Chimera
+            </li>
+            <li>
+              <!-- JAL-2728 -->Protein annotation panel too high in
+              CDS/Protein view
+            </li>
+            <li>
+              <!-- JAL-2757 -->Can't edit the query after the server
+              error warning icon is shown in Uniprot and PDB Free Text
+              Search Dialogs
+            </li>
+            <li>
+              <!-- JAL-2253 -->Slow EnsemblGenome ID lookup
+            </li>
+            <li>
+              <!-- JAL-2529 -->Revised Ensembl REST API CDNA query
+            </li>
+            <li>
+              <!-- JAL-2739 -->Hidden column marker in last column not
+              rendered when switching back from Wrapped to normal view
+            </li>
+            <li>
+              <!-- JAL-2768 -->Annotation display corrupted when
+              scrolling right in unwapped alignment view
+            </li>
+            <li>
+              <!-- JAL-2542 -->Existing features on subsequence
+              incorrectly relocated when full sequence retrieved from
+              database
+            </li>
+            <li>
+              <!-- JAL-2733 -->Last reported memory still shown when
+              Desktop->Show Memory is unticked (OSX only)
+            </li>
+            <li>
+              <!-- JAL-2658 -->Amend Features dialog doesn't allow
+              features of same type and group to be selected for
+              amending
+            </li>
+            <li>
+              <!-- JAL-2524 -->Jalview becomes sluggish in wide
+              alignments when hidden columns are present
+            </li>
+            <li>
+              <!-- JAL-2392 -->Jalview freezes when loading and
+              displaying several structures
+            </li>
+            <li>
+              <!-- JAL-2732 -->Black outlines left after resizing or
+              moving a window
+            </li>
+            <li>
+              <!-- JAL-1900,JAL-1625 -->Unable to minimise windows
+              within the Jalview desktop on OSX
+            </li>
+            <li>
+              <!-- JAL-2667 -->Mouse wheel doesn't scroll vertically
+              when in wrapped alignment mode
+            </li>
+            <li>
+              <!-- JAL-2636 -->Scale mark not shown when close to right
+              hand end of alignment
+            </li>
+            <li>
+              <!-- JAL-2684 -->Pairwise alignment of selected regions of
+              each selected sequence do not have correct start/end
+              positions
+            </li>
+            <li>
+              <!-- JAL-2793 -->Alignment ruler height set incorrectly
+              after canceling the Alignment Window's Font dialog
+            </li>
+            <li>
+              <!-- JAL-2036 -->Show cross-references not enabled after
+              restoring project until a new view is created
+            </li>
+            <li>
+              <!-- JAL-2756 -->Warning popup about use of SEQUENCE_ID in
+              URL links appears when only default EMBL-EBI link is
+              configured (since 2.10.2b2)
+            </li>
+            <li>
+              <!-- JAL-2775 -->Overview redraws whole window when box
+              position is adjusted
+            </li>
+            <li>
+              <!-- JAL-2225 -->Structure viewer doesn't map all chains
+              in a multi-chain structure when viewing alignment
+              involving more than one chain (since 2.10)
+            </li>
+            <li>
+              <!-- JAL-2811 -->Double residue highlights in cursor mode
+              if new selection moves alignment window
+            </li>
+            <li>
+              <!-- JAL-2837,JAL-2840 -->Alignment vanishes when using
+              arrow key in cursor mode to pass hidden column marker
+            </li>
+            <li>
+              <!-- JAL-2679 -->Ensembl Genomes example ID changed to one
+              that produces correctly annotated transcripts and products
+            </li>
+            <li>
+              <!-- JAL-2776 -->Toggling a feature group after first time
+              doesn't update associated structure view
+            </li>
+          </ul>
+          <em>Applet</em><br />
+          <ul>
+            <li>
+              <!-- JAL-2687 -->Concurrent modification exception when
+              closing alignment panel
+            </li>
+          </ul>
+          <em>BioJSON</em><br />
+          <ul>
+            <li>
+              <!-- JAL-2546 -->BioJSON export does not preserve
+              non-positional features
+            </li>
+          </ul>
+          <em>New Known Issues</em>
+          <ul>
+            <li>
+              <!-- JAL-2541 -->Delete/Cut selection doesn't relocate
+              sequence features correctly (for many previous versions of
+              Jalview)
+            </li>
+            <li>
+              <!-- JAL-2841 -->Cursor mode unexpectedly scrolls when
+              using cursor in wrapped panel other than top
+            </li>
+            <li>
+              <!-- JAL-2791 -->Select columns containing feature ignores
+              graduated colour threshold
+            </li>
+            <li>
+              <!-- JAL-2822,JAL-2823 -->Edit sequence operation doesn't
+              always preserve numbering and sequence features
+            </li>
+          </ul>
+          <em>Known Java 9 Issues</em>
+          <ul>
+            <li>
+              <!-- JAL-2902 -->Groovy Console very slow to open and is
+              not responsive when entering characters (Webstart, Java
+              9.01, OSX 10.10)
+            </li>
+          </ul>
+        </div></td>
+    </tr>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
+          <strong><a name="Jalview.2.10.2b2">2.10.2b2</a><br />
+            <em>2/10/2017</em></strong>
+        </div>
+      </td>
+      <td><div align="left">
+          <em>New features in Jalview Desktop</em>
+          <ul>
+            <li>
+              <!-- JAL-2748 -->Uniprot Sequence Fetcher now uses web API at uniprot.org 
+            </li>
+            <li>  <!-- JAL-2745 -->HTTPS used for all connections to ebi.ac.uk 
+            </li>
+          </ul>
+        </div></td>
+      <td><div align="left">
+        </div></td>
+    </tr>
+    <tr>
+      <td width="60" nowrap>
+        <div align="center">
           <strong><a name="Jalview.2.10.2b1">2.10.2b1</a><br />
             <em>7/9/2017</em></strong>
         </div>
@@ -147,7 +635,7 @@ li:before {
             </li>
           </ul>
         </div></td>
-    
+    </tr>
     <tr>
       <td width="60" nowrap>
         <div align="center">
@@ -1402,6 +1890,10 @@ li:before {
               after clicking on it to create new annotation for a
               column.
             </li>
+            <li>
+              <!-- JAL-1980 -->Null Pointer Exception raised when 
+              pressing Add on an orphaned cut'n'paste window.
+            </li>
             <!--  may exclude, this is an external service stability issue  JAL-1941 
             -- > RNA 3D structure not added via DSSR service</li> -->
           </ul>
index 090b695..0abd2a7 100755 (executable)
 </head>
 <body>
   <p>
-    <strong>Jalview 2.10.2b1 bugfix release</strong>
+    <strong>What's new in Jalview 2.10.4 ?</strong>
   </p>
   <p>
-    This is patch release for 2.10.2. See the <a
-      href="releases.html#Jalview.2.10.2b1">release notes</a> for full
-    details about the bugs addressed. This release also introduces
-    additional improvements to the overview panel, and patches for
-    several minor issues including the ability to correctly recover
-    cross-references for Uniprot protein sequences from Ensembl.
-  </p>
-  <p>
-    <strong>What's new in Jalview 2.10.2 ?</strong>
-  </p>
-  <p>
-    Version 2.10.2 was released in August 2017, and introduced new user
-    interface features, improved and more extensible tree and PCA
-    analysis, more robust 3D structure viewing with UCSF Chimera and an
-    updated service client for JABAWS. The full list of bug fixes and
-    new features can be found in the <a
-      href="releases.html#Jalview.2.10.2"> 2.10.2 Release Notes</a>, but
-    the highlights are below.
+    This is the May 2018 release of Jalview, and the last in the 2.10.x series. Jalview 2.10.4 includes:
   </p>
   <ul>
-    <li><strong>New dialog and faster and more
-        configurable Tree and PCA calculations</strong><br> Menu entries for
-      calculating PCA and different types of tree have been replaced by
-      a single <a href="calculations/calculations.html"><em>Calculations</em>
-        dialog box</a>. The underlying implementation for the PCA and tree
-      calculations have been made faster and more memory efficient.</li>
-    <li><strong>Extensible score models</strong><br />A new
-      framework has also been created for the score models used to
-      calculate distances between sequences and shade alignments. This
-      framework allows import of substitution matrices in NCBI and
-      AAIndex format.<br /> <strong>PCA Bug Fixes</strong>. Jalview's
-      implementation of PCA differed in its treatment of gaps and
-      non-standard residues. The BLOSUM62 matrix also included a typo
-      that affected results. See the <a
-      href="releases.html#2102scoremodelbugs">2.10.2 release note
-        about score model bugs</a> for details and how to reinstate legacy
-      behaviour.</li>
-    <li><strong>Update to JABAWS 2.2</strong><br />Jalview's
-      alignment, protein conservation analysis, and protein disorder and
-      RNA secondary structure prediction services are now provided by <a
-      href="http://www.compbio.dundee.ac.uk/jabaws">JABAWS 2.2</a>.
-      Several of the programs provided as JABAWS 2.2 services have been
-      updated, so their options and parameters have changed.</li>
-    <li><strong>URL linkouts to other bioinformatics
-        databases</strong><br />New preferences for <a
-      href="webServices/urllinks.html">opening web pages for
-        database cross-references</a> via the UK Elixir's EMBL-EBI's MIRIAM
-      database and identifiers.org services.</li>
-    <li><strong>Showing and hiding regions</strong> <br /> <a
-      href="menus/popupMenu.html#hideinserts">Hide insertions</a> in the
-      PopUp menu has changed its behaviour. Prior to 2.10.2, columns
-      were only shown or hidden according to gaps in the sequence under
-      the popup menu. Now, only columns that are gapped in all selected
-      sequences as well as the sequence under the popup menu are hidden,
-      and column visibility outside the selected region is left as is.
-      This makes it easy to filter insertions from the alignment view
-      (just select the region containing insertions to remove) without
-      affecting the rest of the hidden columns.</li>
-    <li><strong>Gap count - a.k.a. the Occupancy
-        Annotation Row</strong><br /> Another way to filter columns according to
-      the presence of gaps is to enable the <strong>Occupancy
-        Annotation</strong> row via Jalview's Preferences. This annotation row
-      shows a histogram of the number of aligned residues at each
-      column. The <a href="features/columnFilterByAnnotation.html">Select
-        By Annotation</a> dialog now also includes a percentage threshold
-      mode, to make it easy to filter alignments to show only those
-      columns with a particular fraction of aligned sequences.</li>
-    <li><strong>Recent search history for Find, PDBe and
-        Uniprot</strong><br />Easily repeat a previous search for <a
-      href="features/search.html#queryhistory">Find</a> and the free
-      text search system (for querying Uniprot and the PDBe).</li>
-    <li><strong>Improved Overview Window</strong><br />The <a
-      href="features/overview.html">alignment overview</a> is now easier
-      to use when working with alignments of more than 5000 rows and
-      columns, and features a new pop-up menu that allows hidden regions
-      to be excluded from the overview. It also works with CDS/Protein
-      alignments and MSA views in wrapped mode.</li>
-    <li><strong>3D Structure</strong><br />Jalview's communication
-      with UCSF Chimera has been made more robust, particularly when
-      working with many structures and long sequences. Regions in
-      structures that correspond to hidden regions in an alignment view
-      are now left un-coloured, making it easier to highlight specific
-      features in 3D. See below for <a href="#experimental">experimental
-        features for exchanging annotation between Chimera and Jalview.</a></li>
+    <li>Numerous efficiency improvements in the renderer and overview when working with large alignments with lots of hidden columns</li>
+    <li>Use of HTTPS when connecting to Uniprot, Ensembl and other EBI web services</li>
+    <li>Critical patches for running Jalview on OSX with Java 10</li>
+    <li>Easier adjustment of the Alignment ID panel and Annotation panel</li>
+    <li>Improved support for mapping between 3D Structures and Uniprot Protein Sequences</li>
+    <li>Improved support for discovering CDS and transcripts for Proteins and Ensembl gene IDs</li>
+    <li>New buttons on the Structure Chooser for adding structures
+      to an existing view, and disabling automatic superposition
+      according to linked alignments</li>
+    <li>Annotation transfer between Chimera and Jalview <em>(formerly only
+        available in 'Experimental' mode)</em></li>
   </ul>
   <p>
-    <strong>Scripting</strong><br />New <a
-      href="http://www.jalview.org/examples/groovy">groovy examples</a>
-    demonstrate Jalview 2.10.2 APIs for creation of data-driven
-    colourschemes, and custom alignment file handlers. The <a
-      href="groovy/featuresCounter.html">FeatureAnnotationWorker</a>
-    introduced in Jalview 2.10 has also been refactored to allow
-    efficient counting across multiple feature types. Please be aware
-    that feature counter scripts created for earlier versions will not
-    execute in Jalview 2.10.2.
-  </p>
-  <p>
-    <strong><a name="experimental">Experimental Features</a></strong>
-  </p>
-  <p>
-    This release of Jalview introduces an <em>Experimental Features</em>
-    option in the Jalview Desktop's <em>Tools</em> menu that allows you
-    to try out features that are still in development. To access the
-    experimental features below - first enable the <strong>Tools&#8594;Enable
-      Experimental Features</strong> option, and then restart Jalview.
+    The full list of bugs fixed in this release can be found in the <a href="releases.html#Jalview.2.10.4">2.10.4
+      Release Notes</a>. 
   </p>
-  <ul>
-    <li><em>Annotation transfer between Chimera and Jalview</em><br />Two
-      <a href="features/chimera.html#experimental">new entries in
-        the Chimera viewer's Chimera menu</a> allow positional annotation to
-      be exchanged between Chimera and Jalview.</li>
-  </ul>
-
 </body>
 </html>
diff --git a/lib/VAqua4.jar b/lib/VAqua4.jar
new file mode 100644 (file)
index 0000000..c1e7cfc
Binary files /dev/null and b/lib/VAqua4.jar differ
similarity index 62%
rename from lib/groovy-all-2.4.6-indy.jar
rename to lib/groovy-all-2.4.12-indy.jar
index 5f3d51c..bb246a3 100644 (file)
Binary files a/lib/groovy-all-2.4.6-indy.jar and b/lib/groovy-all-2.4.12-indy.jar differ
diff --git a/lib/htsjdk-2.12.0.jar b/lib/htsjdk-2.12.0.jar
new file mode 100644 (file)
index 0000000..1df12b2
Binary files /dev/null and b/lib/htsjdk-2.12.0.jar differ
diff --git a/resources/images/idwidth.gif b/resources/images/idwidth.gif
deleted file mode 100755 (executable)
index c1bd8cb..0000000
Binary files a/resources/images/idwidth.gif and /dev/null differ
index fe74476..ae5b0e7 100644 (file)
@@ -242,7 +242,6 @@ label.documentation = Documentation
 label.about = About...
 label.show_sequence_limits = Show Sequence Limits
 action.feature_settings = Feature Settings...
-label.feature_settings = Feature Settings
 label.all_columns = All Columns
 label.all_sequences = All Sequences
 label.selected_columns = Selected Columns 
@@ -267,6 +266,7 @@ label.use_rnaview = Use RNAView for secondary structure
 label.autoadd_secstr = Add secondary structure annotation to alignment
 label.autoadd_temp = Add Temperature Factor annotation to alignment
 label.structure_viewer = Default structure viewer
+label.double_click_to_browse = Double-click to browse for file
 label.chimera_path = Path to Chimera program
 label.chimera_path_tip = Jalview will first try any path entered here, else standard installation locations.<br>Double-click to browse for file.
 label.invalid_chimera_path = Chimera path not found or not executable
@@ -274,6 +274,7 @@ label.chimera_missing = Chimera structure viewer not found.<br/>Please enter the
 label.chimera_failed = Error opening Chimera - is it installed?\nCheck path in Preferences, Structure
 label.min_colour = Minimum Colour
 label.max_colour = Maximum Colour
+label.no_colour = No Colour
 label.use_original_colours = Use Original Colours
 label.threshold_minmax = Threshold is min/max
 label.represent_group_with = Represent Group with {0}
@@ -281,9 +282,9 @@ label.selection = Selection
 label.group_colour = Group Colour
 label.sequence = Sequence
 label.view_pdb_structure = View PDB Structure
-label.min = Min:
-label.max = Max:
-label.colour_by_label = Colour by label
+label.min_value = Min value
+label.max_value = Max value
+label.no_value = No value
 label.new_feature = New Feature
 label.match_case = Match Case
 label.view_alignment_editor = View in alignment editor
@@ -368,6 +369,8 @@ label.optimise_order = Optimise Order
 label.seq_sort_by_score = Sequence sort by Score
 label.load_colours = Load Colours
 label.save_colours = Save Colours
+label.load_colours_tooltip = Load feature colours and filters from file
+label.save_colours_tooltip = Save feature colours and filters to file
 label.fetch_das_features = Fetch DAS Features
 label.selected_database_to_fetch_from = Selected {0} database {1} to fetch from {2} 
 label.database_param = Database: {0}
@@ -400,10 +403,6 @@ label.view_name_original = Original
 label.enter_view_name = Enter View Name
 label.enter_label = Enter label
 label.enter_label_for_the_structure = Enter a label for the structure
-label.pdb_entry_is_already_displayed = {0} is already displayed.\nDo you want to re-use this viewer ?
-label.map_sequences_to_visible_window = Map Sequences to Visible Window: {0}
-label.add_pdbentry_to_view = Do you want to add {0} to the view called\n{1}\n
-label.align_to_existing_structure_view = Align to existing structure view
 label.pdb_entries_couldnt_be_retrieved = The following pdb entries could not be retrieved from the PDB\:\n{0}\nPlease retry, or try downloading them manually.
 label.couldnt_load_file = Couldn't load file
 label.couldnt_find_pdb_id_in_file = Couldn't find a PDB id in the file supplied. Please enter an Id to identify this structure.
@@ -490,6 +489,10 @@ label.settings_for_type = Settings for {0}
 label.view_full_application = View in Full Application
 label.load_associated_tree = Load Associated Tree...
 label.load_features_annotations = Load Features/Annotations...
+label.load_vcf = Load SNP variants from plain text or indexed VCF data
+label.load_vcf_file = Load VCF File
+label.searching_vcf = Loading VCF variants...
+label.added_vcf = Added {0} VCF variants to {1} sequence(s)
 label.export_features = Export Features...
 label.export_annotations = Export Annotations...
 label.to_upper_case = To Upper Case
@@ -528,7 +531,6 @@ label.threshold_feature_above_threshold = Above Threshold
 label.threshold_feature_below_threshold = Below Threshold
 label.adjust_threshold = Adjust threshold
 label.toggle_absolute_relative_display_threshold = Toggle between absolute and relative display threshold.
-label.display_features_same_type_different_label_using_different_colour = Display features of the same type with a different label using a different colour. (e.g. domain features)
 label.select_colour_minimum_value = Select Colour for Minimum Value
 label.select_colour_maximum_value = Select Colour for Maximum Value
 label.open_url_param = Open URL {0}
@@ -673,7 +675,8 @@ label.2d_rna_structure_line = 2D RNA {0} (alignment)
 label.2d_rna_sequence_name = 2D RNA - {0}
 label.edit_name_and_description_current_group = Edit name and description of current group
 label.from_file = From File
-label.enter_pdb_id = Enter PDB Id (or pdbid:chaincode)
+label.enter_pdb_id = Enter PDB Id
+label.enter_pdb_id_tip = Enter PDB Id (or pdbid:chaincode)
 label.text_colour = Text Colour...
 label.structure = Structure
 label.show_pdbstruct_dialog = 3D Structure Data...
@@ -779,7 +782,7 @@ label.pairwise_aligned_sequences = Pairwise Aligned Sequences
 label.original_data_for_params = Original Data for {0}
 label.points_for_params = Points for {0}
 label.transformed_points_for_params = Transformed points for {0}
-label.graduated_color_for_params = Graduated Feature Colour for {0}
+label.variable_color_for = Variable Feature Colour for {0}
 label.select_background_colour = Select Background Colour
 label.invalid_font = Invalid Font
 label.separate_multiple_accession_ids = Enter one or more accession IDs separated by a semi-colon ";"
@@ -866,7 +869,7 @@ label.msa_service_is_unknown = The Multiple Sequence Alignment Service named {0}
 label.service_called_is_not_seq_search_service = The Service called \n{0}\nis not a \nSequence Search Service\!
 label.seq_search_service_is_unknown = The Sequence Search Service named {0} is unknown
 label.feature_type = Feature Type
-label.display = Display
+label.show = Show
 label.service_url = Service URL
 label.copied_sequences = Copied sequences
 label.cut_sequences = Cut Sequences
@@ -1212,7 +1215,6 @@ label.pdb_sequence_fetcher = PDB Sequence Fetcher
 label.result = result
 label.results = results
 label.structure_chooser = Structure Chooser
-label.select = Select : 
 label.invert = Invert 
 label.select_pdb_file = Select PDB File
 info.select_filter_option = Select Filter Option/Manual Entry
@@ -1295,7 +1297,6 @@ label.database = Database
 label.urltooltip = Only one url, which must use a sequence id, can be selected for the 'On Click' option
 label.edit_sequence_url_link = Edit sequence URL link
 warn.name_cannot_be_duplicate = User-defined URL names must be unique and cannot be MIRIAM ids
-label.invalid_name = Invalid Name !
 label.output_seq_details = Output Sequence Details to list all database references
 label.urllinks = Links
 label.default_cache_size = Default Cache Size
@@ -1320,3 +1321,45 @@ label.select_hidden_colour = Select hidden colour
 label.overview = Overview
 label.reset_to_defaults = Reset to defaults
 label.oview_calc = Recalculating overview...
+label.feature_details = Feature details
+label.matchCondition_contains = Contains
+label.matchCondition_notcontains = Does not contain
+label.matchCondition_matches = Matches
+label.matchCondition_notmatches = Does not match
+label.matchCondition_present = Is present
+label.matchCondition_notpresent = Is not present
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
+label.numeric_required = The value should be numeric
+label.filter = Filter
+label.filters = Filters
+label.join_conditions = Join conditions with
+label.score = Score
+label.colour_by_label = Colour by label
+label.variable_colour = Variable colour...
+label.select_colour = Select colour
+option.enable_disable_autosearch = When ticked, search is performed automatically
+option.autosearch = Autosearch
+label.retrieve_ids = Retrieve IDs
+label.display_settings_for = Display settings for {0} features
+label.simple = Simple
+label.simple_colour = Simple Colour
+label.colour_by_text = Colour by text
+label.graduated_colour = Graduated Colour
+label.by_text_of = By text of
+label.by_range_of = By range of
+label.filters_tooltip = Click to set or amend filters
+label.or = Or
+label.and = And
+label.sequence_feature_colours = Sequence Feature Colours
+label.best_quality = Best Quality
+label.best_resolution = Best Resolution
+label.most_protein_chain = Most Protein Chain
+label.most_bound_molecules = Most Bound Molecules
+label.most_polymer_residues = Most Polymer Residues
+label.cached_structures = Cached Structures
+label.free_text_search = Free Text Search
index f40f31d..555977d 100644 (file)
@@ -226,7 +226,6 @@ label.automatic_scrolling = Desplazamiento autom
 label.documentation = Documentación
 label.about = Acerca de...
 label.show_sequence_limits = Mostrar los límites de la secuencia
-label.feature_settings = Ajustar funciones...
 label.all_columns = Todas las columnas
 label.all_sequences = Todas las secuencias
 label.selected_columns = Columnas seleccionadas
@@ -243,6 +242,7 @@ label.apply_all_groups = Aplicar a todos los grupos
 label.autocalculated_annotation = Anotación autocalculada
 label.min_colour = Color mínimo
 label.max_colour = Color máximo
+label.no_colour = Sin color
 label.use_original_colours = Usar colores originales
 label.threshold_minmax = El umbral es mín/máx
 label.represent_group_with = Representar al grupo con
@@ -250,8 +250,9 @@ label.selection = Seleccionar
 label.group_colour = Color del grupo
 label.sequence = Secuencia
 label.view_pdb_structure = Ver estructura PDB
-label.min = Mín:
-label.max = Máx:
+label.max_value = Valor máximo
+label.min_value = Valor mínimo
+label.no_value = Sin valor
 label.colour_by_label = Color por etiquetas
 label.new_feature = Nueva función
 label.match_case = Hacer corresponder mayúsculas y minúsculas
@@ -336,6 +337,8 @@ label.optimise_order = Optimizar orden
 label.seq_sort_by_score = Ordenar las secuencias por puntuación
 label.load_colours = Cargar colores
 label.save_colours = Guardar colores
+label.load_colours_tooltip = Cargar colores y filtros desde fichero
+label.save_colours_tooltip = Guardar colores y filtros en fichero
 label.fetch_das_features = Recuperar funciones DAS
 label.selected_database_to_fetch_from = Seleccionada {0} Base de datos {1} para buscar de {2} 
 label.database_param = Base de datos: {0}
@@ -367,10 +370,6 @@ label.ignore_unmatched_dropped_files = Ignorar los ficheros sin coincidencias?
 label.enter_view_name = Introduzca un nombre para la vista
 label.enter_label = Introducir etiqueta
 label.enter_label_for_the_structure = Introducir una etiqueta para la estructura
-label.pdb_entry_is_already_displayed = {0} Ya est\u00E1 mostrado.\nQuieres volver a usar este visor?
-label.map_sequences_to_visible_window = Mapa de secuencias en ventana visible: {0}
-label.add_pdbentry_to_view = Quieres a\u00F1adir {0} a la vista llamada\n{1}\n
-label.align_to_existing_structure_view = Alinear a una estructura ya existente
 label.pdb_entries_couldnt_be_retrieved = Las siguientes entradas pdb no pueden ser extra\u00EDdas del PDB\:\n{0}\nPor favor, prueba descarg\u00E1ndolas manualmente.
 label.couldnt_load_file = No se pudo cargar el fichero
 label.couldnt_find_pdb_id_in_file = No se pudo encontrar un Id PDB en el fichero suministrado. Por favor, introduzca un Id para identificar esta estructura.
@@ -456,6 +455,10 @@ label.settings_for_type = Ajustes para {0}
 label.view_full_application = Ver en la aplicación completa 
 label.load_associated_tree = Cargar Ã¡rbol asociado ...
 label.load_features_annotations = Cargar características/anotaciones ...
+label.load_vcf = Cargar variantes SNP desde fichero VCF texto o tab-indexado
+label.load_vcf_file = Cargar fichero VCF
+label.searching_vcf = Cargando variantes VCF...
+label.added_vcf= {0} variantes VCF añadidas a {1} secuencia(s)
 label.export_features = Exportar características...
 label.export_annotations = Exportar anotaciones ...
 label.to_upper_case = Pasar a mayúsculas
@@ -489,7 +492,6 @@ label.threshold_feature_above_threshold = Por encima del umbral
 label.threshold_feature_below_threshold = Por debajo del umbral
 label.adjust_threshold = Ajustar umbral
 label.toggle_absolute_relative_display_threshold = Cambiar entre mostrar el umbral absoluto y el relativo.
-label.display_features_same_type_different_label_using_different_colour = Mostrar las características del mismo tipo con una etiqueta diferente y empleando un color distinto (p.e. características del dominio)
 label.select_colour_minimum_value = Seleccionar el color para el valor mínimo
 label.select_colour_maximum_value = Seleccionar el color para el valor máximo
 label.open_url_param = Abrir URL {0}
@@ -626,6 +628,7 @@ label.2d_rna_sequence_name = 2D RNA - {0}
 label.edit_name_and_description_current_group = Editar el nombre y la descripción del grupo actual
 label.from_file = desde fichero
 label.enter_pdb_id = Introducir PDB Id
+label.enter_pdb_id_tip = Introducir PDB Id (o pdbid:chaincode)
 label.text_colour = Color de texto...
 label.structure = Estructura
 label.create_sequence_details_report_annotation_for = Anotación para {0}
@@ -708,7 +711,7 @@ label.pairwise_aligned_sequences = Secuencias alineadas a pares
 label.original_data_for_params = Datos originales de {0}
 label.points_for_params = Puntos de {0}
 label.transformed_points_for_params = Puntos transformados de {0}
-label.graduated_color_for_params = Color graduado para la característica de {0}
+label.variable_color_for = Color variable para la característica de {0}
 label.select_background_colour = Seleccionar color de fondo
 label.invalid_font = Fuente no válida
 label.separate_multiple_accession_ids = Separar los accession id con un punto y coma ";"
@@ -791,7 +794,7 @@ label.msa_service_is_unknown = El Servicio de Alineamiento M
 label.service_called_is_not_seq_search_service = El Servicio llamando \n{0}\nno es un \nServicio de B\u00FAsqueda de Secuencias\!
 label.seq_search_service_is_unknown = El Servicio de Búsqueda de Sencuencias llamado {0} es desconocido
 label.feature_type = Tipo de característisca
-label.display = Representación
+label.show = Mostrar
 label.service_url = URL del servicio
 label.copied_sequences = Secuencias copiadas
 label.cut_sequences = Cortar secuencias
@@ -1171,12 +1174,12 @@ label.structures_filter=Filtro de Estructuras
 label.scale_protein_to_cdna=Adaptar proteína a cDNA
 label.scale_protein_to_cdna_tip=Hacer a los residuos de proteínas de la misma anchura que los codones en ventanas divididas
 status.loading_cached_pdb_entries=Cargando Entradas PDB en Caché
-label.select=Seleccionar :
 label.select_by_annotation=Seleccionar/Ocultar Columnas por Anotación
 action.select_by_annotation=Seleccionar/Ocultar Columnas por Anotación...
 action.export_features=Exportar Características
 error.invalid_regex=Expresión regular inválida
 label.autoadd_temp=Añadir anotación factor de temperatura al alineamiento
+label.double_click_to_browse = Haga doble clic para buscar fichero 
 label.chimera_path_tip=Jalview intentará primero las rutas introducidas aquí, Y si no las rutas usuales de instalación
 label.structure_chooser=Selector de Estructuras
 label.structure_chooser_manual_association=Selector de Estructuras - asociación manual
@@ -1223,13 +1226,13 @@ exception.resource_not_be_found=El recurso solicitado no se ha encontrado
 label.aacon_calculations=cálculos AACon
 label.pdb_web-service_error=Error de servicio web PDB
 exception.unable_to_detect_internet_connection=Jalview no puede detectar una conexión a Internet
-label.chimera_path=Ruta de acceso a programa Chimera
+label.chimera_path=Ruta de acceso a Chimera
 warn.delete_all=<html>Borrar todas las secuencias cerrará la ventana del alineamiento.<br>Confirmar o Cancelar.
 label.select_all=Seleccionar Todos
 label.alpha_helix=Hélice Alfa
 label.chimera_help=Ayuda para Chimera
 label.find_tip=Buscar alineamiento, selección o IDs de secuencia para una subsecuencia (sin huecos)
-label.structure_viewer=Visualizador de estructura por defecto
+label.structure_viewer=Visualizador por defecto
 label.embbed_biojson=Incrustar BioJSON al exportar HTML
 label.transparency_tip=Ajustar la transparencia a "ver a través" los colores de las características.
 label.choose_annotations=Escoja anotaciones
@@ -1295,7 +1298,6 @@ label.database = Base de datos
 label.urltooltip = Sólo una url, que debe usar una id de secuencia, puede ser seleccionada en la opción 'On Click'
 label.edit_sequence_url_link = Editar link de secuencia URL
 warn.name_cannot_be_duplicate = Los nombres URL definidos por el usuario deben ser Ãºnicos y no pueden ser ids de MIRIAM
-label.invalid_name = Nombre inválido !
 label.output_seq_details = Seleccionar Detalles de la secuencia para ver todas
 label.urllinks = Enlaces
 label.default_cache_size = Tamaño del caché por defecto
@@ -1320,3 +1322,45 @@ label.select_hidden_colour = Seleccionar color de las regiones ocultas
 label.overview = Resumen
 label.reset_to_defaults = Restablecen a los predeterminados
 label.oview_calc = Recalculando resumen
+label.feature_details = Detalles de característica 
+label.matchCondition_contains = Contiene
+label.matchCondition_notcontains = No contiene
+label.matchCondition_matches = Es igual a
+label.matchCondition_notmatches = No es igual a
+label.matchCondition_present = Está presente
+label.matchCondition_notpresent = No está presente
+label.matchCondition_eq = =
+label.matchCondition_ne = not =
+label.matchCondition_lt = <
+label.matchCondition_le = <=
+label.matchCondition_gt = >
+label.matchCondition_ge = >=
+label.numeric_required = Valor numérico requerido
+label.filter = Filtro
+label.filters = Filtros
+label.join_conditions = Combinar condiciones con
+label.score = Puntuación
+label.colour_by_label = Colorear por texto
+label.variable_colour = Color variable...
+label.select_colour = Seleccionar color
+option.enable_disable_autosearch = Marcar para buscar automáticamente
+option.autosearch = Auto búsqueda
+label.retrieve_ids = Recuperar IDs
+label.display_settings_for = Visualización de características {0}
+label.simple = Simple
+label.simple_colour = Color simple
+label.colour_by_text = Colorear por texto
+label.graduated_colour = Color graduado
+label.by_text_of = Por texto de
+label.by_range_of = Por rango de
+label.filters_tooltip = Haga clic para configurar o modificar los filtros
+label.or = O
+label.and = Y
+label.sequence_feature_colours = Colores de características de las secuencias
+label.best_quality = Mejor Calidad
+label.best_resolution = Mejor Resolución
+label.most_protein_chain = Más Cadena de Proteína
+label.most_bound_molecules = Más Moléculas Ligadas
+label.most_polymer_residues = Más Residuos de Polímeros
+label.cached_structures = Estructuras en Caché
+label.free_text_search = Búsqueda de texto libre
index 6344d1e..832d3e5 100755 (executable)
                </field>
                <field name="position">
                 <bind-xml name="position" node="attribute" location="location/position"/>
-                </field>
+               </field>
                <field name="begin">
                 <bind-xml name="position" node="attribute" location="location/begin"/>
-                </field>
-               <field name="end">
-                <bind-xml name="position" node="attribute" location="location/end"/>
-                </field>
+               </field>
+    <field name="end">
+      <bind-xml name="position" node="attribute" location="location/end"/>
+    </field>
+    <field name="variation">
+     <bind-xml name="variation"/>
+    </field>
+    <field name="original">
+     <bind-xml name="original"/>
+    </field>
         </class>
        
           <class name="jalview.datamodel.xdb.uniprot.UniprotSequence">
index bd43e9d..3934d66 100755 (executable)
@@ -16,8 +16,7 @@
   
   You should have received a copy of the GNU General Public License along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<!-- edited with XMLSpy v2005 rel. 3 U (http://www.altova.com) by lj (jl) -->
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="www.jalview.org/colours">
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jalview="www.jalview.org/colours" targetNamespace="www.jalview.org/colours">
        <xs:complexType name="JalviewUserColours">
                <xs:sequence>
                        <xs:element name="Version" maxOccurs="1" minOccurs="0" type="xs:string">
                        </xs:element>
                        <xs:element name="colour" maxOccurs="unbounded" minOccurs="0">
                                <xs:complexType>
-                                       <xs:attribute name="Name" type="xs:string"/>
+                                   <xs:sequence>
+                                           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                                                       <xs:annotation>
+                                                               <xs:documentation>name of feature attribute to colour by, or attribute and sub-attribute</xs:documentation>
+                                                       </xs:annotation>
+                                           </xs:element> 
+                                       </xs:sequence>
+                                       <xs:attribute name="Name" type="xs:string">
+                                               <xs:annotation>
+                                                       <xs:documentation>Single letter residue code for an alignment colour scheme, or feature type for a feature colour scheme</xs:documentation>
+                                               </xs:annotation>
+                                       </xs:attribute>
                                        <xs:attribute name="RGB" type="xs:string" use="required"/>
                                        <xs:attribute name="minRGB" type="xs:string" use="optional"/>
-                                       <xs:attribute name="threshType" type="xs:string" use="optional">
-                                       <xs:annotation>
-                                       <xs:documentation>loosely specified enumeration: NONE,ABOVE, or BELOW</xs:documentation>
-                                       </xs:annotation>
+                                       <xs:attribute name="noValueColour" use="optional" type="jalview:NoValueColour" default="Min" />
+                                       <xs:attribute name="threshType" use="optional">
+                                               <xs:simpleType> 
+                                                       <xs:restriction base="xs:string">
+                                                     <xs:enumeration value="NONE" />
+                                                     <xs:enumeration value="ABOVE" />
+                                                     <xs:enumeration value="BELOW" />
+                                                   </xs:restriction>   
+                                           </xs:simpleType> 
                                        </xs:attribute>
                                        <xs:attribute name="threshold" type="xs:float" use="optional"/>
                                        <xs:attribute name="max" type="xs:float" use="optional"/>
                                        <xs:attribute name="autoScale" type="xs:boolean" use="optional"/>
                                </xs:complexType>
                        </xs:element>
+                       <xs:element name="filter" maxOccurs="unbounded" minOccurs="0" >
+                           <xs:complexType>
+                                       <xs:sequence> 
+                                               <xs:element name="matcherSet" type="jalview:FeatureMatcherSet" />
+                                       </xs:sequence> 
+                                       <xs:attribute name="featureType" type="xs:string" use="required"/>
+                           </xs:complexType>
+                       </xs:element>
                </xs:sequence>
                <xs:attribute name="schemeName" type="xs:string" use="optional"/>
        </xs:complexType>
+
+       <xs:complexType name="FeatureMatcherSet"> 
+               <xs:annotation>
+                       <xs:documentation>A feature match condition, which may be simple or compound</xs:documentation>
+               </xs:annotation>
+         <xs:choice> 
+           <xs:element name="matchCondition" type="jalview:FeatureMatcher" /> 
+           <xs:element name="compoundMatcher">
+                   <xs:complexType>
+                         <xs:sequence> 
+                             <xs:element name="matcherSet" minOccurs="2" maxOccurs="2" type="jalview:FeatureMatcherSet" /> 
+                         </xs:sequence> 
+                     <xs:attribute name="and" type="xs:boolean" use="required">
+                                 <xs:annotation>
+                                       <xs:documentation>If true, matchers are AND-ed, if false they are OR-ed</xs:documentation>
+                                 </xs:annotation>
+                     </xs:attribute>
+                   </xs:complexType>
+           </xs:element>
+         </xs:choice> 
+       </xs:complexType> 
+       
+       <xs:complexType name="FeatureMatcher"> 
+         <xs:sequence> 
+           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                       <xs:annotation>
+                               <xs:documentation>name of feature attribute to filter on, or attribute and sub-attribute</xs:documentation>
+                       </xs:annotation>
+           </xs:element> 
+           <xs:element name="condition" type="xs:string" /> 
+           <xs:element name="value" type="xs:string" /> 
+         </xs:sequence>
+         <xs:attribute name="by">
+               <xs:simpleType> 
+                       <xs:restriction base="xs:string">
+                     <xs:enumeration value="byLabel" />
+                     <xs:enumeration value="byScore" />
+                     <xs:enumeration value="byAttribute" />
+                   </xs:restriction>   
+           </xs:simpleType> 
+         </xs:attribute> 
+       </xs:complexType> 
+       
+       <xs:simpleType name="NoValueColour">
+               <xs:annotation>
+                       <xs:documentation>Graduated feature colour if no score (or attribute) value</xs:documentation>
+               </xs:annotation>
+               <xs:restriction base="xs:string">
+             <xs:enumeration value="None" />
+             <xs:enumeration value="Min" />
+             <xs:enumeration value="Max" />
+           </xs:restriction>   
+       </xs:simpleType>
 </xs:schema>
index f0bd638..48824e7 100755 (executable)
                                                <xs:sequence>
                                                        <xs:element name="setting" minOccurs="0" maxOccurs="unbounded">
                                                                <xs:complexType>
+                                                                       <xs:sequence>
+                                                                           <xs:element name="attributeName" type="xs:string" minOccurs="0" maxOccurs="2">
+                                                                                       <xs:annotation>
+                                                                                               <xs:documentation>name of feature attribute to colour by, or attribute and sub-attribute</xs:documentation>
+                                                                                       </xs:annotation>
+                                                                           </xs:element> 
+                                                                               <xs:element name="matcherSet" minOccurs="0" type="jalview:FeatureMatcherSet">
+                                                                                       <xs:annotation>
+                                                                                               <xs:documentation>optional filter(s) applied to the feature type</xs:documentation>
+                                                                                       </xs:annotation>
+                                                                               </xs:element>
+                                                                       </xs:sequence>
                                                                        <xs:attribute name="type" type="xs:string" use="required" />
                                                                        <xs:attribute name="colour" type="xs:int" use="required" />
                                                                        <xs:attribute name="display" type="xs:boolean"
                                                                                        </xs:documentation>
                                                                                </xs:annotation>
                                                                        </xs:attribute>
+                                                                       <xs:attribute name="noValueColour" use="optional" type="jalview:NoValueColour" default="Min" />
                                                                        <xs:attribute name="threshold" type="xs:float"
                                                                                use="optional">
                                                                                <xs:annotation>
                        <xs:element name="otherData" minOccurs="0" maxOccurs="unbounded">
                                <xs:complexType>
                                        <xs:attribute name="key" type="xs:string" use="required" />
+                                       <xs:attribute name="key2" type="xs:string" use="optional">
+                                               <xs:annotation>
+                                                       <xs:documentation>key2 may be used for a sub-attribute of key</xs:documentation>
+                                               </xs:annotation>
+                                       </xs:attribute>
                                        <xs:attribute name="value" type="xs:string" use="required" />
                                </xs:complexType>
                        </xs:element>
index f94faba..b15c3cc 100644 (file)
@@ -159,7 +159,7 @@ public class AppletPDBCanvas extends Panel
 
     try
     {
-      pdb = ssm.setMapping(seq, chains, pdbentry.getFile(), protocol);
+      pdb = ssm.setMapping(seq, chains, pdbentry.getFile(), protocol, null);
 
       if (protocol == DataSourceType.PASTE)
       {
index b2f2503..ab172f2 100644 (file)
@@ -153,7 +153,8 @@ public class PDBCanvas extends JPanel
 
     try
     {
-      pdb = ssm.setMapping(seq, chains, pdbentry.getFile(), protocol);
+      pdb = ssm.setMapping(seq, chains, pdbentry.getFile(), protocol,
+              ap.alignFrame);
 
       if (protocol.equals(jalview.io.DataSourceType.PASTE))
       {
index f4bd31c..904a860 100755 (executable)
@@ -45,11 +45,11 @@ public class PDBChain
 
   public String id;
 
-  public Vector<Bond> bonds = new Vector<Bond>();
+  public Vector<Bond> bonds = new Vector<>();
 
-  public Vector<Atom> atoms = new Vector<Atom>();
+  public Vector<Atom> atoms = new Vector<>();
 
-  public Vector<Residue> residues = new Vector<Residue>();
+  public Vector<Residue> residues = new Vector<>();
 
   public int offset;
 
@@ -162,6 +162,50 @@ public class PDBChain
   }
 
   /**
+   * Annotate the residues with their corresponding positions in s1 using the
+   * alignment in as NOTE: This clears all atom.alignmentMapping values on the
+   * structure.
+   * 
+   * @param as
+   * @param s1
+   */
+  public void makeExactMapping(StructureMapping mapping, SequenceI s1)
+  {
+    // first clear out any old alignmentMapping values:
+    for (Atom atom : atoms)
+    {
+      atom.alignmentMapping = -1;
+    }
+    SequenceI ds = s1;
+    while (ds.getDatasetSequence() != null)
+    {
+      ds = ds.getDatasetSequence();
+    }
+    int pdboffset = 0;
+    for (Residue res : residues)
+    {
+      // res.number isn't set correctly for discontinuous/mismapped residues
+      int seqpos = mapping.getSeqPos(res.atoms.get(0).resNumber);
+      char strchar = sequence.getCharAt(pdboffset++);
+      if (seqpos == StructureMapping.UNASSIGNED_VALUE)
+      {
+        continue;
+      }
+      char seqchar = ds.getCharAt(seqpos - ds.getStart());
+
+      boolean sameResidue = Comparison.isSameResidue(
+              seqchar, strchar, false);
+      if (sameResidue)
+      {
+        for (Atom atom : res.atoms)
+        {
+          atom.alignmentMapping = seqpos - 1;
+        }
+      }
+    }
+  }
+
+  /**
    * Copies over the RESNUM seqfeatures from the internal chain sequence to the
    * mapped sequence
    * 
@@ -299,12 +343,13 @@ public class PDBChain
     boolean deoxyn = false;
     boolean nucleotide = false;
     StringBuilder seq = new StringBuilder(256);
-    Vector<SequenceFeature> resFeatures = new Vector<SequenceFeature>();
-    Vector<Annotation> resAnnotation = new Vector<Annotation>();
-    int i, iSize = atoms.size() - 1;
+    Vector<SequenceFeature> resFeatures = new Vector<>();
+    Vector<Annotation> resAnnotation = new Vector<>();
+    int iSize = atoms.size() - 1;
     int resNumber = -1;
     char insCode = ' ';
-    for (i = 0; i <= iSize; i++)
+
+    for (int i = 0; i <= iSize; i++)
     {
       Atom tmp = atoms.elementAt(i);
       resNumber = tmp.resNumber;
@@ -318,7 +363,7 @@ public class PDBChain
         offset = resNumber;
       }
 
-      Vector<Atom> resAtoms = new Vector<Atom>();
+      Vector<Atom> resAtoms = new Vector<>();
       // Add atoms to a vector while the residue number
       // remains the same as the first atom's resNumber (res)
       while ((resNumber == res) && (ins == insCode) && (i < atoms.size()))
@@ -425,7 +470,8 @@ public class PDBChain
 
     if (StructureImportSettings.isShowSeqFeatures())
     {
-      for (i = 0, iSize = resFeatures.size(); i < iSize; i++)
+      iSize = resFeatures.size();
+      for (int i = 0; i < iSize; i++)
       {
         sequence.addSequenceFeature(resFeatures.elementAt(i));
         resFeatures.setElementAt(null, i);
@@ -434,20 +480,20 @@ public class PDBChain
     if (visibleChainAnnotation)
     {
       Annotation[] annots = new Annotation[resAnnotation.size()];
-      float max = 0;
-      for (i = 0, iSize = annots.length; i < iSize; i++)
+      float max = 0f;
+      float min = 0f;
+      iSize = annots.length;
+      for (int i = 0; i < iSize; i++)
       {
         annots[i] = resAnnotation.elementAt(i);
-        if (annots[i].value > max)
-        {
-          max = annots[i].value;
-        }
+        max = Math.max(max, annots[i].value);
+        min = Math.min(min, annots[i].value);
         resAnnotation.setElementAt(null, i);
       }
 
       AlignmentAnnotation tfactorann = new AlignmentAnnotation(
               "Temperature Factor", "Temperature Factor for " + pdbid + id,
-              annots, 0, max, AlignmentAnnotation.LINE_GRAPH);
+              annots, min, max, AlignmentAnnotation.LINE_GRAPH);
       tfactorann.setSequenceRef(sequence);
       sequence.addAlignmentAnnotation(tfactorann);
     }
@@ -550,6 +596,12 @@ public class PDBChain
   {
     SequenceI sq = mapping.getSequence();
     SequenceI dsq = sq;
+    if (sqmpping == null)
+    {
+      // SIFTS mappings are recorded in the StructureMapping object...
+
+      sqmpping = mapping.getSeqToPdbMapping();
+    }
     if (sq != null)
     {
       while (dsq.getDatasetSequence() != null)
index 0045e97..3abbe75 100644 (file)
@@ -383,6 +383,8 @@ public class ChimeraResidue implements ChimeraStructuralObject,
   public void splitInsertionCode(String residue)
   {
     // OK, split the index into number and insertion code
+    // JBPNote - m.matches() can be true even if there is no resnum - this can
+    // cause NumberFormatExceptions below
     Pattern p = Pattern.compile("(\\d*)([A-Z]?)");
     Matcher m = p.matcher(residue);
     if (m.matches())
index effe556..09a9713 100644 (file)
@@ -97,7 +97,7 @@ public class StructureManager
     this.haveGUI = haveGUI;
     // Create the Chimera interface
     chimeraManager = new ChimeraManager(this);
-    chimSelectionList = new ArrayList<ChimeraStructuralObject>();
+    chimSelectionList = new ArrayList<>();
     pathProps = new Properties();
   }
 
@@ -110,7 +110,7 @@ public class StructureManager
           ModelType type)
   {
     // new models
-    Map<String, List<ChimeraModel>> newModels = new HashMap<String, List<ChimeraModel>>();
+    Map<String, List<ChimeraModel>> newModels = new HashMap<>();
     if (chimObjNames.size() > 0)
     {
       List<String> names = chimObjNames.iterator().next();
@@ -846,7 +846,7 @@ public class StructureManager
     // alDialog.dispose();
     // }
     // System.out.println("launch align dialog");
-    List<ChimeraStructuralObject> chimObjectList = new ArrayList<ChimeraStructuralObject>();
+    List<ChimeraStructuralObject> chimObjectList = new ArrayList<>();
     for (ChimeraModel model : chimeraManager.getChimeraModels())
     {
       if (useChains)
@@ -887,7 +887,7 @@ public class StructureManager
 
   public List<String> getAllChimeraResidueAttributes()
   {
-    List<String> attributes = new ArrayList<String>();
+    List<String> attributes = new ArrayList<>();
     // attributes.addAll(rinManager.getResAttrs());
     attributes.addAll(chimeraManager.getAttrList());
     return attributes;
@@ -898,7 +898,7 @@ public class StructureManager
   // TODO: [Optional] Change priority of Chimera paths
   public static List<String> getChimeraPaths()
   {
-    List<String> pathList = new ArrayList<String>();
+    List<String> pathList = new ArrayList<>();
 
     // if no network is available and the settings have been modified by the
     // user, check for a
@@ -934,8 +934,18 @@ public class StructureManager
     }
     else if (os.startsWith("Windows"))
     {
-      pathList.add("\\Program Files\\Chimera\\bin\\chimera");
-      pathList.add("C:\\Program Files\\Chimera\\bin\\chimera.exe");
+      for (String root : new String[] { "\\Program Files",
+          "C:\\Program Files", "\\Program Files (x86)",
+          "C:\\Program Files (x86)" })
+      {
+        for (String version : new String[] { "1.11", "1.11.1", "1.11.2",
+            "1.12", "1.12.1", "1.12.2", "1.13" })
+        {
+          pathList.add(root + "\\Chimera " + version + "\\bin\\chimera");
+          pathList.add(
+                  root + "\\Chimera " + version + "\\bin\\chimera.exe");
+        }
+      }
     }
     else if (os.startsWith("Mac"))
     {
index 34a21e6..1b2578e 100755 (executable)
@@ -36,6 +36,7 @@ import jalview.util.MessageManager;
 
 import java.awt.Color;
 import java.awt.Graphics;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -49,6 +50,14 @@ import java.util.StringTokenizer;
  */
 public class AlignSeq
 {
+  private static final int MAX_NAME_LENGTH = 30;
+
+  private static final int GAP_OPEN_COST = 120;
+
+  private static final int GAP_EXTEND_COST = 20;
+
+  private static final int GAP_INDEX = -1;
+
   public static final String PEP = "pep";
 
   public static final String DNA = "dna";
@@ -61,7 +70,7 @@ public class AlignSeq
 
   float[][] F;
 
-  int[][] traceback;
+  int[][] traceback; // todo is this actually used?
 
   int[] seq1;
 
@@ -96,30 +105,20 @@ public class AlignSeq
   /** DOCUMENT ME!! */
   public int seq2start;
 
-  /** DOCUMENT ME!! */
   public int seq2end;
 
   int count;
 
-  /** DOCUMENT ME!! */
   public float maxscore;
 
-  float pid;
-
   int prev = 0;
 
-  int gapOpen = 120;
-
-  int gapExtend = 20;
-
   StringBuffer output = new StringBuffer();
 
   String type; // AlignSeq.PEP or AlignSeq.DNA
 
   private ScoreMatrix scoreMatrix;
 
-  private static final int GAP_INDEX = -1;
-
   /**
    * Creates a new AlignSeq object.
    * 
@@ -378,11 +377,10 @@ public class AlignSeq
       }
     }
 
-    // System.out.println(maxi + " " + maxj + " " + score[maxi][maxj]);
     int i = maxi;
     int j = maxj;
     int trace;
-    maxscore = score[i][j] / 10;
+    maxscore = score[i][j] / 10f;
 
     seq1end = maxi + 1;
     seq2end = maxj + 1;
@@ -451,49 +449,48 @@ public class AlignSeq
   /**
    * DOCUMENT ME!
    */
-  public void printAlignment(java.io.PrintStream os)
+  public void printAlignment(PrintStream os)
   {
     // TODO: Use original sequence characters rather than re-translated
     // characters in output
     // Find the biggest id length for formatting purposes
-    String s1id = s1.getName(), s2id = s2.getName();
-    int maxid = s1.getName().length();
-    if (s2.getName().length() > maxid)
-    {
-      maxid = s2.getName().length();
-    }
-    if (maxid > 30)
+    String s1id = getAlignedSeq1().getDisplayId(true);
+    String s2id = getAlignedSeq2().getDisplayId(true);
+    int nameLength = Math.max(s1id.length(), s2id.length());
+    if (nameLength > MAX_NAME_LENGTH)
     {
-      maxid = 30;
+      int truncateBy = nameLength - MAX_NAME_LENGTH;
+      nameLength = MAX_NAME_LENGTH;
       // JAL-527 - truncate the sequence ids
-      if (s1.getName().length() > maxid)
+      if (s1id.length() > nameLength)
       {
-        s1id = s1.getName().substring(0, 30);
+        int slashPos = s1id.lastIndexOf('/');
+        s1id = s1id.substring(0, slashPos - truncateBy)
+                + s1id.substring(slashPos);
       }
-      if (s2.getName().length() > maxid)
+      if (s2id.length() > nameLength)
       {
-        s2id = s2.getName().substring(0, 30);
+        int slashPos = s2id.lastIndexOf('/');
+        s2id = s2id.substring(0, slashPos - truncateBy)
+                + s2id.substring(slashPos);
       }
     }
-    int len = 72 - maxid - 1;
+    int len = 72 - nameLength - 1;
     int nochunks = ((aseq1.length - count) / len)
             + ((aseq1.length - count) % len > 0 ? 1 : 0);
-    pid = 0;
+    float pid = 0f;
 
     output.append("Score = ").append(score[maxi][maxj]).append(NEWLINE);
     output.append("Length of alignment = ")
             .append(String.valueOf(aseq1.length - count)).append(NEWLINE);
     output.append("Sequence ");
-    output.append(new Format("%" + maxid + "s").form(s1.getName()));
-    output.append(" :  ").append(String.valueOf(s1.getStart()))
-            .append(" - ").append(String.valueOf(s1.getEnd()));
+    Format nameFormat = new Format("%" + nameLength + "s");
+    output.append(nameFormat.form(s1id));
     output.append(" (Sequence length = ")
             .append(String.valueOf(s1str.length())).append(")")
             .append(NEWLINE);
     output.append("Sequence ");
-    output.append(new Format("%" + maxid + "s").form(s2.getName()));
-    output.append(" :  ").append(String.valueOf(s2.getStart()))
-            .append(" - ").append(String.valueOf(s2.getEnd()));
+    output.append(nameFormat.form(s2id));
     output.append(" (Sequence length = ")
             .append(String.valueOf(s2str.length())).append(")")
             .append(NEWLINE).append(NEWLINE);
@@ -503,7 +500,7 @@ public class AlignSeq
     for (int j = 0; j < nochunks; j++)
     {
       // Print the first aligned sequence
-      output.append(new Format("%" + (maxid) + "s").form(s1id)).append(" ");
+      output.append(nameFormat.form(s1id)).append(" ");
 
       for (int i = 0; i < len; i++)
       {
@@ -514,7 +511,7 @@ public class AlignSeq
       }
 
       output.append(NEWLINE);
-      output.append(new Format("%" + (maxid) + "s").form(" ")).append(" ");
+      output.append(nameFormat.form(" ")).append(" ");
 
       /*
        * Print out the match symbols:
@@ -534,7 +531,7 @@ public class AlignSeq
             pid++;
             output.append("|");
           }
-          else if (type.equals("pep"))
+          else if (PEP.equals(type))
           {
             if (pam250.getPairwiseScore(c1, c2) > 0)
             {
@@ -554,8 +551,7 @@ public class AlignSeq
 
       // Now print the second aligned sequence
       output = output.append(NEWLINE);
-      output = output.append(new Format("%" + (maxid) + "s").form(s2id))
-              .append(" ");
+      output = output.append(nameFormat.form(s2id)).append(" ");
 
       for (int i = 0; i < len; i++)
       {
@@ -569,7 +565,8 @@ public class AlignSeq
     }
 
     pid = pid / (aseq1.length - count) * 100;
-    output = output.append(new Format("Percentage ID = %2.2f\n").form(pid));
+    output.append(new Format("Percentage ID = %3.2f\n").form(pid));
+    output.append(NEWLINE);
     try
     {
       os.print(output.toString());
@@ -591,7 +588,6 @@ public class AlignSeq
   public int findTrace(int i, int j)
   {
     int t = 0;
-    // float pairwiseScore = lookup[seq1[i]][seq2[j]];
     float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(i),
             s2str.charAt(j));
     float max = score[i - 1][j - 1] + (pairwiseScore * 10);
@@ -640,19 +636,19 @@ public class AlignSeq
     // top left hand element
     score[0][0] = scoreMatrix.getPairwiseScore(s1str.charAt(0),
             s2str.charAt(0)) * 10;
-    E[0][0] = -gapExtend;
+    E[0][0] = -GAP_EXTEND_COST;
     F[0][0] = 0;
 
     // Calculate the top row first
     for (int j = 1; j < m; j++)
     {
       // What should these values be? 0 maybe
-      E[0][j] = max(score[0][j - 1] - gapOpen, E[0][j - 1] - gapExtend);
-      F[0][j] = -gapExtend;
+      E[0][j] = max(score[0][j - 1] - GAP_OPEN_COST, E[0][j - 1] - GAP_EXTEND_COST);
+      F[0][j] = -GAP_EXTEND_COST;
 
       float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(0),
               s2str.charAt(j));
-      score[0][j] = max(pairwiseScore * 10, -gapOpen, -gapExtend);
+      score[0][j] = max(pairwiseScore * 10, -GAP_OPEN_COST, -GAP_EXTEND_COST);
 
       traceback[0][j] = 1;
     }
@@ -660,8 +656,8 @@ public class AlignSeq
     // Now do the left hand column
     for (int i = 1; i < n; i++)
     {
-      E[i][0] = -gapOpen;
-      F[i][0] = max(score[i - 1][0] - gapOpen, F[i - 1][0] - gapExtend);
+      E[i][0] = -GAP_OPEN_COST;
+      F[i][0] = max(score[i - 1][0] - GAP_OPEN_COST, F[i - 1][0] - GAP_EXTEND_COST);
 
       float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(i),
               s2str.charAt(0));
@@ -674,8 +670,8 @@ public class AlignSeq
     {
       for (int j = 1; j < m; j++)
       {
-        E[i][j] = max(score[i][j - 1] - gapOpen, E[i][j - 1] - gapExtend);
-        F[i][j] = max(score[i - 1][j] - gapOpen, F[i - 1][j] - gapExtend);
+        E[i][j] = max(score[i][j - 1] - GAP_OPEN_COST, E[i][j - 1] - GAP_EXTEND_COST);
+        F[i][j] = max(score[i - 1][j] - GAP_OPEN_COST, F[i - 1][j] - GAP_EXTEND_COST);
 
         float pairwiseScore = scoreMatrix.getPairwiseScore(s1str.charAt(i),
                 s2str.charAt(j));
index 2b9b9f9..d1217bf 100644 (file)
@@ -29,6 +29,7 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.IncompleteCodonException;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.Sequence;
@@ -36,6 +37,7 @@ import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.features.SequenceFeatures;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.schemes.ResidueProperties;
 import jalview.util.Comparison;
@@ -105,6 +107,15 @@ public class AlignmentUtils
     {
       return variant == null ? null : variant.getFeatureGroup();
     }
+
+    /**
+     * toString for aid in the debugger only
+     */
+    @Override
+    public String toString()
+    {
+      return base + ":" + (variant == null ? "" : variant.getDescription());
+    }
   }
 
   /**
@@ -117,7 +128,7 @@ public class AlignmentUtils
    */
   public static AlignmentI expandContext(AlignmentI core, int flankSize)
   {
-    List<SequenceI> sq = new ArrayList<SequenceI>();
+    List<SequenceI> sq = new ArrayList<>();
     int maxoffset = 0;
     for (SequenceI s : core.getSequences())
     {
@@ -247,7 +258,7 @@ public class AlignmentUtils
   public static Map<String, List<SequenceI>> getSequencesByName(
           AlignmentI al)
   {
-    Map<String, List<SequenceI>> theMap = new LinkedHashMap<String, List<SequenceI>>();
+    Map<String, List<SequenceI>> theMap = new LinkedHashMap<>();
     for (SequenceI seq : al.getSequences())
     {
       String name = seq.getName();
@@ -256,7 +267,7 @@ public class AlignmentUtils
         List<SequenceI> seqs = theMap.get(name);
         if (seqs == null)
         {
-          seqs = new ArrayList<SequenceI>();
+          seqs = new ArrayList<>();
           theMap.put(name, seqs);
         }
         seqs.add(seq);
@@ -283,8 +294,8 @@ public class AlignmentUtils
       return false;
     }
 
-    Set<SequenceI> mappedDna = new HashSet<SequenceI>();
-    Set<SequenceI> mappedProtein = new HashSet<SequenceI>();
+    Set<SequenceI> mappedDna = new HashSet<>();
+    Set<SequenceI> mappedProtein = new HashSet<>();
 
     /*
      * First pass - map sequences where cross-references exist. This include
@@ -384,7 +395,7 @@ public class AlignmentUtils
    * Answers true if the mappings include one between the given (dataset)
    * sequences.
    */
-  public static boolean mappingExists(List<AlignedCodonFrame> mappings,
+  protected static boolean mappingExists(List<AlignedCodonFrame> mappings,
           SequenceI aaSeq, SequenceI cdnaSeq)
   {
     if (mappings != null)
@@ -454,7 +465,7 @@ public class AlignmentUtils
     {
       String lastCodon = String.valueOf(cdnaSeqChars,
               cdnaLength - CODON_LENGTH, CODON_LENGTH).toUpperCase();
-      for (String stop : ResidueProperties.STOP)
+      for (String stop : ResidueProperties.STOP_CODONS)
       {
         if (lastCodon.equals(stop))
         {
@@ -525,7 +536,8 @@ public class AlignmentUtils
        * allow * in protein to match untranslatable in dna
        */
       final char aaRes = aaSeqChars[aaPos];
-      if ((translated == null || "STOP".equals(translated)) && aaRes == '*')
+      if ((translated == null || ResidueProperties.STOP.equals(translated))
+              && aaRes == '*')
       {
         continue;
       }
@@ -557,7 +569,8 @@ public class AlignmentUtils
     if (dnaPos == cdnaSeqChars.length - CODON_LENGTH)
     {
       String codon = String.valueOf(cdnaSeqChars, dnaPos, CODON_LENGTH);
-      if ("STOP".equals(ResidueProperties.codonTranslate(codon)))
+      if (ResidueProperties.STOP
+              .equals(ResidueProperties.codonTranslate(codon)))
       {
         return true;
       }
@@ -870,7 +883,7 @@ public class AlignmentUtils
       System.err.println("Wrong alignment type in alignProteinAsDna");
       return 0;
     }
-    List<SequenceI> unmappedProtein = new ArrayList<SequenceI>();
+    List<SequenceI> unmappedProtein = new ArrayList<>();
     Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = buildCodonColumnsMap(
             protein, dna, unmappedProtein);
     return alignProteinAs(protein, alignedCodons, unmappedProtein);
@@ -1081,7 +1094,7 @@ public class AlignmentUtils
      * {dnaSequence, {proteinSequence, codonProduct}} at that position. The
      * comparator keeps the codon positions ordered.
      */
-    Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = new TreeMap<AlignedCodon, Map<SequenceI, AlignedCodon>>(
+    Map<AlignedCodon, Map<SequenceI, AlignedCodon>> alignedCodons = new TreeMap<>(
             new CodonComparator());
 
     for (SequenceI dnaSeq : dna.getSequences())
@@ -1127,9 +1140,9 @@ public class AlignmentUtils
     // TODO delete this ugly hack once JAL-2022 is resolved
     // i.e. we can model startPhase > 0 (incomplete start codon)
 
-    List<SequenceI> sequencesChecked = new ArrayList<SequenceI>();
+    List<SequenceI> sequencesChecked = new ArrayList<>();
     AlignedCodon lastCodon = null;
-    Map<SequenceI, AlignedCodon> toAdd = new HashMap<SequenceI, AlignedCodon>();
+    Map<SequenceI, AlignedCodon> toAdd = new HashMap<>();
 
     for (Entry<AlignedCodon, Map<SequenceI, AlignedCodon>> entry : alignedCodons
             .entrySet())
@@ -1308,7 +1321,7 @@ public class AlignmentUtils
     Map<SequenceI, AlignedCodon> seqProduct = alignedCodons.get(codon);
     if (seqProduct == null)
     {
-      seqProduct = new HashMap<SequenceI, AlignedCodon>();
+      seqProduct = new HashMap<>();
       alignedCodons.put(codon, seqProduct);
     }
     seqProduct.put(protein, codon);
@@ -1445,7 +1458,7 @@ public class AlignmentUtils
       {
         continue;
       }
-      final List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+      final List<AlignmentAnnotation> result = new ArrayList<>();
       for (AlignmentAnnotation dsann : datasetAnnotations)
       {
         /*
@@ -1627,17 +1640,17 @@ public class AlignmentUtils
       throw new IllegalArgumentException(
               "IMPLEMENTATION ERROR: dataset.getDataset() must be null!");
     }
-    List<SequenceI> foundSeqs = new ArrayList<SequenceI>();
-    List<SequenceI> cdsSeqs = new ArrayList<SequenceI>();
+    List<SequenceI> foundSeqs = new ArrayList<>();
+    List<SequenceI> cdsSeqs = new ArrayList<>();
     List<AlignedCodonFrame> mappings = dataset.getCodonFrames();
     HashSet<SequenceI> productSeqs = null;
     if (products != null)
     {
-      productSeqs = new HashSet<SequenceI>();
+      productSeqs = new HashSet<>();
       for (SequenceI seq : products)
       {
-        productSeqs.add(seq.getDatasetSequence() == null ? seq
-                : seq.getDatasetSequence());
+        productSeqs.add(seq.getDatasetSequence() == null ? seq : seq
+                .getDatasetSequence());
       }
     }
 
@@ -1730,9 +1743,8 @@ public class AlignmentUtils
           /*
            * add a mapping from CDS to the (unchanged) mapped to range
            */
-          List<int[]> cdsRange = Collections
-                  .singletonList(new int[]
-                  { 1, cdsSeq.getLength() });
+          List<int[]> cdsRange = Collections.singletonList(new int[] { 1,
+              cdsSeq.getLength() });
           MapList cdsToProteinMap = new MapList(cdsRange,
                   mapList.getToRanges(), mapList.getFromRatio(),
                   mapList.getToRatio());
@@ -1754,7 +1766,7 @@ public class AlignmentUtils
            * add another mapping from original 'from' range to CDS
            */
           AlignedCodonFrame dnaToCdsMapping = new AlignedCodonFrame();
-          MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
+          final MapList dnaToCdsMap = new MapList(mapList.getFromRanges(),
                   cdsRange, 1, 1);
           dnaToCdsMapping.addMap(dnaSeq.getDatasetSequence(), cdsSeqDss,
                   dnaToCdsMap);
@@ -1764,6 +1776,13 @@ public class AlignmentUtils
           }
 
           /*
+           * transfer dna chromosomal loci (if known) to the CDS
+           * sequence (via the mapping)
+           */
+          final MapList cdsToDnaMap = dnaToCdsMap.getInverse();
+          transferGeneLoci(dnaSeq, cdsToDnaMap, cdsSeq);
+
+          /*
            * add DBRef with mapping from protein to CDS
            * (this enables Get Cross-References from protein alignment)
            * This is tricky because we can't have two DBRefs with the
@@ -1782,26 +1801,30 @@ public class AlignmentUtils
 
           for (DBRefEntry primRef : dnaDss.getPrimaryDBRefs())
           {
-            // creates a complementary cross-reference to the source sequence's
-            // primary reference.
-
-            DBRefEntry cdsCrossRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getSource() + ":" + primRef.getVersion(),
-                    primRef.getAccessionId());
-            cdsCrossRef
-                    .setMap(new Mapping(dnaDss, new MapList(dnaToCdsMap)));
+            /*
+             * create a cross-reference from CDS to the source sequence's
+             * primary reference and vice versa
+             */
+            String source = primRef.getSource();
+            String version = primRef.getVersion();
+            DBRefEntry cdsCrossRef = new DBRefEntry(source, source + ":"
+                    + version, primRef.getAccessionId());
+            cdsCrossRef.setMap(new Mapping(dnaDss, new MapList(cdsToDnaMap)));
             cdsSeqDss.addDBRef(cdsCrossRef);
 
+            dnaSeq.addDBRef(new DBRefEntry(source, version, cdsSeq
+                    .getName(), new Mapping(cdsSeqDss, dnaToCdsMap)));
+
             // problem here is that the cross-reference is synthesized -
             // cdsSeq.getName() may be like 'CDS|dnaaccession' or
             // 'CDS|emblcdsacc'
             // assuming cds version same as dna ?!?
 
-            DBRefEntry proteinToCdsRef = new DBRefEntry(primRef.getSource(),
-                    primRef.getVersion(), cdsSeq.getName());
+            DBRefEntry proteinToCdsRef = new DBRefEntry(source, version,
+                    cdsSeq.getName());
             //
-            proteinToCdsRef.setMap(
-                    new Mapping(cdsSeqDss, cdsToProteinMap.getInverse()));
+            proteinToCdsRef.setMap(new Mapping(cdsSeqDss, cdsToProteinMap
+                    .getInverse()));
             proteinProduct.addDBRef(proteinToCdsRef);
           }
 
@@ -1814,14 +1837,46 @@ public class AlignmentUtils
       }
     }
 
-    AlignmentI cds = new Alignment(
-            cdsSeqs.toArray(new SequenceI[cdsSeqs.size()]));
+    AlignmentI cds = new Alignment(cdsSeqs.toArray(new SequenceI[cdsSeqs
+            .size()]));
     cds.setDataset(dataset);
 
     return cds;
   }
 
   /**
+   * Tries to transfer gene loci (dbref to chromosome positions) from fromSeq to
+   * toSeq, mediated by the given mapping between the sequences
+   * 
+   * @param fromSeq
+   * @param targetToFrom
+   *          Map
+   * @param targetSeq
+   */
+  protected static void transferGeneLoci(SequenceI fromSeq,
+          MapList targetToFrom, SequenceI targetSeq)
+  {
+    if (targetSeq.getGeneLoci() != null)
+    {
+      // already have - don't override
+      return;
+    }
+    GeneLociI fromLoci = fromSeq.getGeneLoci();
+    if (fromLoci == null)
+    {
+      return;
+    }
+
+    MapList newMap = targetToFrom.traverse(fromLoci.getMap());
+
+    if (newMap != null)
+    {
+      targetSeq.setGeneLoci(fromLoci.getSpeciesId(),
+              fromLoci.getAssemblyId(), fromLoci.getChromosomeId(), newMap);
+    }
+  }
+
+  /**
    * A helper method that finds a CDS sequence in the alignment dataset that is
    * mapped to the given protein sequence, and either is, or has a mapping from,
    * the given dna sequence.
@@ -1833,7 +1888,7 @@ public class AlignmentUtils
    * @param seqMappings
    *          the set of mappings involving dnaSeq
    * @param aMapping
-   *          an initial candidate from seqMappings
+   *          a transcript-to-peptide mapping
    * @return
    */
   static SequenceI findCdsForProtein(List<AlignedCodonFrame> mappings,
@@ -1858,7 +1913,15 @@ public class AlignmentUtils
     if (mappedFromLength == dnaLength
             || mappedFromLength == dnaLength - CODON_LENGTH)
     {
-      return seqDss;
+      /*
+       * if sequence has CDS features, this is a transcript with no UTR
+       * - do not take this as the CDS sequence! (JAL-2789)
+       */
+      if (seqDss.getFeatures().getFeaturesByOntology(SequenceOntologyI.CDS)
+              .isEmpty())
+      {
+        return seqDss;
+      }
     }
 
     /*
@@ -1883,10 +1946,12 @@ public class AlignmentUtils
           {
             /*
             * found a 3:1 mapping to the protein product which covers
-            * the whole dna sequence i.e. is from CDS; finally check it
-            * is from the dna start sequence
+            * the whole dna sequence i.e. is from CDS; finally check the CDS
+            * is mapped from the given dna start sequence
             */
             SequenceI cdsSeq = map.getFromSeq();
+            // todo this test is weak if seqMappings contains multiple mappings;
+            // we get away with it if transcript:cds relationship is 1:1
             List<AlignedCodonFrame> dnaToCdsMaps = MappingUtils
                     .findMappingsForSequence(cdsSeq, seqMappings);
             if (!dnaToCdsMaps.isEmpty())
@@ -1989,21 +2054,23 @@ public class AlignmentUtils
   }
 
   /**
-   * add any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
+   * Adds any DBRefEntrys to cdsSeq from contig that have a Mapping congruent to
    * the given mapping.
    * 
    * @param cdsSeq
    * @param contig
+   * @param proteinProduct
    * @param mapping
-   * @return list of DBRefEntrys added.
+   * @return list of DBRefEntrys added
    */
-  public static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
+  protected static List<DBRefEntry> propagateDBRefsToCDS(SequenceI cdsSeq,
           SequenceI contig, SequenceI proteinProduct, Mapping mapping)
   {
 
-    // gather direct refs from contig congrent with mapping
-    List<DBRefEntry> direct = new ArrayList<DBRefEntry>();
-    HashSet<String> directSources = new HashSet<String>();
+    // gather direct refs from contig congruent with mapping
+    List<DBRefEntry> direct = new ArrayList<>();
+    HashSet<String> directSources = new HashSet<>();
+
     if (contig.getDBRefs() != null)
     {
       for (DBRefEntry dbr : contig.getDBRefs())
@@ -2023,7 +2090,7 @@ public class AlignmentUtils
     DBRefEntry[] onSource = DBRefUtils.selectRefs(
             proteinProduct.getDBRefs(),
             directSources.toArray(new String[0]));
-    List<DBRefEntry> propagated = new ArrayList<DBRefEntry>();
+    List<DBRefEntry> propagated = new ArrayList<>();
 
     // and generate appropriate mappings
     for (DBRefEntry cdsref : direct)
@@ -2081,7 +2148,7 @@ public class AlignmentUtils
    *          subtypes in the Sequence Ontology)
    * @param omitting
    */
-  public static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
+  protected static int transferFeatures(SequenceI fromSeq, SequenceI toSeq,
           MapList mapping, String select, String... omitting)
   {
     SequenceI copyTo = toSeq;
@@ -2164,7 +2231,10 @@ public class AlignmentUtils
 
   /**
    * Returns a mapping from dna to protein by inspecting sequence features of
-   * type "CDS" on the dna.
+   * type "CDS" on the dna. A mapping is constructed if the total CDS feature
+   * length is 3 times the peptide length (optionally after dropping a trailing
+   * stop codon). This method does not check whether the CDS nucleotide sequence
+   * translates to the peptide sequence.
    * 
    * @param dnaSeq
    * @param proteinSeq
@@ -2176,6 +2246,16 @@ public class AlignmentUtils
     List<int[]> ranges = findCdsPositions(dnaSeq);
     int mappedDnaLength = MappingUtils.getLength(ranges);
 
+    /*
+     * if not a whole number of codons, truncate mapping
+     */
+    int codonRemainder = mappedDnaLength % CODON_LENGTH;
+    if (codonRemainder > 0)
+    {
+      mappedDnaLength -= codonRemainder;
+      MappingUtils.removeEndPositions(codonRemainder, ranges);
+    }
+
     int proteinLength = proteinSeq.getLength();
     int proteinStart = proteinSeq.getStart();
     int proteinEnd = proteinSeq.getEnd();
@@ -2190,7 +2270,7 @@ public class AlignmentUtils
       proteinStart++;
       proteinLength--;
     }
-    List<int[]> proteinRange = new ArrayList<int[]>();
+    List<int[]> proteinRange = new ArrayList<>();
 
     /*
      * dna length should map to protein (or protein plus stop codon)
@@ -2199,8 +2279,12 @@ public class AlignmentUtils
     if (codesForResidues == (proteinLength + 1))
     {
       // assuming extra codon is for STOP and not in peptide
+      // todo: check trailing codon is indeed a STOP codon
       codesForResidues--;
+      mappedDnaLength -= CODON_LENGTH;
+      MappingUtils.removeEndPositions(CODON_LENGTH, ranges);
     }
+
     if (codesForResidues == proteinLength)
     {
       proteinRange.add(new int[] { proteinStart, proteinEnd });
@@ -2211,7 +2295,7 @@ public class AlignmentUtils
 
   /**
    * Returns a list of CDS ranges found (as sequence positions base 1), i.e. of
-   * start/end positions of sequence features of type "CDS" (or a sub-type of
+   * [start, end] positions of sequence features of type "CDS" (or a sub-type of
    * CDS in the Sequence Ontology). The ranges are sorted into ascending start
    * position order, so this method is only valid for linear CDS in the same
    * sense as the protein product.
@@ -2219,9 +2303,9 @@ public class AlignmentUtils
    * @param dnaSeq
    * @return
    */
-  public static List<int[]> findCdsPositions(SequenceI dnaSeq)
+  protected static List<int[]> findCdsPositions(SequenceI dnaSeq)
   {
-    List<int[]> result = new ArrayList<int[]>();
+    List<int[]> result = new ArrayList<>();
 
     List<SequenceFeature> sfs = dnaSeq.getFeatures().getFeaturesByOntology(
             SequenceOntologyI.CDS);
@@ -2230,7 +2314,6 @@ public class AlignmentUtils
       return result;
     }
     SequenceFeatures.sortFeatures(sfs, true);
-    int startPhase = 0;
 
     for (SequenceFeature sf : sfs)
     {
@@ -2248,7 +2331,7 @@ public class AlignmentUtils
        */
       int begin = sf.getBegin();
       int end = sf.getEnd();
-      if (result.isEmpty())
+      if (result.isEmpty() && phase > 0)
       {
         begin += phase;
         if (begin > end)
@@ -2263,16 +2346,6 @@ public class AlignmentUtils
     }
 
     /*
-     * remove 'startPhase' positions (usually 0) from the first range 
-     * so we begin at the start of a complete codon
-     */
-    if (!result.isEmpty())
-    {
-      // TODO JAL-2022 correctly model start phase > 0
-      result.get(0)[0] += startPhase;
-    }
-
-    /*
      * Finally sort ranges by start position. This avoids a dependency on 
      * keeping features in order on the sequence (if they are in order anyway,
      * the sort will have almost no work to do). The implicit assumption is CDS
@@ -2365,15 +2438,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base + base2 + base3;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base1.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base.toUpperCase() + base2.toLowerCase()
+                      + base3.toLowerCase();
+              String canonical = base1.toUpperCase() + base2.toLowerCase()
+                      + base3.toLowerCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2387,15 +2467,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base1 + base + base3;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base2.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base1.toLowerCase() + base.toUpperCase()
+                      + base3.toLowerCase();
+              String canonical = base1.toLowerCase() + base2.toUpperCase()
+                      + base3.toLowerCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2409,15 +2496,22 @@ public class AlignmentUtils
     {
       if (var.variant != null)
       {
-        String alleles = (String) var.variant.getValue("alleles");
+        String alleles = (String) var.variant.getValue(Gff3Helper.ALLELES);
         if (alleles != null)
         {
           for (String base : alleles.split(","))
           {
-            String codon = base1 + base2 + base;
-            if (addPeptideVariant(peptide, peptidePos, residue, var, codon))
+            if (!base3.equalsIgnoreCase(base))
             {
-              count++;
+              String codon = base1.toLowerCase() + base2.toLowerCase()
+                      + base.toUpperCase();
+              String canonical = base1.toLowerCase() + base2.toLowerCase()
+                      + base3.toUpperCase();
+              if (addPeptideVariant(peptide, peptidePos, residue, var,
+                      codon, canonical))
+              {
+                count++;
+              }
             }
           }
         }
@@ -2428,20 +2522,22 @@ public class AlignmentUtils
   }
 
   /**
-   * Helper method that adds a peptide variant feature, provided the given codon
-   * translates to a value different to the current residue (is a non-synonymous
-   * variant). ID and clinical_significance attributes of the dna variant (if
-   * present) are copied to the new feature.
+   * Helper method that adds a peptide variant feature. ID and
+   * clinical_significance attributes of the dna variant (if present) are copied
+   * to the new feature.
    * 
    * @param peptide
    * @param peptidePos
    * @param residue
    * @param var
    * @param codon
+   *          the variant codon e.g. aCg
+   * @param canonical
+   *          the 'normal' codon e.g. aTg
    * @return true if a feature was added, else false
    */
   static boolean addPeptideVariant(SequenceI peptide, int peptidePos,
-          String residue, DnaVariant var, String codon)
+          String residue, DnaVariant var, String codon, String canonical)
   {
     /*
      * get peptide translation of codon e.g. GAT -> D
@@ -2449,62 +2545,79 @@ public class AlignmentUtils
      * e.g. multibase variants or HGMD_MUTATION etc
      * are currently ignored here
      */
-    String trans = codon.contains("-") ? "-"
+    String trans = codon.contains("-") ? null
             : (codon.length() > CODON_LENGTH ? null
                     : ResidueProperties.codonTranslate(codon));
-    if (trans != null && !trans.equals(residue))
+    if (trans == null)
+    {
+      return false;
+    }
+    String desc = canonical + "/" + codon;
+    String featureType = "";
+    if (trans.equals(residue))
+    {
+      featureType = SequenceOntologyI.SYNONYMOUS_VARIANT;
+    }
+    else if (ResidueProperties.STOP.equals(trans))
+    {
+      featureType = SequenceOntologyI.STOP_GAINED;
+    }
+    else
     {
       String residue3Char = StringUtils
               .toSentenceCase(ResidueProperties.aa2Triplet.get(residue));
       String trans3Char = StringUtils
               .toSentenceCase(ResidueProperties.aa2Triplet.get(trans));
-      String desc = "p." + residue3Char + peptidePos + trans3Char;
-      SequenceFeature sf = new SequenceFeature(
-              SequenceOntologyI.SEQUENCE_VARIANT, desc, peptidePos,
-              peptidePos, var.getSource());
-      StringBuilder attributes = new StringBuilder(32);
-      String id = (String) var.variant.getValue(ID);
-      if (id != null)
-      {
-        if (id.startsWith(SEQUENCE_VARIANT))
-        {
-          id = id.substring(SEQUENCE_VARIANT.length());
-        }
-        sf.setValue(ID, id);
-        attributes.append(ID).append("=").append(id);
-        // TODO handle other species variants JAL-2064
-        StringBuilder link = new StringBuilder(32);
-        try
-        {
-          link.append(desc).append(" ").append(id).append(
-                  "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
-                  .append(URLEncoder.encode(id, "UTF-8"));
-          sf.addLink(link.toString());
-        } catch (UnsupportedEncodingException e)
-        {
-          // as if
-        }
-      }
-      String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
-      if (clinSig != null)
+      desc = "p." + residue3Char + peptidePos + trans3Char;
+      featureType = SequenceOntologyI.NONSYNONYMOUS_VARIANT;
+    }
+    SequenceFeature sf = new SequenceFeature(featureType, desc, peptidePos,
+            peptidePos, var.getSource());
+
+    StringBuilder attributes = new StringBuilder(32);
+    String id = (String) var.variant.getValue(ID);
+    if (id != null)
+    {
+      if (id.startsWith(SEQUENCE_VARIANT))
       {
-        sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
-        attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
-                .append(clinSig);
+        id = id.substring(SEQUENCE_VARIANT.length());
       }
-      peptide.addSequenceFeature(sf);
-      if (attributes.length() > 0)
+      sf.setValue(ID, id);
+      attributes.append(ID).append("=").append(id);
+      // TODO handle other species variants JAL-2064
+      StringBuilder link = new StringBuilder(32);
+      try
       {
-        sf.setAttributes(attributes.toString());
+        link.append(desc).append(" ").append(id).append(
+                "|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=")
+                .append(URLEncoder.encode(id, "UTF-8"));
+        sf.addLink(link.toString());
+      } catch (UnsupportedEncodingException e)
+      {
+        // as if
       }
-      return true;
     }
-    return false;
+    String clinSig = (String) var.variant.getValue(CLINICAL_SIGNIFICANCE);
+    if (clinSig != null)
+    {
+      sf.setValue(CLINICAL_SIGNIFICANCE, clinSig);
+      attributes.append(";").append(CLINICAL_SIGNIFICANCE).append("=")
+              .append(clinSig);
+    }
+    peptide.addSequenceFeature(sf);
+    if (attributes.length() > 0)
+    {
+      sf.setAttributes(attributes.toString());
+    }
+    return true;
   }
 
   /**
    * Builds a map whose key is position in the protein sequence, and value is a
-   * list of the base and all variants for each corresponding codon position
+   * list of the base and all variants for each corresponding codon position.
+   * <p>
+   * This depends on dna variants being held as a comma-separated list as
+   * property "alleles" on variant features.
    * 
    * @param dnaSeq
    * @param dnaToProtein
@@ -2518,7 +2631,7 @@ public class AlignmentUtils
      * map from peptide position to all variants of the codon which codes for it
      * LinkedHashMap ensures we keep the peptide features in sequence order
      */
-    LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<Integer, List<DnaVariant>[]>();
+    LinkedHashMap<Integer, List<DnaVariant>[]> variants = new LinkedHashMap<>();
 
     List<SequenceFeature> dnaFeatures = dnaSeq.getFeatures()
             .getFeaturesByOntology(SequenceOntologyI.SEQUENCE_VARIANT);
@@ -2542,6 +2655,30 @@ public class AlignmentUtils
         // not handling multi-locus variant features
         continue;
       }
+
+      /*
+       * ignore variant if not a SNP
+       */
+      String alls = (String) sf.getValue(Gff3Helper.ALLELES);
+      if (alls == null)
+      {
+        continue; // non-SNP VCF variant perhaps - can't process this
+      }
+
+      String[] alleles = alls.toUpperCase().split(",");
+      boolean isSnp = true;
+      for (String allele : alleles)
+      {
+        if (allele.trim().length() > 1)
+        {
+          isSnp = false;
+        }
+      }
+      if (!isSnp)
+      {
+        continue;
+      }
+
       int[] mapsTo = dnaToProtein.locateInTo(dnaCol, dnaCol);
       if (mapsTo == null)
       {
@@ -2553,28 +2690,13 @@ public class AlignmentUtils
       if (codonVariants == null)
       {
         codonVariants = new ArrayList[CODON_LENGTH];
-        codonVariants[0] = new ArrayList<DnaVariant>();
-        codonVariants[1] = new ArrayList<DnaVariant>();
-        codonVariants[2] = new ArrayList<DnaVariant>();
+        codonVariants[0] = new ArrayList<>();
+        codonVariants[1] = new ArrayList<>();
+        codonVariants[2] = new ArrayList<>();
         variants.put(peptidePosition, codonVariants);
       }
 
       /*
-       * extract dna variants to a string array
-       */
-      String alls = (String) sf.getValue("alleles");
-      if (alls == null)
-      {
-        continue;
-      }
-      String[] alleles = alls.toUpperCase().split(",");
-      int i = 0;
-      for (String allele : alleles)
-      {
-        alleles[i++] = allele.trim(); // lose any space characters "A, G"
-      }
-
-      /*
        * get this peptide's codon positions e.g. [3, 4, 5] or [4, 7, 10]
        */
       int[] codon = peptidePosition == lastPeptidePostion ? lastCodon
@@ -2694,7 +2816,7 @@ public class AlignmentUtils
     /*
      * fancy case - aligning via mappings between sequences
      */
-    List<SequenceI> unmapped = new ArrayList<SequenceI>();
+    List<SequenceI> unmapped = new ArrayList<>();
     Map<Integer, Map<SequenceI, Character>> columnMap = buildMappedColumnsMap(
             unaligned, aligned, unmapped);
     int width = columnMap.size();
@@ -2769,7 +2891,7 @@ public class AlignmentUtils
     }
 
     // map from dataset sequence to alignment sequence(s)
-    Map<SequenceI, List<SequenceI>> alignedDatasets = new HashMap<SequenceI, List<SequenceI>>();
+    Map<SequenceI, List<SequenceI>> alignedDatasets = new HashMap<>();
     for (SequenceI seq : aligned.getSequences())
     {
       SequenceI ds = seq.getDatasetSequence();
@@ -2832,7 +2954,7 @@ public class AlignmentUtils
      * {unalignedSequence, characterPerSequence} at that position.
      * TreeMap keeps the entries in ascending column order. 
      */
-    SortedMap<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
+    SortedMap<Integer, Map<SequenceI, Character>> map = new TreeMap<>();
 
     /*
      * record any sequences that have no mapping so can't be realigned
@@ -2937,7 +3059,7 @@ public class AlignmentUtils
             Map<SequenceI, Character> seqsMap = map.get(fromCol);
             if (seqsMap == null)
             {
-              seqsMap = new HashMap<SequenceI, Character>();
+              seqsMap = new HashMap<>();
               map.put(fromCol, seqsMap);
             }
             seqsMap.put(seq, seq.getCharAt(mappedCharPos - toStart));
index a10b037..2ad8487 100644 (file)
@@ -44,6 +44,7 @@ import jalview.util.ShiftList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.List;
 
 public class Dna
@@ -56,19 +57,23 @@ public class Dna
    * 'final' variables describe the inputs to the translation, which should not
    * be modified.
    */
-  final private List<SequenceI> selection;
+  private final List<SequenceI> selection;
 
-  final private String[] seqstring;
+  private final String[] seqstring;
 
-  final private int[] contigs;
+  private final Iterator<int[]> contigs;
 
-  final private char gapChar;
+  private final char gapChar;
 
-  final private AlignmentAnnotation[] annotations;
+  private final AlignmentAnnotation[] annotations;
 
-  final private int dnaWidth;
+  private final int dnaWidth;
 
-  final private AlignmentI dataset;
+  private final AlignmentI dataset;
+
+  private ShiftList vismapping;
+
+  private int[] startcontigs;
 
   /*
    * Working variables for the translation.
@@ -91,7 +96,7 @@ public class Dna
    * @param viewport
    * @param visibleContigs
    */
-  public Dna(AlignViewportI viewport, int[] visibleContigs)
+  public Dna(AlignViewportI viewport, Iterator<int[]> visibleContigs)
   {
     this.selection = Arrays.asList(viewport.getSequenceSelection());
     this.seqstring = viewport.getViewAsString(true);
@@ -100,6 +105,45 @@ public class Dna
     this.annotations = viewport.getAlignment().getAlignmentAnnotation();
     this.dnaWidth = viewport.getAlignment().getWidth();
     this.dataset = viewport.getAlignment().getDataset();
+    initContigs();
+  }
+
+  /**
+   * Initialise contigs used as starting point for translateCodingRegion
+   */
+  private void initContigs()
+  {
+    vismapping = new ShiftList(); // map from viscontigs to seqstring
+    // intervals
+
+    int npos = 0;
+    int[] lastregion = null;
+    ArrayList<Integer> tempcontigs = new ArrayList<>();
+    while (contigs.hasNext())
+    {
+      int[] region = contigs.next();
+      if (lastregion == null)
+      {
+        vismapping.addShift(npos, region[0]);
+      }
+      else
+      {
+        // hidden region
+        vismapping.addShift(npos, region[0] - lastregion[1] + 1);
+      }
+      lastregion = region;
+      tempcontigs.add(region[0]);
+      tempcontigs.add(region[1]);
+    }
+
+    startcontigs = new int[tempcontigs.size()];
+    int i = 0;
+    for (Integer val : tempcontigs)
+    {
+      startcontigs[i] = val;
+      i++;
+    }
+    tempcontigs = null;
   }
 
   /**
@@ -161,7 +205,7 @@ public class Dna
 
     int s;
     int sSize = selection.size();
-    List<SequenceI> pepseqs = new ArrayList<SequenceI>();
+    List<SequenceI> pepseqs = new ArrayList<>();
     for (s = 0; s < sSize; s++)
     {
       SequenceI newseq = translateCodingRegion(selection.get(s),
@@ -213,7 +257,7 @@ public class Dna
       if (dnarefs != null)
       {
         // intersect with pep
-        List<DBRefEntry> mappedrefs = new ArrayList<DBRefEntry>();
+        List<DBRefEntry> mappedrefs = new ArrayList<>();
         DBRefEntry[] refs = dna.getDBRefs();
         for (int d = 0; d < refs.length; d++)
         {
@@ -391,27 +435,14 @@ public class Dna
           String seqstring, AlignedCodonFrame acf,
           List<SequenceI> proteinSeqs)
   {
-    List<int[]> skip = new ArrayList<int[]>();
-    int skipint[] = null;
-    ShiftList vismapping = new ShiftList(); // map from viscontigs to seqstring
-    // intervals
-    int vc;
-    int[] scontigs = new int[contigs.length];
+    List<int[]> skip = new ArrayList<>();
+    int[] skipint = null;
+
     int npos = 0;
-    for (vc = 0; vc < contigs.length; vc += 2)
-    {
-      if (vc == 0)
-      {
-        vismapping.addShift(npos, contigs[vc]);
-      }
-      else
-      {
-        // hidden region
-        vismapping.addShift(npos, contigs[vc] - contigs[vc - 1] + 1);
-      }
-      scontigs[vc] = contigs[vc];
-      scontigs[vc + 1] = contigs[vc + 1];
-    }
+    int vc = 0;
+
+    int[] scontigs = new int[startcontigs.length];
+    System.arraycopy(startcontigs, 0, scontigs, 0, startcontigs.length);
 
     // allocate a roughly sized buffer for the protein sequence
     StringBuilder protein = new StringBuilder(seqstring.length() / 2);
@@ -544,7 +575,7 @@ public class Dna
             skip.add(skipint);
             skipint = null;
           }
-          if (aa.equals("STOP"))
+          if (aa.equals(ResidueProperties.STOP))
           {
             aa = STOP_ASTERIX;
           }
@@ -800,7 +831,7 @@ public class Dna
   public AlignmentI reverseCdna(boolean complement)
   {
     int sSize = selection.size();
-    List<SequenceI> reversed = new ArrayList<SequenceI>();
+    List<SequenceI> reversed = new ArrayList<>();
     for (int s = 0; s < sSize; s++)
     {
       SequenceI newseq = reverseSequence(selection.get(s).getName(),
@@ -851,6 +882,23 @@ public class Dna
   }
 
   /**
+   * Answers the reverse complement of the input string
+   * 
+   * @see #getComplement(char)
+   * @param s
+   * @return
+   */
+  public static String reverseComplement(String s)
+  {
+    StringBuilder sb = new StringBuilder(s.length());
+    for (int i = s.length() - 1; i >= 0; i--)
+    {
+      sb.append(Dna.getComplement(s.charAt(i)));
+    }
+    return sb.toString();
+  }
+
+  /**
    * Returns dna complement (preserving case) for aAcCgGtTuU. Ambiguity codes
    * are treated as on http://reverse-complement.com/. Anything else is left
    * unchanged.
index 9e6d1c0..931eba6 100644 (file)
 package jalview.api;
 
 import jalview.analysis.Conservation;
+import jalview.analysis.TreeModel;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
-import jalview.datamodel.CigarArray;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.ProfilesI;
 import jalview.datamodel.SearchResultsI;
@@ -243,16 +243,6 @@ public interface AlignViewportI extends ViewStyleI
   void clearSequenceColours();
 
   /**
-   * This method returns the visible alignment as text, as seen on the GUI, ie
-   * if columns are hidden they will not be returned in the result. Use this for
-   * calculating trees, PCA, redundancy etc on views which contain hidden
-   * columns.
-   * 
-   * @return String[]
-   */
-  CigarArray getViewAsCigars(boolean selectedRegionOnly);
-
-  /**
    * return a compact representation of the current alignment selection to pass
    * to an analysis function
    * 
@@ -486,6 +476,7 @@ public interface AlignViewportI extends ViewStyleI
    * 
    * @return
    */
+  @Override
   boolean isProteinFontAsCdna();
 
   /**
@@ -493,5 +484,10 @@ public interface AlignViewportI extends ViewStyleI
    * 
    * @return
    */
+  @Override
   void setProteinFontAsCdna(boolean b);
+
+  public abstract TreeModel getCurrentTree();
+
+  public abstract void setCurrentTree(TreeModel tree);
 }
index ef59996..0b1ca21 100644 (file)
@@ -43,9 +43,10 @@ public interface AlignmentViewPanel extends OOMHandlerI
    * 
    * @param updateOverview
    *          - if true, the overview panel will also be updated and repainted
+   * @param updateStructures
+   *          - if true then any linked structure views will also be updated
    */
-
-  void paintAlignment(boolean updateOverview);
+  void paintAlignment(boolean updateOverview, boolean updateStructures);
 
   /**
    * automatically adjust annotation panel height for new annotation whilst
index 0ded079..4dbb1bb 100644 (file)
@@ -56,6 +56,14 @@ public interface FeatureColourI
   Color getMaxColour();
 
   /**
+   * Returns the 'no value' colour (used when a feature lacks score, or the
+   * attribute, being used for colouring)
+   * 
+   * @return
+   */
+  Color getNoColour();
+
+  /**
    * Answers true if the feature has a single colour, i.e. if isColourByLabel()
    * and isGraduatedColour() both answer false
    * 
@@ -64,7 +72,8 @@ public interface FeatureColourI
   boolean isSimpleColour();
 
   /**
-   * Answers true if the feature is coloured by label (description)
+   * Answers true if the feature is coloured by label (description) or by text
+   * value of an attribute
    * 
    * @return
    */
@@ -93,18 +102,6 @@ public interface FeatureColourI
   void setAboveThreshold(boolean b);
 
   /**
-   * Answers true if the threshold is the minimum value (when
-   * isAboveThreshold()) or maximum value (when isBelowThreshold()) of the
-   * colour range; only applicable when isGraduatedColour and either
-   * isAboveThreshold() or isBelowThreshold() answers true
-   * 
-   * @return
-   */
-  boolean isThresholdMinMax();
-
-  void setThresholdMinMax(boolean b);
-
-  /**
    * Returns the threshold value (if any), else zero
    * 
    * @return
@@ -156,7 +153,10 @@ public interface FeatureColourI
   Color getColor(SequenceFeature feature);
 
   /**
-   * Update the min-max range for a graduated colour scheme
+   * Update the min-max range for a graduated colour scheme. Note that the
+   * colour scheme may be configured to colour by feature score, or a
+   * (numeric-valued) attribute - the caller should ensure that the correct
+   * range is being set.
    * 
    * @param min
    * @param max
@@ -169,4 +169,27 @@ public interface FeatureColourI
    * @return
    */
   String toJalviewFormat(String featureType);
+
+  /**
+   * Answers true if colour is by attribute text or numerical value
+   * 
+   * @return
+   */
+  boolean isColourByAttribute();
+
+  /**
+   * Answers the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null
+   * 
+   * @return
+   */
+  String[] getAttributeName();
+
+  /**
+   * Sets the name of the attribute (and optional sub-attribute...) used for
+   * colouring if any, or null to remove this property
+   * 
+   * @return
+   */
+  void setAttributeName(String... name);
 }
index 9d2d7f4..cf3c8da 100644 (file)
@@ -22,6 +22,7 @@ package jalview.api;
 
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 
 import java.awt.Color;
 import java.awt.Graphics;
@@ -132,7 +133,7 @@ public interface FeatureRenderer
   List<String> getGroups(boolean visible);
 
   /**
-   * change visibility for a range of groups
+   * Set visibility for a list of groups
    * 
    * @param toset
    * @param visible
@@ -140,7 +141,7 @@ public interface FeatureRenderer
   void setGroupVisibility(List<String> toset, boolean visible);
 
   /**
-   * change visibiilty of given group
+   * Set visibility of the given feature group
    * 
    * @param group
    * @param visible
@@ -148,9 +149,9 @@ public interface FeatureRenderer
   void setGroupVisibility(String group, boolean visible);
 
   /**
-   * Returns features at the specified aligned column on the given sequence.
-   * Non-positional features are not included. If the column has a gap, then
-   * enclosing features are included (but not contact features).
+   * Returns visible features at the specified aligned column on the given
+   * sequence. Non-positional features are not included. If the column has a gap,
+   * then enclosing features are included (but not contact features).
    * 
    * @param sequence
    * @param column
@@ -215,4 +216,53 @@ public interface FeatureRenderer
    */
   float getTransparency();
 
+  /**
+   * Answers the filters applied to the given feature type, or null if none is
+   * set
+   * 
+   * @param featureType
+   * @return
+   */
+  FeatureMatcherSetI getFeatureFilter(String featureType);
+
+  /**
+   * Answers the feature filters map
+   * 
+   * @return
+   */
+  public Map<String, FeatureMatcherSetI> getFeatureFilters();
+
+  /**
+   * Sets the filters for the feature type, or removes them if a null or empty
+   * filter is passed
+   * 
+   * @param featureType
+   * @param filter
+   */
+  void setFeatureFilter(String featureType, FeatureMatcherSetI filter);
+
+  /**
+   * Replaces all feature filters with the given map
+   * 
+   * @param filters
+   */
+  void setFeatureFilters(Map<String, FeatureMatcherSetI> filters);
+
+  /**
+   * Returns the colour for a particular feature instance. This includes
+   * calculation of 'colour by label', or of a graduated score colour, if
+   * applicable.
+   * <p>
+   * Returns null if
+   * <ul>
+   * <li>feature type is not visible, or</li>
+   * <li>feature group is not visible, or</li>
+   * <li>feature values lie outside any colour threshold, or</li>
+   * <li>feature is excluded by filter conditions</li>
+   * </ul>
+   * 
+   * @param feature
+   * @return
+   */
+  Color getColour(SequenceFeature feature);
 }
index fd66388..8f778f7 100644 (file)
@@ -20,6 +20,9 @@
  */
 package jalview.api.structures;
 
+import jalview.api.AlignmentViewPanel;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SequenceI;
 import jalview.schemes.ColourSchemeI;
 import jalview.structures.models.AAStructureBindingModel;
 
@@ -62,4 +65,64 @@ public interface JalviewStructureDisplayI
    */
   void setJalviewColourScheme(ColourSchemeI colourScheme);
 
+  /**
+   * 
+   * @return true if all background sequence/structure binding threads have
+   *         completed for this viewer instance
+   */
+  boolean hasMapping();
+
+  /**
+   * Checks if the PDB file is already loaded in this viewer, if so just adds
+   * mappings as necessary and answers true, else answers false. This supports
+   * the use case of adding additional chains of the same structure to a viewer.
+   * 
+   * @param seq
+   * @param chains
+   * @param apanel
+   * @param pdbId
+   * @return
+   */
+  boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
+          AlignmentViewPanel apanel, String pdbId);
+
+  /**
+   * Adds one or more chains (sequences) of a PDB structure to this structure
+   * viewer
+   * 
+   * @param pdbentry
+   * @param seq
+   * @param chains
+   * @param apanel
+   * @param pdbId
+   * @return
+   */
+  void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
+          String[] chains, AlignmentViewPanel apanel, String pdbId);
+
+  /**
+   * refresh GUI after reconfiguring structure(s) and alignment panels
+   */
+  void updateTitleAndMenus();
+
+  /**
+   * Answers true if the viewer should attempt to align any added structures,
+   * else false
+   * 
+   * @return
+   */
+  boolean isAlignAddedStructures();
+
+  /**
+   * Sets the flag for whether added structures should be aligned
+   * 
+   * @param alignAdded
+   */
+  void setAlignAddedStructures(boolean alignAdded);
+
+  /**
+   * Raise the panel to the top of the stack...
+   */
+  void raiseViewer();
+
 }
index 9d44479..76f2705 100644 (file)
@@ -383,7 +383,7 @@ public class APopupMenu extends java.awt.PopupMenu
   void addFeatureLinks(final SequenceI seq, List<String> links)
   {
     Menu linkMenu = new Menu(MessageManager.getString("action.link"));
-    Map<String, List<String>> linkset = new LinkedHashMap<String, List<String>>();
+    Map<String, List<String>> linkset = new LinkedHashMap<>();
 
     for (String link : links)
     {
@@ -485,8 +485,8 @@ public class APopupMenu extends java.awt.PopupMenu
      * Temporary store to hold distinct calcId / type pairs for the tooltip.
      * Using TreeMap means calcIds are shown in alphabetical order.
      */
-    SortedMap<String, String> tipEntries = new TreeMap<String, String>();
-    final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<SequenceI, List<AlignmentAnnotation>>();
+    SortedMap<String, String> tipEntries = new TreeMap<>();
+    final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
     AlignmentI al = this.ap.av.getAlignment();
     AlignmentUtils.findAddableReferenceAnnotations(forSequences, tipEntries,
             candidates, al);
@@ -825,8 +825,8 @@ public class APopupMenu extends java.awt.PopupMenu
       }
 
       int gSize = sg.getSize();
-      List<SequenceI> seqs = new ArrayList<SequenceI>();
-      List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+      List<SequenceI> seqs = new ArrayList<>();
+      List<SequenceFeature> features = new ArrayList<>();
 
       for (int i = 0; i < gSize; i++)
       {
@@ -901,10 +901,7 @@ public class APopupMenu extends java.awt.PopupMenu
               .formatMessage("label.annotation_for_displayid", new Object[]
               { seq.getDisplayId(true) }));
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.seqPanel.seqCanvas.fr != null)
-                      ? ap.seqPanel.seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.seqPanel.seqCanvas.fr);
       contents.append("</p>");
     }
     Frame frame = new Frame();
@@ -930,7 +927,7 @@ public class APopupMenu extends java.awt.PopupMenu
     {
       seq.setName(dialog.getName());
       seq.setDescription(dialog.getDescription());
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -1163,7 +1160,7 @@ public class APopupMenu extends java.awt.PopupMenu
 
   void refresh()
   {
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   protected void clustalColour_actionPerformed()
@@ -1339,7 +1336,7 @@ public class APopupMenu extends java.awt.PopupMenu
     SequenceGroup sg = ap.av.getSelectionGroup();
     ap.av.getAlignment().deleteGroup(sg);
     ap.av.setSelectionGroup(null);
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   void createGroupMenuItem_actionPerformed()
@@ -1440,8 +1437,8 @@ public class APopupMenu extends java.awt.PopupMenu
      * the insertion order, which is the order of the annotations on the
      * alignment.
      */
-    Map<String, List<List<String>>> shownTypes = new LinkedHashMap<String, List<List<String>>>();
-    Map<String, List<List<String>>> hiddenTypes = new LinkedHashMap<String, List<List<String>>>();
+    Map<String, List<List<String>>> shownTypes = new LinkedHashMap<>();
+    Map<String, List<List<String>>> hiddenTypes = new LinkedHashMap<>();
     AlignmentAnnotationUtils.getShownHiddenTypes(shownTypes, hiddenTypes,
             AlignmentAnnotationUtils.asList(annotations), forSequences);
 
index ed04a0a..5ad212e 100644 (file)
@@ -343,7 +343,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     createAlignFrameWindow(embedded);
     validate();
     alignPanel.adjustAnnotationHeight();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   public AlignViewport getAlignViewport()
@@ -415,7 +415,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
       {
         viewport.featureSettings.refreshTable();
       }
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       statusBar.setText(MessageManager
               .getString("label.successfully_added_features_alignment"));
     }
@@ -691,7 +691,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
       break;
 
     }
-    alignPanel.paintAlignment(true);
+    // TODO: repaint flags set only if the keystroke warrants it
+    alignPanel.paintAlignment(true, true);
   }
 
   /**
@@ -917,7 +918,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       applyAutoAnnotationSettings_actionPerformed();
     }
-    alignPanel.paintAlignment(true);
+    // TODO: repaint flags set only if warranted
+    alignPanel.paintAlignment(true, true);
   }
 
   /**
@@ -1094,7 +1096,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     else if (source == invertColSel)
     {
       viewport.invertColumnSelection();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(false, false);
       viewport.sendSelection();
     }
     else if (source == remove2LeftMenuItem)
@@ -1128,34 +1130,34 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     else if (source == showColumns)
     {
       viewport.showAllHiddenColumns();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       viewport.sendSelection();
     }
     else if (source == showSeqs)
     {
       viewport.showAllHiddenSeqs();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       // uncomment if we want to slave sequence selections in split frame
       // viewport.sendSelection();
     }
     else if (source == hideColumns)
     {
       viewport.hideSelectedColumns();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       viewport.sendSelection();
     }
     else if (source == hideSequences
             && viewport.getSelectionGroup() != null)
     {
       viewport.hideAllSelectedSeqs();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       // uncomment if we want to slave sequence selections in split frame
       // viewport.sendSelection();
     }
     else if (source == hideAllButSelection)
     {
       toggleHiddenRegions(false, false);
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       viewport.sendSelection();
     }
     else if (source == hideAllSelection)
@@ -1164,14 +1166,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
       viewport.expandColSelection(sg, false);
       viewport.hideAllSelectedSeqs();
       viewport.hideSelectedColumns();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       viewport.sendSelection();
     }
     else if (source == showAllHidden)
     {
       viewport.showAllHiddenColumns();
       viewport.showAllHiddenSeqs();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
       viewport.sendSelection();
     }
     else if (source == showGroupConsensus)
@@ -1443,9 +1445,10 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     FeaturesFile formatter = new FeaturesFile();
     if (format.equalsIgnoreCase("Jalview"))
     {
-      features = formatter.printJalviewFormat(viewport.getAlignment()
-              .getSequencesArray(), getDisplayedFeatureCols(),
-              getDisplayedFeatureGroups(), true);
+      features = formatter.printJalviewFormat(
+              viewport.getAlignment().getSequencesArray(),
+              getDisplayedFeatureCols(), null, getDisplayedFeatureGroups(),
+              true);
     }
     else
     {
@@ -1601,10 +1604,12 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       System.exit(0);
     }
-    else
+
+    viewport = null;
+    if (alignPanel != null && alignPanel.overviewPanel != null)
     {
+      alignPanel.overviewPanel.dispose();
     }
-    viewport = null;
     alignPanel = null;
     this.dispose();
   }
@@ -1781,7 +1786,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     }
     viewport.getAlignment().moveSelectedSequencesByOne(sg,
             up ? null : viewport.getHiddenRepSequences(), up);
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
 
     /*
      * Also move cDNA/protein complement sequences
@@ -1793,7 +1798,8 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
               viewport, complement);
       complement.getAlignment().moveSelectedSequencesByOne(mappedSelection,
               up ? null : complement.getHiddenRepSequences(), up);
-      getSplitFrame().getComplement(this).alignPanel.paintAlignment(true);
+      getSplitFrame().getComplement(this).alignPanel.paintAlignment(true,
+              false);
     }
   }
 
@@ -1900,7 +1906,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
   static StringBuffer copiedSequences;
 
-  static Vector<int[]> copiedHiddenColumns;
+  static HiddenColumns copiedHiddenColumns;
 
   protected void copy_actionPerformed()
   {
@@ -1924,14 +1930,14 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
     if (viewport.hasHiddenColumns() && viewport.getSelectionGroup() != null)
     {
-      copiedHiddenColumns = new Vector<>(viewport.getAlignment()
-              .getHiddenColumns().getHiddenColumnsCopy());
       int hiddenOffset = viewport.getSelectionGroup().getStartRes();
-      for (int[] region : copiedHiddenColumns)
-      {
-        region[0] = region[0] - hiddenOffset;
-        region[1] = region[1] - hiddenOffset;
-      }
+      int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
+
+      // create new HiddenColumns object with copy of hidden regions
+      // between startRes and endRes, offset by startRes
+      copiedHiddenColumns = new HiddenColumns(
+              viewport.getAlignment().getHiddenColumns(), hiddenOffset,
+              hiddenCutoff, hiddenOffset);
     }
     else
     {
@@ -2000,13 +2006,13 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   {
     try
     {
-
       if (copiedSequences == null)
       {
         return;
       }
 
-      StringTokenizer st = new StringTokenizer(copiedSequences.toString());
+      StringTokenizer st = new StringTokenizer(copiedSequences.toString(),
+              "\t");
       Vector seqs = new Vector();
       while (st.hasMoreElements())
       {
@@ -2038,14 +2044,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
         }
         AlignFrame af = new AlignFrame(new Alignment(newSeqs),
                 viewport.applet, newtitle, false);
-        if (copiedHiddenColumns != null)
-        {
-          for (int i = 0; i < copiedHiddenColumns.size(); i++)
-          {
-            int[] region = copiedHiddenColumns.elementAt(i);
-            af.viewport.hideColumns(region[0], region[1]);
-          }
-        }
+        af.viewport.setHiddenColumns(copiedHiddenColumns);
 
         jalview.bin.JalviewLite.addFrame(af, newtitle, frameWidth,
                 frameHeight);
@@ -2226,7 +2225,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -2263,7 +2262,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
     // updating.
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
     viewport.sendSelection();
   }
@@ -2283,7 +2282,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
     // updating.
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
     viewport.sendSelection();
   }
@@ -2303,7 +2302,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   public void invertColSel_actionPerformed()
   {
     viewport.invertColumnSelection();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
     viewport.sendSelection();
   }
@@ -2595,19 +2594,19 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   {
     viewport.setShowJVSuffix(seqLimits.getState());
     alignPanel.fontChanged();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   protected void colourTextMenuItem_actionPerformed()
   {
     viewport.setColourText(colourTextMenuItem.getState());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   protected void displayNonconservedMenuItem_actionPerformed()
   {
     viewport.setShowUnconserved(displayNonconservedMenuItem.getState());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   protected void wrapMenuItem_actionPerformed()
@@ -2617,7 +2616,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     scaleAbove.setEnabled(wrapMenuItem.getState());
     scaleLeft.setEnabled(wrapMenuItem.getState());
     scaleRight.setEnabled(wrapMenuItem.getState());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   public void overviewMenuItem_actionPerformed()
@@ -2660,7 +2659,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
   {
     viewport.setGlobalColourScheme(cs);
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   protected void modifyPID_actionPerformed()
@@ -2735,7 +2734,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
 
     addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   public void sortIDMenuItem_actionPerformed()
@@ -2744,7 +2743,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     AlignmentSorter.sortByID(viewport.getAlignment());
     addHistoryItem(
             new OrderCommand("ID Sort", oldOrder, viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   public void sortLengthMenuItem_actionPerformed()
@@ -2753,7 +2752,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     AlignmentSorter.sortByLength(viewport.getAlignment());
     addHistoryItem(new OrderCommand("Length Sort", oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   public void sortGroupMenuItem_actionPerformed()
@@ -2762,7 +2761,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     AlignmentSorter.sortByGroup(viewport.getAlignment());
     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
 
   }
 
@@ -2802,7 +2801,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
           current.insertCharAt(Width - 1, viewport.getGapCharacter());
         }
       }
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(false, false);
     }
 
     if ((viewport.getSelectionGroup() != null
@@ -2866,7 +2865,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
           current.insertCharAt(Width - 1, viewport.getGapCharacter());
         }
       }
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(false, false);
 
     }
 
@@ -2919,7 +2918,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     addHistoryItem(new OrderCommand(MessageManager
             .formatMessage("label.order_by_params", new String[]
             { title }), oldOrder, viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -2977,7 +2976,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
       addHistoryItem(new OrderCommand(undoname, oldOrder,
               viewport.getAlignment()));
     }
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
     return true;
   }
 
@@ -4144,7 +4143,7 @@ public class AlignFrame extends EmbmenuFrame implements ActionListener,
     {
       // register the association(s) and quit, don't create any windows.
       if (StructureSelectionManager.getStructureSelectionManager(applet)
-              .setMapping(seqs, chains, pdb.getFile(), protocol) == null)
+              .setMapping(seqs, chains, pdb.getFile(), protocol, null) == null)
       {
         System.err.println("Failed to map " + pdb.getFile() + " ("
                 + protocol + ") to any sequences");
index b07666e..262948d 100644 (file)
@@ -20,7 +20,6 @@
  */
 package jalview.appletgui;
 
-import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.bin.JalviewLite;
@@ -54,23 +53,12 @@ public class AlignViewport extends AlignmentViewport
 
   boolean validCharWidth = true;
 
-  TreeModel currentTree = null;
-
   public jalview.bin.JalviewLite applet;
 
   boolean MAC = false;
 
   private AnnotationColumnChooser annotationColumnSelectionState;
 
-  @Override
-  public void finalize()
-  {
-    applet = null;
-    quality = null;
-    alignment = null;
-    colSel = null;
-  }
-
   public AlignViewport(AlignmentI al, JalviewLite applet)
   {
     super(al);
@@ -274,16 +262,6 @@ public class AlignViewport extends AlignmentViewport
     ranges.setEndSeq(height / getCharHeight());
   }
 
-  public void setCurrentTree(TreeModel tree)
-  {
-    currentTree = tree;
-  }
-
-  public TreeModel getCurrentTree()
-  {
-    return currentTree;
-  }
-
   boolean centreColumnLabels;
 
   public boolean getCentreColumnLabels()
index 8e333ba..83d8ade 100644 (file)
@@ -73,23 +73,6 @@ public class AlignmentPanel extends Panel
   // this value is set false when selection area being dragged
   boolean fastPaint = true;
 
-  @Override
-  public void finalize() throws Throwable
-  {
-    alignFrame = null;
-    av = null;
-    vpRanges = null;
-    seqPanel = null;
-    seqPanelHolder = null;
-    sequenceHolderPanel = null;
-    scalePanel = null;
-    scalePanelHolder = null;
-    annotationPanel = null;
-    annotationPanelHolder = null;
-    annotationSpaceFillerHolder = null;
-    super.finalize();
-  }
-
   public AlignmentPanel(AlignFrame af, final AlignViewport av)
   {
     try
@@ -438,8 +421,8 @@ public class AlignmentPanel extends Panel
     if (av.hasHiddenColumns())
     {
       AlignmentI al = av.getAlignment();
-      start = al.getHiddenColumns().findColumnPosition(ostart);
-      end = al.getHiddenColumns().findColumnPosition(end);
+      start = al.getHiddenColumns().absoluteToVisibleColumn(ostart);
+      end = al.getHiddenColumns().absoluteToVisibleColumn(end);
       if (start == end)
       {
         if (!scrollToNearest && !al.getHiddenColumns().isVisible(ostart))
@@ -530,7 +513,7 @@ public class AlignmentPanel extends Panel
       vpRanges.scrollToWrappedVisible(start);
     }
 
-    paintAlignment(redrawOverview);
+    paintAlignment(redrawOverview, false);
     return true;
   }
 
@@ -579,7 +562,7 @@ public class AlignmentPanel extends Panel
     apvscroll.addNotify();
     hscroll.addNotify();
     validate();
-    paintAlignment(true);
+    paintAlignment(true, false);
   }
 
   /**
@@ -692,7 +675,7 @@ public class AlignmentPanel extends Panel
       if (av.hasHiddenColumns())
       {
         width = av.getAlignment().getHiddenColumns()
-                .findColumnPosition(width);
+                .absoluteToVisibleColumn(width);
       }
       if (x < 0)
       {
@@ -930,7 +913,8 @@ public class AlignmentPanel extends Panel
    * Repaint the alignment and annotations, and, optionally, any overview window
    */
   @Override
-  public void paintAlignment(boolean updateOverview)
+  public void paintAlignment(boolean updateOverview,
+          boolean updateStructures)
   {
     final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
             av.isShowAutocalculatedAbove());
@@ -938,13 +922,14 @@ public class AlignmentPanel extends Panel
             av.getSortAnnotationsBy());
     repaint();
 
-    if (updateOverview)
+    if (updateStructures)
     {
-      // TODO: determine if this paintAlignment changed structure colours
       jalview.structure.StructureSelectionManager
               .getStructureSelectionManager(av.applet)
               .sequenceColoursChanged(this);
-
+    }
+    if (updateOverview)
+    {
       if (overviewPanel != null)
       {
         overviewPanel.updateOverviewImage();
index 8de751a..533226e 100644 (file)
@@ -85,7 +85,7 @@ public class AnnotationColourChooser extends Panel implements
     oldcs = av.getGlobalColourScheme();
     if (av.getAlignment().getGroups() != null)
     {
-      oldgroupColours = new HashMap<SequenceGroup, ColourSchemeI>();
+      oldgroupColours = new HashMap<>();
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
         oldgroupColours.put(sg, sg.getColourScheme());
@@ -180,8 +180,8 @@ public class AnnotationColourChooser extends Panel implements
     // TODO remove duplication with gui.AnnotationRowFilter
     // TODO add 'per sequence only' option / parameter
 
-    annotationLabels = new HashMap<AlignmentAnnotation, String>();
-    Vector<String> list = new Vector<String>();
+    annotationLabels = new HashMap<>();
+    Vector<String> list = new Vector<>();
     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
     if (anns == null)
     {
@@ -376,7 +376,7 @@ public class AnnotationColourChooser extends Panel implements
     else if (evt.getSource() == cancel)
     {
       reset();
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
       frame.setVisible(false);
     }
 
@@ -417,7 +417,7 @@ public class AnnotationColourChooser extends Panel implements
       }
 
       currentAnnotation.threshold.value = slider.getValue() / 1000f;
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -559,7 +559,7 @@ public class AnnotationColourChooser extends Panel implements
 
     // update colours in linked windows
     ap.alignmentChanged();
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   void reset()
@@ -572,7 +572,7 @@ public class AnnotationColourChooser extends Panel implements
         sg.setColourScheme(oldgroupColours.get(sg));
       }
     }
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   @Override
@@ -588,7 +588,7 @@ public class AnnotationColourChooser extends Panel implements
   @Override
   public void mouseReleased(MouseEvent evt)
   {
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   @Override
index b4c1d54..206b132 100644 (file)
@@ -46,7 +46,6 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.TextEvent;
 import java.awt.event.TextListener;
-import java.util.ArrayList;
 import java.util.Vector;
 
 //import javax.swing.JPanel;
@@ -293,20 +292,10 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
       {
         HiddenColumns oldHidden = av.getAnnotationColumnSelectionState()
                 .getOldHiddenColumns();
-        if (oldHidden != null)
-        {
-          ArrayList<int[]> regions = oldHidden.getHiddenColumnsCopy();
-          for (int[] positions : regions)
-          {
-            av.hideColumns(positions[0], positions[1]);
-          }
-        }
-        // TODO not clear why we need to hide all the columns (above) if we are
-        // going to copy the hidden columns over wholesale anyway
         av.getAlignment().setHiddenColumns(oldHidden);
       }
       av.sendSelection();
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
 
   }
@@ -348,7 +337,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
           sliderDragging = false;
           valueChanged(true);
         }
-        ap.paintAlignment(true);
+        ap.paintAlignment(true, true);
       }
     });
   }
@@ -359,8 +348,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     if (slider.isEnabled())
     {
       getCurrentAnnotation().threshold.value = slider.getValue() / 1000f;
-      updateView();
-      ap.paintAlignment(false);
+      updateView(); // this also calls paintAlignment(true,true)
     }
   }
 
@@ -515,7 +503,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter implements
     filterParams = null;
     av.setAnnotationColumnSelectionState(this);
     av.sendSelection();
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   public HiddenColumns getOldHiddenColumns()
index 2fb737a..1366f31 100755 (executable)
@@ -23,6 +23,7 @@ package jalview.appletgui;
 import jalview.analysis.AlignmentUtils;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
@@ -31,6 +32,7 @@ import jalview.util.ParseHtmlBodyAndLinks;
 import java.awt.Checkbox;
 import java.awt.CheckboxMenuItem;
 import java.awt.Color;
+import java.awt.Cursor;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
 import java.awt.FontMetrics;
@@ -50,13 +52,22 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Vector;
 
 public class AnnotationLabels extends Panel
         implements ActionListener, MouseListener, MouseMotionListener
 {
   Image image;
 
+  /**
+   * width in pixels within which height adjuster arrows are shown and active
+   */
+  private static final int HEIGHT_ADJUSTER_WIDTH = 50;
+
+  /**
+   * height in pixels for allowing height adjuster to be active
+   */
+  private static int HEIGHT_ADJUSTER_HEIGHT = 10;
+
   boolean active = false;
 
   AlignmentPanel ap;
@@ -92,23 +103,6 @@ public class AnnotationLabels extends Panel
     this.ap = ap;
     this.av = ap.av;
     setLayout(null);
-
-    /**
-     * this retrieves the adjustable height glyph from resources. we don't use
-     * it at the moment. java.net.URL url =
-     * getClass().getResource("/images/idwidth.gif"); Image temp = null;
-     * 
-     * if (url != null) { temp =
-     * java.awt.Toolkit.getDefaultToolkit().createImage(url); }
-     * 
-     * try { MediaTracker mt = new MediaTracker(this); mt.addImage(temp, 0);
-     * mt.waitForID(0); } catch (Exception ex) { }
-     * 
-     * BufferedImage bi = new BufferedImage(temp.getHeight(this),
-     * temp.getWidth(this), BufferedImage.TYPE_INT_RGB); Graphics2D g =
-     * (Graphics2D) bi.getGraphics(); g.rotate(Math.toRadians(90));
-     * g.drawImage(temp, 0, -bi.getWidth(this), this); image = (Image) bi;
-     */
     addMouseListener(this);
     addMouseMotionListener(this);
   }
@@ -208,7 +202,9 @@ public class AnnotationLabels extends Panel
     }
     else if (evt.getActionCommand().equals(COPYCONS_SEQ))
     {
-      SequenceI cons = av.getConsensusSeq();
+      SequenceGroup group = aa[selectedRow].groupRef;
+      SequenceI cons = group == null ? av.getConsensusSeq()
+              : group.getConsensusSeq();
       if (cons != null)
       {
         copy_annotseqtoclipboard(cons);
@@ -226,7 +222,8 @@ public class AnnotationLabels extends Panel
     ap.annotationPanel.adjustPanelHeight();
     setSize(getSize().width, ap.annotationPanel.getSize().height);
     ap.validate();
-    ap.paintAlignment(true);
+    // TODO: only paint if we needed to
+    ap.paintAlignment(true, true);
   }
 
   boolean editLabelDescription(AlignmentAnnotation annotation)
@@ -267,7 +264,10 @@ public class AnnotationLabels extends Panel
   @Override
   public void mouseMoved(MouseEvent evt)
   {
-    resizePanel = evt.getY() < 10 && evt.getX() < 14;
+    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT
+            && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
+    setCursor(Cursor.getPredefinedCursor(
+            resizePanel ? Cursor.S_RESIZE_CURSOR : Cursor.DEFAULT_CURSOR));
     int row = getSelectedRow(evt.getY() + scrollOffset);
 
     if (row > -1)
@@ -405,6 +405,7 @@ public class AnnotationLabels extends Panel
     resizePanel = false;
     dragEvent = null;
     dragCancelled = false;
+    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
     repaint();
     ap.annotationPanel.repaint();
   }
@@ -417,6 +418,8 @@ public class AnnotationLabels extends Panel
       resizePanel = true;
       repaint();
     }
+    setCursor(Cursor.getPredefinedCursor(
+            resizePanel ? Cursor.S_RESIZE_CURSOR : Cursor.DEFAULT_CURSOR));
   }
 
   @Override
@@ -548,7 +551,7 @@ public class AnnotationLabels extends Panel
                 {
                   ap.av.setIgnoreGapsConsensus(cbmi.getState(), ap);
                 }
-                ap.paintAlignment(true);
+                ap.paintAlignment(true, true);
               }
             });
             popup.add(cbmi);
@@ -756,7 +759,7 @@ public class AnnotationLabels extends Panel
                 }
               }
             }
-            ap.paintAlignment(false);
+            ap.paintAlignment(false, false);
             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
             ap.av.sendSelection();
           }
@@ -813,7 +816,7 @@ public class AnnotationLabels extends Panel
               sg.addSequence(aa[selectedRow].sequenceRef, false);
             }
             ap.av.setSelectionGroup(sg);
-            ap.paintAlignment(false);
+            ap.paintAlignment(false, false);
             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
             ap.av.sendSelection();
           }
@@ -842,8 +845,8 @@ public class AnnotationLabels extends Panel
                     + "\t" + sq.getSequenceAsString() + "\n");
     if (av.hasHiddenColumns())
     {
-      jalview.appletgui.AlignFrame.copiedHiddenColumns = new Vector<>(
-              av.getAlignment().getHiddenColumns().getHiddenColumnsCopy());
+      jalview.appletgui.AlignFrame.copiedHiddenColumns = new HiddenColumns(
+              av.getAlignment().getHiddenColumns());
     }
   }
 
@@ -902,14 +905,8 @@ public class AnnotationLabels extends Panel
       }
     }
     g.translate(0, +scrollOffset);
-    if (resizePanel)
-    {
-      g.setColor(Color.red);
-      g.setPaintMode();
-      g.drawLine(2, 8, 5, 2);
-      g.drawLine(5, 2, 8, 8);
-    }
-    else if (!dragCancelled && dragEvent != null && aa != null)
+
+    if (!resizePanel && !dragCancelled && dragEvent != null && aa != null)
     {
       g.setColor(Color.lightGray);
       g.drawString(aa[selectedRow].label, dragEvent.getX(),
index 6fe71de..50bc184 100755 (executable)
@@ -439,7 +439,8 @@ public class AnnotationPanel extends Panel
       graphStretchY = evt.getY();
       av.calcPanelHeight();
       needValidating = true;
-      ap.paintAlignment(true);
+      // TODO: only update overview visible geometry
+      ap.paintAlignment(true, false);
     }
     else
     {
@@ -479,7 +480,7 @@ public class AnnotationPanel extends Panel
     if (av.hasHiddenColumns())
     {
       column = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(column);
+              .visibleToAbsoluteColumn(column);
     }
 
     if (row > -1 && column < aa[row].annotations.length
@@ -777,5 +778,14 @@ public class AnnotationPanel extends Panel
     {
       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
     }
+    else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      fastPaint(((int[]) evt.getNewValue())[0]
+              - ((int[]) evt.getOldValue())[0]);
+    }
+    else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      repaint();
+    }
   }
 }
index 5efd177..c96dbab 100644 (file)
@@ -114,7 +114,7 @@ public abstract class AnnotationRowFilter extends Panel
   public void cancel_actionPerformed(ActionEvent e)
   {
     reset();
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
     frame.setVisible(false);
   }
 
index 49219b9..3d1442d 100644 (file)
@@ -134,7 +134,7 @@ public class AppletJmol extends EmbmenuFrame implements
 
   AlignmentPanel ap;
 
-  List<AlignmentPanel> _aps = new ArrayList<AlignmentPanel>(); // remove? never
+  List<AlignmentPanel> _aps = new ArrayList<>(); // remove? never
                                                                // added to
 
   String fileLoadingError;
@@ -213,7 +213,7 @@ public class AppletJmol extends EmbmenuFrame implements
     {
       reader = StructureSelectionManager
               .getStructureSelectionManager(ap.av.applet)
-              .setMapping(seq, chains, pdbentry.getFile(), protocol);
+              .setMapping(seq, chains, pdbentry.getFile(), protocol, null);
       // PROMPT USER HERE TO ADD TO NEW OR EXISTING VIEW?
       // FOR NOW, LETS JUST OPEN A NEW WINDOW
     }
@@ -394,7 +394,7 @@ public class AppletJmol extends EmbmenuFrame implements
 
   void centerViewer()
   {
-    Vector<String> toshow = new Vector<String>();
+    Vector<String> toshow = new Vector<>();
     for (int i = 0; i < chainMenu.getItemCount(); i++)
     {
       if (chainMenu.getItem(i) instanceof CheckboxMenuItem)
index d5d53fb..2f61b24 100644 (file)
@@ -24,6 +24,7 @@ import jalview.api.AlignmentViewPanel;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.ext.jmol.JalviewJmolBinding;
+import jalview.gui.IProgressIndicator;
 import jalview.io.DataSourceType;
 import jalview.structure.StructureSelectionManager;
 
@@ -183,4 +184,11 @@ class AppletJmolBinding extends JalviewJmolBinding
     // TODO Auto-generated method stub
     return null;
   }
+
+  @Override
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    // no progress indicators on the applet
+    return null;
+  }
 }
index 3966536..89228d5 100644 (file)
@@ -26,6 +26,7 @@ import jalview.api.SequenceRenderer;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
 import jalview.ext.jmol.JalviewJmolBinding;
+import jalview.gui.IProgressIndicator;
 import jalview.io.DataSourceType;
 
 import java.awt.Container;
@@ -65,6 +66,13 @@ public class ExtJmol extends JalviewJmolBinding
   }
 
   @Override
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    // no progress indicators on applet (could access javascript for this)
+    return null;
+  }
+
+  @Override
   public void updateColours(Object source)
   {
 
@@ -92,6 +100,7 @@ public class ExtJmol extends JalviewJmolBinding
     }
   }
 
+
   @Override
   public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
   {
@@ -137,8 +146,8 @@ public class ExtJmol extends JalviewJmolBinding
   @Override
   public void refreshPdbEntries()
   {
-    List<PDBEntry> pdbe = new ArrayList<PDBEntry>();
-    List<String> fileids = new ArrayList<String>();
+    List<PDBEntry> pdbe = new ArrayList<>();
+    List<String> fileids = new ArrayList<>();
     SequenceI[] sq = ap.av.getAlignment().getSequencesArray();
     for (int s = 0; s < sq.length; s++)
     {
index 5a073c6..d9eae11 100644 (file)
@@ -58,6 +58,8 @@ public class FeatureColourChooser extends Panel implements ActionListener,
    */
   private static final int SCALE_FACTOR_1K = 1000;
 
+  private static final String COLON = ":";
+
   private JVDialog frame;
 
   private Frame owner;
@@ -167,9 +169,9 @@ public class FeatureColourChooser extends Panel implements ActionListener,
     slider.addAdjustmentListener(this);
     slider.addMouseListener(this);
     owner = (af != null) ? af : fs.frame;
-    frame = new JVDialog(owner, MessageManager
-            .formatMessage("label.graduated_color_for_params", new String[]
-            { type }), true, 480, 248);
+    frame = new JVDialog(owner, MessageManager.formatMessage(
+            "label.variable_color_for", new String[] { type }), true, 480,
+            248);
     frame.setMainPanel(this);
     validate();
     frame.setVisible(true);
@@ -198,8 +200,10 @@ public class FeatureColourChooser extends Panel implements ActionListener,
 
   private void jbInit() throws Exception
   {
-    Label minLabel = new Label(MessageManager.getString("label.min")),
-            maxLabel = new Label(MessageManager.getString("label.max"));
+    Label minLabel = new Label(
+            MessageManager.getString("label.min_value") + COLON);
+    Label maxLabel = new Label(
+            MessageManager.getString("label.max_value") + COLON);
     minLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
     maxLabel.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
     // minColour.setFont(new java.awt.Font("Verdana", Font.PLAIN, 11));
index b35c079..df407d6 100644 (file)
@@ -493,7 +493,7 @@ public class FeatureRenderer
     }
     // findAllFeatures();
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
 
     return true;
   }
index 20d4d74..a60aacd 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.api.FeatureSettingsControllerI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 import jalview.util.MessageManager;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.BorderLayout;
 import java.awt.Button;
@@ -65,7 +66,7 @@ import java.util.Set;
 
 public class FeatureSettings extends Panel
         implements ItemListener, MouseListener, MouseMotionListener,
-        ActionListener, AdjustmentListener, FeatureSettingsControllerI
+        AdjustmentListener, FeatureSettingsControllerI
 {
   FeatureRenderer fr;
 
@@ -120,8 +121,17 @@ public class FeatureSettings extends Panel
       add(scrollPane, BorderLayout.CENTER);
     }
 
-    Button invert = new Button("Invert Selection");
-    invert.addActionListener(this);
+    Button invert = new Button(
+            MessageManager.getString("label.invert_selection"));
+    invert.addActionListener(new ActionListener()
+    {
+
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        invertSelection();
+      }
+    });
 
     Panel lowerPanel = new Panel(new GridLayout(2, 1, 5, 10));
     lowerPanel.add(invert);
@@ -377,8 +387,8 @@ public class FeatureSettings extends Panel
   // Group selection states
   void resetTable(boolean groupsChanged)
   {
-    List<String> displayableTypes = new ArrayList<String>();
-    Set<String> foundGroups = new HashSet<String>();
+    List<String> displayableTypes = new ArrayList<>();
+    Set<String> foundGroups = new HashSet<>();
 
     AlignmentI alignment = av.getAlignment();
 
@@ -391,7 +401,7 @@ public class FeatureSettings extends Panel
        * and keep track of which groups are visible
        */
       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
-      Set<String> visibleGroups = new HashSet<String>();
+      Set<String> visibleGroups = new HashSet<>();
       for (String group : groups)
       {
         // if (group == null || fr.checkGroupVisibility(group, true))
@@ -545,8 +555,7 @@ public class FeatureSettings extends Panel
     }
   }
 
-  @Override
-  public void actionPerformed(ActionEvent evt)
+  protected void invertSelection()
   {
     for (int i = 0; i < featurePanel.getComponentCount(); i++)
     {
@@ -583,24 +592,21 @@ public class FeatureSettings extends Panel
   {
     Component[] comps = featurePanel.getComponents();
     int cSize = comps.length;
-
-    Object[][] tmp = new Object[cSize][3];
-    int tmpSize = 0;
-    for (int i = 0; i < cSize; i++)
-    {
-      MyCheckbox check = (MyCheckbox) comps[i];
-      tmp[tmpSize][0] = check.type;
-      tmp[tmpSize][1] = fr.getFeatureStyle(check.type);
-      tmp[tmpSize][2] = new Boolean(check.getState());
-      tmpSize++;
+    FeatureSettingsBean[] rowData = new FeatureSettingsBean[cSize];
+    int i = 0;
+    for (Component comp : comps)
+    {
+      MyCheckbox check = (MyCheckbox) comp;
+      // feature filter set to null as not (yet) offered in applet
+      FeatureColourI colour = fr.getFeatureStyle(check.type);
+      rowData[i] = new FeatureSettingsBean(check.type, colour, null,
+              check.getState());
+      i++;
     }
 
-    Object[][] data = new Object[tmpSize][3];
-    System.arraycopy(tmp, 0, data, 0, tmpSize);
-
-    fr.setFeaturePriority(data);
+    fr.setFeaturePriority(rowData);
 
-    ap.paintAlignment(updateOverview);
+    ap.paintAlignment(updateOverview, updateOverview);
   }
 
   MyCheckbox selectedCheck;
@@ -680,7 +686,7 @@ public class FeatureSettings extends Panel
   {
     featurePanel.removeAll();
     resetTable(false);
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   @Override
@@ -732,7 +738,7 @@ public class FeatureSettings extends Panel
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
     fr.setTransparency((100 - transparency.getValue()) / 100f);
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   class MyCheckbox extends Checkbox
index c9a92b2..443ebce 100644 (file)
@@ -261,7 +261,7 @@ public class FontChooser extends Panel implements ItemListener
       {
         ap.av.setCharWidth(oldCharWidth);
       }
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, false);
     }
     else if (tp != null)
     {
index 5eddc4f..296f898 100755 (executable)
@@ -286,7 +286,7 @@ public class IdCanvas extends Panel implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     int annotationHeight = 0;
@@ -448,5 +448,14 @@ public class IdCanvas extends Panel implements ViewportListenerI
     {
       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
     }
+    else if (propertyName.equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      fastPaint(((int[]) evt.getNewValue())[1]
+              - ((int[]) evt.getOldValue())[1]);
+    }
+    else if (propertyName.equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      repaint();
+    }
   }
 }
index 7d9d278..15e269c 100755 (executable)
@@ -70,7 +70,7 @@ public class IdPanel extends Panel
     // TODO: add in group link parameter
 
     // make a list of label,url pairs
-    HashMap<String, String> urlList = new HashMap<String, String>();
+    HashMap<String, String> urlList = new HashMap<>();
     if (viewport.applet != null)
     {
       for (int i = 1; i < 10; i++)
@@ -198,7 +198,7 @@ public class IdPanel extends Panel
     }
 
     lastid = seq;
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   @Override
@@ -295,7 +295,7 @@ public class IdPanel extends Panel
       }
       else
       {
-        nlinks = new ArrayList<String>();
+        nlinks = new ArrayList<>();
       }
 
       for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
@@ -333,7 +333,7 @@ public class IdPanel extends Panel
       selectSeq(seq);
     }
 
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   void selectSeq(int seq)
@@ -459,7 +459,7 @@ public class IdPanel extends Panel
           running = false;
         }
 
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, false);
         try
         {
           Thread.sleep(100);
index 75e3243..2602268 100755 (executable)
@@ -21,9 +21,8 @@
 package jalview.appletgui;
 
 import java.awt.Color;
+import java.awt.Cursor;
 import java.awt.Dimension;
-import java.awt.Graphics;
-import java.awt.Image;
 import java.awt.Panel;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
@@ -36,29 +35,24 @@ public class IdwidthAdjuster extends Panel
 
   int oldX = 0;
 
-  Image image;
-
   AlignmentPanel ap;
 
   public IdwidthAdjuster(AlignmentPanel ap)
   {
     setLayout(null);
     this.ap = ap;
-    java.net.URL url = getClass().getResource("/images/idwidth.gif");
-    if (url != null)
-    {
-      image = java.awt.Toolkit.getDefaultToolkit().getImage(url);
-    }
-
+    setBackground(Color.WHITE);
     addMouseListener(this);
     addMouseMotionListener(this);
   }
 
+  @Override
   public void mousePressed(MouseEvent evt)
   {
     oldX = evt.getX();
   }
 
+  @Override
   public void mouseReleased(MouseEvent evt)
   {
     active = false;
@@ -85,18 +79,24 @@ public class IdwidthAdjuster extends Panel
     // }
   }
 
+  @Override
   public void mouseEntered(MouseEvent evt)
   {
     active = true;
+    setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
+
     repaint();
   }
 
+  @Override
   public void mouseExited(MouseEvent evt)
   {
     active = false;
+    setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
     repaint();
   }
 
+  @Override
   public void mouseDragged(MouseEvent evt)
   {
     active = true;
@@ -112,25 +112,13 @@ public class IdwidthAdjuster extends Panel
     }
   }
 
+  @Override
   public void mouseMoved(MouseEvent evt)
   {
   }
 
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
   }
-
-  public void paint(Graphics g)
-  {
-    g.setColor(Color.white);
-    g.fillRect(0, 0, getSize().width, getSize().height);
-    if (active)
-    {
-      if (image != null)
-      {
-        g.drawImage(image, getSize().width - 20, 2, this);
-      }
-    }
-  }
-
 }
index 9597b44..e99c021 100644 (file)
@@ -128,11 +128,9 @@ public class OverviewCanvas extends Component
     {
       mg.translate(0, od.getSequencesHeight());
       or.drawGraph(mg, av.getAlignmentConservationAnnotation(),
-              av.getCharWidth(), od.getGraphHeight(),
-              od.getColumns(av.getAlignment()));
+              od.getGraphHeight(), od.getColumns(av.getAlignment()));
       mg.translate(0, -od.getSequencesHeight());
     }
-    System.gc();
 
     if (restart)
     {
index e74e1cd..3bbbe95 100755 (executable)
@@ -31,6 +31,7 @@ import java.awt.BorderLayout;
 import java.awt.CheckboxMenuItem;
 import java.awt.Cursor;
 import java.awt.Dimension;
+import java.awt.Frame;
 import java.awt.Panel;
 import java.awt.PopupMenu;
 import java.awt.event.ComponentAdapter;
@@ -154,6 +155,10 @@ public class OverviewPanel extends Panel implements Runnable,
       if (!od.isPositionInBox(evt.getX(), evt.getY()))
       { 
        draggingBox = false;
+
+        // display drag cursor at mouse position
+        setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+
         od.updateViewportFromMouse(evt.getX(), evt.getY(),
                 av.getAlignment().getHiddenSequences(),
                 av.getAlignment().getHiddenColumns());
@@ -171,6 +176,7 @@ public class OverviewPanel extends Panel implements Runnable,
   @Override
   public void mouseReleased(MouseEvent evt)
   {
+    draggingBox = false;
   }
 
   @Override
@@ -200,7 +206,7 @@ public class OverviewPanel extends Panel implements Runnable,
                 av.getAlignment().getHiddenSequences(),
                 av.getAlignment().getHiddenColumns());
       }
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -322,6 +328,9 @@ public class OverviewPanel extends Panel implements Runnable,
     try
     {
       av.getRanges().removePropertyChangeListener(this);
+      Frame parent = (Frame) getParent();
+      parent.dispose();
+      parent.setVisible(false);
     } finally
     {
       av = null;
index 32507fe..fe99187 100755 (executable)
@@ -24,8 +24,8 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
 
 import java.awt.Component;
-import java.util.Enumeration;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Vector;
@@ -78,13 +78,14 @@ public class PaintRefresher
       return;
     }
 
-    for (String id : components.keySet())
+    Iterator<String> it = components.keySet().iterator();
+    while (it.hasNext())
     {
-      Vector<Component> comps = components.get(id);
+      Vector<Component> comps = components.get(it.next());
       comps.removeElement(comp);
-      if (comps.size() == 0)
+      if (comps.isEmpty())
       {
-        components.remove(id);
+        it.remove();
       }
     }
   }
@@ -110,10 +111,10 @@ public class PaintRefresher
       return;
     }
 
-    Enumeration<Component> e = comps.elements();
-    while (e.hasMoreElements())
+    Iterator<Component> it = comps.iterator();
+    while (it.hasNext())
     {
-      comp = e.nextElement();
+      comp = it.next();
 
       if (comp == source)
       {
@@ -122,7 +123,7 @@ public class PaintRefresher
 
       if (!comp.isValid())
       {
-        comps.removeElement(comp);
+        it.remove();
       }
       else if (validateSequences && comp instanceof AlignmentPanel
               && source instanceof AlignmentPanel)
index 2aba20c..bd36b0d 100644 (file)
@@ -160,7 +160,7 @@ public class RedundancyPanel extends SliderPanel
 
     float value = slider.getValue();
 
-    List<SequenceI> redundantSequences = new ArrayList<SequenceI>();
+    List<SequenceI> redundantSequences = new ArrayList<>();
     for (int i = 0; i < redundancy.length; i++)
     {
       if (value <= redundancy[i])
@@ -247,7 +247,7 @@ public class RedundancyPanel extends SliderPanel
               ap.av.getAlignment().getSequences());
     }
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
 
     if (historyList.size() == 0)
     {
index 514c3f9..c91449f 100755 (executable)
@@ -42,6 +42,7 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.beans.PropertyChangeEvent;
+import java.util.Iterator;
 import java.util.List;
 
 public class ScalePanel extends Panel
@@ -86,7 +87,7 @@ public class ScalePanel extends Panel
 
     if (av.hasHiddenColumns())
     {
-      res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(x);
+      res = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(x);
     }
     else
     {
@@ -141,7 +142,7 @@ public class ScalePanel extends Panel
       sg.setStartRes(min);
       sg.setEndRes(max);
     }
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -167,13 +168,13 @@ public class ScalePanel extends Panel
         {
           av.showColumn(reveal[0]);
           reveal = null;
-          ap.paintAlignment(true);
+          ap.paintAlignment(true, true);
           av.sendSelection();
         }
       });
       pop.add(item);
 
-      if (av.getAlignment().getHiddenColumns().hasManyHiddenColumns())
+      if (av.getAlignment().getHiddenColumns().hasMultiHiddenColumnRegions())
       {
         item = new MenuItem(MessageManager.getString("action.reveal_all"));
         item.addActionListener(new ActionListener()
@@ -183,7 +184,7 @@ public class ScalePanel extends Panel
           {
             av.showAllHiddenColumns();
             reveal = null;
-            ap.paintAlignment(true);
+            ap.paintAlignment(true, true);
             av.sendSelection();
           }
         });
@@ -208,7 +209,7 @@ public class ScalePanel extends Panel
             av.setSelectionGroup(null);
           }
 
-          ap.paintAlignment(true);
+          ap.paintAlignment(true, true);
           av.sendSelection();
         }
       });
@@ -234,12 +235,12 @@ public class ScalePanel extends Panel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     if (!stretchingGroup)
     {
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
 
       return;
     }
@@ -256,7 +257,7 @@ public class ScalePanel extends Panel
     }
 
     stretchingGroup = false;
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -275,7 +276,7 @@ public class ScalePanel extends Panel
     int res = (evt.getX() / av.getCharWidth())
             + av.getRanges().getStartRes();
     res = Math.max(0, res);
-    res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
+    res = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(res);
     res = Math.min(res, av.getAlignment().getWidth() - 1);
     min = Math.min(res, min);
     max = Math.max(res, max);
@@ -285,7 +286,7 @@ public class ScalePanel extends Panel
     {
       stretchingGroup = true;
       cs.stretchGroup(res, sg, min, max);
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -377,7 +378,7 @@ public class ScalePanel extends Panel
         {
           if (hidden.isVisible(sel))
           {
-            sel = hidden.findColumnPosition(sel);
+            sel = hidden.absoluteToVisibleColumn(sel);
           }
           else
           {
@@ -436,24 +437,17 @@ public class ScalePanel extends Panel
       if (av.getShowHiddenMarkers())
       {
         int widthx = 1 + endx - startx;
-        List<Integer> positions = hidden.findHiddenRegionPositions();
-        for (int pos : positions)
+        Iterator<Integer> it = hidden.getStartRegionIterator(startx,
+                startx + widthx + 1);
+        while (it.hasNext())
         {
-
-          res = pos - startx;
-
-          if (res < 0 || res > widthx)
-          {
-            continue;
-          }
+          res = it.next() - startx;
 
           gg.fillPolygon(
                   new int[]
-                  { -1 + res * avCharWidth - avcharHeight / 4,
-                      -1 + res * avCharWidth + avcharHeight / 4,
-                      -1 + res * avCharWidth },
-                  new int[]
-                  { y, y, y + 2 * yOf }, 3);
+                  { -1 + res * avCharWidth - avcharHeight / 4, -1 + res * avCharWidth + avcharHeight / 4,
+              -1 + res * avCharWidth }, new int[]
+          { y, y, y + 2 * yOf }, 3);
         }
       }
     }
@@ -468,7 +462,9 @@ public class ScalePanel extends Panel
     // Here we only want to fastpaint on a scroll, with resize using a normal
     // paint, so scroll events are identified as changes to the horizontal or
     // vertical start value.
-    if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
+    if (evt.getPropertyName().equals(ViewportRanges.STARTRES)
+            || evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ)
+            || evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
     {
       // scroll event, repaint panel
       repaint();
index f59967d..35d73de 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.VisibleContigsIterator;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
 import jalview.viewmodel.AlignmentViewport;
@@ -37,7 +38,7 @@ import java.awt.Graphics;
 import java.awt.Image;
 import java.awt.Panel;
 import java.beans.PropertyChangeEvent;
-import java.util.List;
+import java.util.Iterator;
 
 public class SeqCanvas extends Panel implements ViewportListenerI
 {
@@ -130,16 +131,16 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       startx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(startx);
+              .visibleToAbsoluteColumn(startx);
       endx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(endx);
+              .visibleToAbsoluteColumn(endx);
     }
 
     int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     // WEST SCALE
@@ -180,7 +181,7 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       endx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(endx);
+              .visibleToAbsoluteColumn(endx);
     }
 
     SequenceI seq;
@@ -417,71 +418,71 @@ public class SeqCanvas extends Panel implements ViewportListenerI
           int canvasHeight, int startRes)
   {
     AlignmentI al = av.getAlignment();
-
+  
     FontMetrics fm = getFontMetrics(av.getFont());
-
+  
     LABEL_EAST = 0;
     LABEL_WEST = 0;
-
+  
     if (av.getScaleRightWrapped())
     {
       LABEL_EAST = fm.stringWidth(getMask());
     }
-
+  
     if (av.getScaleLeftWrapped())
     {
       LABEL_WEST = fm.stringWidth(getMask());
     }
-
+  
     int hgap = avcharHeight;
     if (av.getScaleAboveWrapped())
     {
       hgap += avcharHeight;
     }
-
+  
     int cWidth = (canvasWidth - LABEL_EAST - LABEL_WEST) / avcharWidth;
     int cHeight = av.getAlignment().getHeight() * avcharHeight;
-
+  
     av.setWrappedWidth(cWidth);
-
+  
     av.getRanges().setViewportStartAndWidth(startRes, cWidth);
-
+  
     int endx;
     int ypos = hgap;
-
+  
     int maxwidth = av.getAlignment().getWidth();
-
+  
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth);
+              .absoluteToVisibleColumn(maxwidth);
     }
-
+  
     while ((ypos <= canvasHeight) && (startRes < maxwidth))
     {
       endx = startRes + cWidth - 1;
-
+  
       if (endx > maxwidth)
       {
         endx = maxwidth;
       }
-
+  
       g.setColor(Color.black);
-
+  
       if (av.getScaleLeftWrapped())
       {
         drawWestScale(g, startRes, endx, ypos);
       }
-
+  
       if (av.getScaleRightWrapped())
       {
         g.translate(canvasWidth - LABEL_EAST, 0);
         drawEastScale(g, startRes, endx, ypos);
         g.translate(-(canvasWidth - LABEL_EAST), 0);
       }
-
+  
       g.translate(LABEL_WEST, 0);
-
+  
       if (av.getScaleAboveWrapped())
       {
         drawNorthScale(g, startRes, endx, ypos);
@@ -491,37 +492,27 @@ public class SeqCanvas extends Panel implements ViewportListenerI
         HiddenColumns hidden = av.getAlignment().getHiddenColumns();
         g.setColor(Color.blue);
         int res;
-        List<Integer> positions = hidden.findHiddenRegionPositions();
-        for (int pos : positions)
+        Iterator<Integer> it = hidden.getStartRegionIterator(startRes,
+                endx + 1);
+        while (it.hasNext())
         {
-          res = pos - startRes;
-
-          if (res < 0 || res > endx - startRes)
-          {
-            continue;
-          }
-
+          res = it.next() - startRes;
           gg.fillPolygon(
                   new int[]
-                  { res * avcharWidth - avcharHeight / 4,
-                      res * avcharWidth + avcharHeight / 4,
-                      res * avcharWidth },
+                  { res * avcharWidth - avcharHeight / 4, res * avcharWidth + avcharHeight / 4, res * avcharWidth },
                   new int[]
-                  { ypos - (avcharHeight / 2), ypos - (avcharHeight / 2),
-                      ypos - (avcharHeight / 2) + 8 },
-                  3);
-
+                  { ypos - (avcharHeight / 2), ypos - (avcharHeight / 2), ypos - (avcharHeight / 2) + 8 }, 3);
         }
       }
-
+  
       if (g.getClip() == null)
       {
         g.setClip(0, 0, cWidth * avcharWidth, canvasHeight);
       }
-
+  
       drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
       g.setClip(null);
-
+  
       if (av.isShowAnnotation())
       {
         g.translate(0, cHeight + ypos + 4);
@@ -529,17 +520,17 @@ public class SeqCanvas extends Panel implements ViewportListenerI
         {
           annotations = new AnnotationPanel(av);
         }
-
+  
         annotations.drawComponent(g, startRes, endx + 1);
         g.translate(0, -cHeight - ypos - 4);
       }
       g.translate(-LABEL_WEST, 0);
-
+  
       ypos += cHeight + getAnnotationHeight() + hgap;
-
+  
       startRes += cWidth;
     }
-
+  
   }
 
   AnnotationPanel annotations;
@@ -570,70 +561,44 @@ public class SeqCanvas extends Panel implements ViewportListenerI
     else
     {
       int screenY = 0;
-      final int screenYMax = endRes - startRes;
-      int blockStart = startRes;
-      int blockEnd = endRes;
-
-      if (av.hasHiddenColumns())
-      {
-        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-        for (int[] region : hidden.getHiddenColumnsCopy())
-        {
-          int hideStart = region[0];
-          int hideEnd = region[1];
+      int blockStart;
+      int blockEnd;
 
-          if (hideStart <= blockStart)
-          {
-            blockStart += (hideEnd - hideStart) + 1;
-            continue;
-          }
-
-          /*
-           * draw up to just before the next hidden region, or the end of
-           * the visible region, whichever comes first
-           */
-          blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
-                  - screenY);
-
-          g1.translate(screenY * avcharWidth, 0);
-
-          draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
-
-          /*
-           * draw the downline of the hidden column marker (ScalePanel draws the
-           * triangle on top) if we reached it
-           */
-          if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
-          {
-            g1.setColor(Color.blue);
-            g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
-                    0 + offset,
-                    (blockEnd - blockStart + 1) * avcharWidth - 1,
-                    (endSeq - startSeq + 1) * avcharHeight + offset);
-          }
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      VisibleContigsIterator regions = (VisibleContigsIterator) hidden
+              .getVisContigsIterator(startRes, endRes + 1, true);
 
-          g1.translate(-screenY * avcharWidth, 0);
-          screenY += blockEnd - blockStart + 1;
-          blockStart = hideEnd + 1;
-
-          if (screenY > screenYMax)
-          {
-            // already rendered last block
-            return;
-          }
-        }
-      }
-      if (screenY <= screenYMax)
+      while (regions.hasNext())
       {
-        // remaining visible region to render
-        blockEnd = blockStart + (endRes - startRes) - screenY;
+        int[] region = regions.next();
+        blockEnd = region[1];
+        blockStart = region[0];
+
+        /*
+         * draw up to just before the next hidden region, or the end of
+         * the visible region, whichever comes first
+         */
         g1.translate(screenY * avcharWidth, 0);
+
         draw(g1, blockStart, blockEnd, startSeq, endSeq, offset);
 
+        /*
+         * draw the downline of the hidden column marker (ScalePanel draws the
+         * triangle on top) if we reached it
+         */
+        if (av.getShowHiddenMarkers()
+                && (regions.hasNext() || regions.endsAtHidden()))
+        {
+          g1.setColor(Color.blue);
+          g1.drawLine((blockEnd - blockStart + 1) * avcharWidth - 1,
+                  0 + offset, (blockEnd - blockStart + 1) * avcharWidth - 1,
+                  (endSeq - startSeq + 1) * avcharHeight + offset);
+        }
+
         g1.translate(-screenY * avcharWidth, 0);
+        screenY += blockEnd - blockStart + 1;
       }
     }
-
   }
 
   // int startRes, int endRes, int startSeq, int endSeq, int x, int y,
@@ -889,15 +854,37 @@ public class SeqCanvas extends Panel implements ViewportListenerI
   {
     String eventName = evt.getPropertyName();
 
+    if (eventName.equals(SequenceGroup.SEQ_GROUP_CHANGED))
+    {
+      fastPaint = true;
+      repaint();
+      return;
+    }
+    else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      fastPaint = false;
+      repaint();
+      return;
+    }
+
     if (!av.getWrapAlignment())
     {
       int scrollX = 0;
-      if (eventName.equals(ViewportRanges.STARTRES))
+      if (eventName.equals(ViewportRanges.STARTRES)
+              || eventName.equals(ViewportRanges.STARTRESANDSEQ))
       {
         // Make sure we're not trying to draw a panel
         // larger than the visible window
+        if (eventName.equals(ViewportRanges.STARTRES))
+        {
+          scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
+        }
+        else
+        {
+          scrollX = ((int[]) evt.getNewValue())[0]
+                  - ((int[]) evt.getOldValue())[0];
+        }
         ViewportRanges vpRanges = av.getRanges();
-        scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
         int range = vpRanges.getEndRes() - vpRanges.getStartRes();
         if (scrollX > range)
         {
@@ -924,6 +911,10 @@ public class SeqCanvas extends Panel implements ViewportListenerI
         // scroll
         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
       }
+      else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+      {
+        fastPaint(scrollX, 0);
+      }
     }
   }
 
index f36a8e2..e07dae6 100644 (file)
@@ -43,7 +43,6 @@ import jalview.util.Comparison;
 import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.viewmodel.AlignmentViewport;
-import jalview.viewmodel.ViewportRanges;
 
 import java.awt.BorderLayout;
 import java.awt.Font;
@@ -148,13 +147,13 @@ public class SeqPanel extends Panel implements MouseMotionListener,
   void setCursorRow()
   {
     seqCanvas.cursorY = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorColumn()
   {
     seqCanvas.cursorX = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorRowAndColumn()
@@ -167,7 +166,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -176,7 +175,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void moveCursor(int dx, int dy)
@@ -202,10 +201,16 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         seqCanvas.cursorX = original;
       }
     }
-    scrollToVisible();
+    scrollToVisible(false);
   }
 
-  void scrollToVisible()
+  /**
+   * Scroll to make the cursor visible in the viewport.
+   * 
+   * @param jump
+   *          just jump to the location rather than scrolling
+   */
+  void scrollToVisible(boolean jump)
   {
     if (seqCanvas.cursorX < 0)
     {
@@ -226,44 +231,34 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     }
 
     endEditing();
-    if (av.getWrapAlignment())
+
+    boolean repaintNeeded = true;
+    if (jump)
     {
-      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
+      // only need to repaint if the viewport did not move, as otherwise it will
+      // get a repaint
+      repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
+              seqCanvas.cursorY);
     }
     else
     {
-      ViewportRanges ranges = av.getRanges();
-      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-      while (seqCanvas.cursorY < ranges.getStartSeq())
+      if (av.getWrapAlignment())
       {
-        ranges.scrollUp(true);
+        av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
       }
-      while (seqCanvas.cursorY > ranges.getEndSeq())
-      {
-        ranges.scrollUp(false);
-      }
-      while (seqCanvas.cursorX < hidden
-              .adjustForHiddenColumns(ranges.getStartRes()))
-      {
-
-        if (!ranges.scrollRight(false))
-        {
-          break;
-        }
-      }
-      while (seqCanvas.cursorX > hidden
-              .adjustForHiddenColumns(ranges.getEndRes()))
+      else
       {
-        if (!ranges.scrollRight(true))
-        {
-          break;
-        }
+        av.getRanges().scrollToVisible(seqCanvas.cursorX,
+                seqCanvas.cursorY);
       }
     }
     setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
             seqCanvas.cursorX, seqCanvas.cursorY);
 
-    seqCanvas.repaint();
+    if (repaintNeeded)
+    {
+      seqCanvas.repaint();
+    }
   }
 
   void setSelectionAreaAtCursor(boolean topLeft)
@@ -335,7 +330,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       sg.addSequence(sequence, false);
       av.setSelectionGroup(sg);
     }
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -417,7 +412,6 @@ public class SeqPanel extends Panel implements MouseMotionListener,
    *          alignment column
    * @param seq
    *          index of sequence in alignment
-   * @return position of column in sequence or -1 if at gap
    */
   void setStatusMessage(SequenceI sequence, int column, int seq)
   {
@@ -653,7 +647,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     return res;
@@ -983,7 +977,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
 
       lastMousePress = evt.getPoint();
 
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
       ap.annotationPanel.image = null;
       return;
     }
@@ -1129,9 +1123,9 @@ public class SeqPanel extends Panel implements MouseMotionListener,
     {
       fixedColumns = true;
       int y1 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryLeft(startres);
+              .getNextHiddenBoundary(true, startres);
       int y2 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryRight(startres);
+              .getNextHiddenBoundary(false, startres);
 
       if ((insertGap && startres > y1 && lastres < y1)
               || (!insertGap && startres < y2 && lastres > y2))
@@ -1203,7 +1197,8 @@ public class SeqPanel extends Panel implements MouseMotionListener,
           if (sg.getSize() == av.getAlignment().getHeight())
           {
             if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
+                    .getHiddenColumns()
+                    .getNextHiddenBoundary(false, startres)))
             {
               endEditing();
               return;
@@ -1450,7 +1445,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
         {
           if (links == null)
           {
-            links = new Vector<String>();
+            links = new Vector<>();
           }
           links.addAll(sf.links);
         }
@@ -1524,7 +1519,7 @@ public class SeqPanel extends Panel implements MouseMotionListener,
       }
     }
     PaintRefresher.Refresh(ap, av.getSequenceSetId());
-    ap.paintAlignment(needOverviewUpdate);
+    ap.paintAlignment(needOverviewUpdate, needOverviewUpdate);
     needOverviewUpdate = false;
     changeEndRes = false;
     changeStartRes = false;
index 565ebe8..5841e80 100644 (file)
@@ -441,7 +441,7 @@ public class SliderPanel extends Panel
   @Override
   public void mouseReleased(MouseEvent evt)
   {
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 
   @Override
index ed531d3..777e307 100644 (file)
@@ -187,9 +187,9 @@ public class SplitFrame extends EmbmenuFrame
     createSplitFrameWindow(embedded, applet);
     validate();
     topFrame.alignPanel.adjustAnnotationHeight();
-    topFrame.alignPanel.paintAlignment(true);
+    topFrame.alignPanel.paintAlignment(true, true);
     bottomFrame.alignPanel.adjustAnnotationHeight();
-    bottomFrame.alignPanel.paintAlignment(true);
+    bottomFrame.alignPanel.paintAlignment(true, true);
   }
 
   /**
index d1c0e1b..6831a73 100644 (file)
@@ -64,7 +64,7 @@ public class UserDefinedColours extends Panel
 
   Button selectedButton;
 
-  Vector<Color> oldColours = new Vector<Color>();
+  Vector<Color> oldColours = new Vector<>();
 
   ColourSchemeI oldColourScheme;
 
@@ -520,7 +520,7 @@ public class UserDefinedColours extends Panel
                 ap.av.isIgnoreGapsConsensus());
       }
       ap.seqPanel.seqCanvas.img = null;
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
     else if (jmol != null)
     {
@@ -599,7 +599,7 @@ public class UserDefinedColours extends Panel
       {
         ap.av.setGlobalColourScheme(oldColourScheme);
       }
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
 
     frame.setVisible(false);
index dc50843..dcd6546 100755 (executable)
@@ -281,7 +281,7 @@ public class Cache
     @Override
     public synchronized Enumeration<Object> keys()
     {
-      return Collections.enumeration(new TreeSet<Object>(super.keySet()));
+      return Collections.enumeration(new TreeSet<>(super.keySet()));
     }
   };
 
@@ -334,7 +334,10 @@ public class Cache
     }
   }
 
-  /** Called when Jalview is started */
+  /**
+   * Loads properties from the given properties file. Any existing properties
+   * are first cleared.
+   */
   public static void loadProperties(String propsFile)
   {
     propertiesFile = propsFile;
@@ -369,6 +372,7 @@ public class Cache
       {
         fis = new FileInputStream(propertiesFile);
       }
+      applicationProperties.clear();
       applicationProperties.load(fis);
 
       // remove any old build properties
@@ -621,14 +625,14 @@ public class Cache
    * @param obj
    *          String value of property
    * 
-   * @return String value of property
+   * @return previous value of property (or null)
    */
-  public static String setProperty(String key, String obj)
+  public static Object setProperty(String key, String obj)
   {
-
+    Object oldValue = null;
     try
     {
-      applicationProperties.setProperty(key, obj);
+      oldValue = applicationProperties.setProperty(key, obj);
       if (!propsAreReadOnly)
       {
         FileOutputStream out = new FileOutputStream(propertiesFile);
@@ -640,7 +644,7 @@ public class Cache
       System.out.println(
               "Error setting property: " + key + " " + obj + "\n" + ex);
     }
-    return obj;
+    return oldValue;
   }
 
   /**
index 9ec0033..30620a1 100755 (executable)
@@ -20,9 +20,6 @@
  */
 package jalview.bin;
 
-import groovy.lang.Binding;
-import groovy.util.GroovyScriptEngine;
-
 import jalview.ext.so.SequenceOntology;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
@@ -64,8 +61,12 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Vector;
 
+import javax.swing.LookAndFeel;
 import javax.swing.UIManager;
 
+import groovy.lang.Binding;
+import groovy.util.GroovyScriptEngine;
+
 /**
  * Main class for Jalview Application <br>
  * <br>
@@ -275,20 +276,43 @@ public class Jalview
       UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
     } catch (Exception ex)
     {
+      System.err.println("Unexpected Look and Feel Exception");
+      ex.printStackTrace();
     }
     if (Platform.isAMac())
     {
+
+      LookAndFeel lookAndFeel = ch.randelshofer.quaqua.QuaquaManager
+              .getLookAndFeel();
       System.setProperty("com.apple.mrj.application.apple.menu.about.name",
               "Jalview");
       System.setProperty("apple.laf.useScreenMenuBar", "true");
-      try
+      if (lookAndFeel != null)
       {
-        UIManager.setLookAndFeel(
-                ch.randelshofer.quaqua.QuaquaManager.getLookAndFeel());
-      } catch (Throwable e)
+        try
+        {
+          UIManager.setLookAndFeel(lookAndFeel);
+        } catch (Throwable e)
+        {
+          System.err.println(
+                  "Failed to set QuaQua look and feel: " + e.toString());
+        }
+      }
+      if (lookAndFeel == null || !(lookAndFeel.getClass()
+              .isAssignableFrom(UIManager.getLookAndFeel().getClass()))
+              || !UIManager.getLookAndFeel().getClass().toString()
+                      .toLowerCase().contains("quaqua"))
       {
-        System.err.println(
-                "Failed to set QuaQua look and feel: " + e.toString());
+        try
+        {
+          System.err.println(
+                  "Quaqua LaF not available on this plaform. Using VAqua(4).\nSee https://issues.jalview.org/browse/JAL-2976");
+          UIManager.setLookAndFeel("org.violetlib.aqua.AquaLookAndFeel");
+        } catch (Throwable e)
+        {
+          System.err.println(
+                  "Failed to reset look and feel: " + e.toString());
+        }
       }
     }
 
@@ -970,7 +994,7 @@ public class Jalview
     }
     try
     {
-      Map<String, Object> vbinding = new HashMap<String, Object>();
+      Map<String, Object> vbinding = new HashMap<>();
       vbinding.put("Jalview", this);
       if (af != null)
       {
@@ -1036,7 +1060,7 @@ public class Jalview
                         + nickname + "|" + url);
         if (source == null)
         {
-          source = new Vector<String>();
+          source = new Vector<>();
         }
         source.addElement(nickname);
       }
@@ -1054,7 +1078,7 @@ public class Jalview
       System.out.println("adding source '" + data + "'");
       if (source == null)
       {
-        source = new Vector<String>();
+        source = new Vector<>();
       }
       source.addElement(data);
     }
index 6504290..a60496c 100644 (file)
@@ -31,7 +31,6 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentOrder;
 import jalview.datamodel.ColumnSelection;
-import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
@@ -471,7 +470,7 @@ public class JalviewLite extends Applet
         SequenceI rs = sel.getSequenceAt(0);
         start = rs.findIndex(start);
         end = rs.findIndex(end);
-        List<Integer> cs = new ArrayList<Integer>(csel.getSelected());
+        List<Integer> cs = new ArrayList<>(csel.getSelected());
         csel.clear();
         for (Integer selectedCol : cs)
         {
@@ -921,7 +920,7 @@ public class JalviewLite extends Applet
     setMouseoverListener(currentAlignFrame, listener);
   }
 
-  private Vector<jalview.javascript.JSFunctionExec> javascriptListeners = new Vector<jalview.javascript.JSFunctionExec>();
+  private Vector<jalview.javascript.JSFunctionExec> javascriptListeners = new Vector<>();
 
   /*
    * (non-Javadoc)
@@ -2165,8 +2164,8 @@ public class JalviewLite extends Applet
           else
           {
             param = st.nextToken();
-            List<SequenceI> tmp = new ArrayList<SequenceI>();
-            List<String> tmp2 = new ArrayList<String>();
+            List<SequenceI> tmp = new ArrayList<>();
+            List<String> tmp2 = new ArrayList<>();
 
             while (st.hasMoreTokens())
             {
@@ -2279,12 +2278,9 @@ public class JalviewLite extends Applet
           JnetAnnotationMaker.add_annotation(predictions,
                   alignFrame.viewport.getAlignment(), 0, false);
           // false == do not add sequence profile from concise output
-          SequenceI repseq = alignFrame.viewport.getAlignment()
-                  .getSequenceAt(0);
-          alignFrame.viewport.getAlignment().setSeqrep(repseq);
-          HiddenColumns cs = new HiddenColumns();
-          cs.hideInsertionsFor(repseq);
-          alignFrame.viewport.getAlignment().setHiddenColumns(cs);
+
+          alignFrame.viewport.getAlignment().setupJPredAlignment();
+
           alignFrame.alignPanel.fontChanged();
           alignFrame.alignPanel.setScrollValues(0, 0);
           result = true;
@@ -2802,9 +2798,9 @@ public class JalviewLite extends Applet
     // callInitCallback();
   }
 
-  private Hashtable<String, long[]> jshashes = new Hashtable<String, long[]>();
+  private Hashtable<String, long[]> jshashes = new Hashtable<>();
 
-  private Hashtable<String, Hashtable<String, String[]>> jsmessages = new Hashtable<String, Hashtable<String, String[]>>();
+  private Hashtable<String, Hashtable<String, String[]>> jsmessages = new Hashtable<>();
 
   public void setJsMessageSet(String messageclass, String viewId,
           String[] colcommands)
@@ -2812,7 +2808,7 @@ public class JalviewLite extends Applet
     Hashtable<String, String[]> msgset = jsmessages.get(messageclass);
     if (msgset == null)
     {
-      msgset = new Hashtable<String, String[]>();
+      msgset = new Hashtable<>();
       jsmessages.put(messageclass, msgset);
     }
     msgset.put(viewId, colcommands);
index 25cf9bf..f51e9af 100644 (file)
@@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable
   // --------------------------/
 
   /**
-   * Field _name.
+   * Single letter residue code for an alignment colour scheme, or feature type
+   * for a feature colour scheme
    */
   private java.lang.String _name;
 
@@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable
   private java.lang.String _minRGB;
 
   /**
-   * loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Field _noValueColour.
    */
-  private java.lang.String _threshType;
+  private jalview.binding.types.NoValueColour _noValueColour = jalview.binding.types.NoValueColour
+          .valueOf("Min");
+
+  /**
+   * Field _threshType.
+   */
+  private jalview.binding.types.ColourThreshTypeType _threshType;
 
   /**
    * Field _threshold.
@@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -103,6 +115,8 @@ public class Colour implements java.io.Serializable
   public Colour()
   {
     super();
+    setNoValueColour(jalview.binding.types.NoValueColour.valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -110,41 +124,140 @@ public class Colour implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -195,7 +308,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'name'.
+   * Returns the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @return the value of field 'Name'.
    */
@@ -205,6 +320,16 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.binding.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'RGB'.
    * 
    * @return the value of field 'RGB'.
@@ -215,12 +340,11 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Returns the value of field 'threshType'.
    * 
    * @return the value of field 'ThreshType'.
    */
-  public java.lang.String getThreshType()
+  public jalview.binding.types.ColourThreshTypeType getThreshType()
   {
     return this._threshType;
   }
@@ -360,6 +484,76 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -419,7 +613,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'name'.
+   * Sets the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @param name
    *          the value of field 'name'.
@@ -430,6 +626,18 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.binding.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'RGB'.
    * 
    * @param RGB
@@ -441,13 +649,13 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Sets the value of field 'threshType'.
    * 
    * @param threshType
    *          the value of field 'threshType'.
    */
-  public void setThreshType(final java.lang.String threshType)
+  public void setThreshType(
+          final jalview.binding.types.ColourThreshTypeType threshType)
   {
     this._threshType = threshType;
   }
diff --git a/src/jalview/binding/CompoundMatcher.java b/src/jalview/binding/CompoundMatcher.java
new file mode 100644 (file)
index 0000000..a2d1048
--- /dev/null
@@ -0,0 +1,368 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class CompoundMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * If true, matchers are AND-ed, if false they are OR-ed
+   */
+  private boolean _and;
+
+  /**
+   * keeps track of state for field: _and
+   */
+  private boolean _has_and;
+
+  /**
+   * Field _matcherSetList.
+   */
+  private java.util.Vector _matcherSetList;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcher()
+  {
+    super();
+    this._matcherSetList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.addElement(vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final int index,
+          final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.add(index, vMatcherSet);
+  }
+
+  /**
+   */
+  public void deleteAnd()
+  {
+    this._has_and = false;
+  }
+
+  /**
+   * Method enumerateMatcherSet.
+   * 
+   * @return an Enumeration over all jalview.binding.MatcherSet elements
+   */
+  public java.util.Enumeration enumerateMatcherSet()
+  {
+    return this._matcherSetList.elements();
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean getAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method getMatcherSet.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the jalview.binding.MatcherSet at the given index
+   */
+  public jalview.binding.MatcherSet getMatcherSet(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    return (jalview.binding.MatcherSet) _matcherSetList.get(index);
+  }
+
+  /**
+   * Method getMatcherSet.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public jalview.binding.MatcherSet[] getMatcherSet()
+  {
+    jalview.binding.MatcherSet[] array = new jalview.binding.MatcherSet[0];
+    return (jalview.binding.MatcherSet[]) this._matcherSetList
+            .toArray(array);
+  }
+
+  /**
+   * Method getMatcherSetCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getMatcherSetCount()
+  {
+    return this._matcherSetList.size();
+  }
+
+  /**
+   * Method hasAnd.
+   * 
+   * @return true if at least one And has been added
+   */
+  public boolean hasAnd()
+  {
+    return this._has_and;
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean isAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllMatcherSet()
+  {
+    this._matcherSetList.clear();
+  }
+
+  /**
+   * Method removeMatcherSet.
+   * 
+   * @param vMatcherSet
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeMatcherSet(
+          final jalview.binding.MatcherSet vMatcherSet)
+  {
+    boolean removed = _matcherSetList.remove(vMatcherSet);
+    return removed;
+  }
+
+  /**
+   * Method removeMatcherSetAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public jalview.binding.MatcherSet removeMatcherSetAt(final int index)
+  {
+    java.lang.Object obj = this._matcherSetList.remove(index);
+    return (jalview.binding.MatcherSet) obj;
+  }
+
+  /**
+   * Sets the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @param and
+   *          the value of field 'and'.
+   */
+  public void setAnd(final boolean and)
+  {
+    this._and = and;
+    this._has_and = true;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setMatcherSet(final int index,
+          final jalview.binding.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    this._matcherSetList.set(index, vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param vMatcherSetArray
+   */
+  public void setMatcherSet(
+          final jalview.binding.MatcherSet[] vMatcherSetArray)
+  {
+    // -- copy array
+    _matcherSetList.clear();
+
+    for (int i = 0; i < vMatcherSetArray.length; i++)
+    {
+      this._matcherSetList.add(vMatcherSetArray[i]);
+    }
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.CompoundMatcher
+   */
+  public static jalview.binding.CompoundMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.CompoundMatcher) Unmarshaller
+            .unmarshal(jalview.binding.CompoundMatcher.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/FeatureMatcher.java b/src/jalview/binding/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..e4e52fb
--- /dev/null
@@ -0,0 +1,381 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class FeatureMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _by.
+   */
+  private jalview.binding.types.FeatureMatcherByType _by;
+
+  /**
+   * name of feature attribute to filter on, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * Field _condition.
+   */
+  private java.lang.String _condition;
+
+  /**
+   * Field _value.
+   */
+  private java.lang.String _value;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcher()
+  {
+    super();
+    this._attributeNameList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
+   * Returns the value of field 'by'.
+   * 
+   * @return the value of field 'By'.
+   */
+  public jalview.binding.types.FeatureMatcherByType getBy()
+  {
+    return this._by;
+  }
+
+  /**
+   * Returns the value of field 'condition'.
+   * 
+   * @return the value of field 'Condition'.
+   */
+  public java.lang.String getCondition()
+  {
+    return this._condition;
+  }
+
+  /**
+   * Returns the value of field 'value'.
+   * 
+   * @return the value of field 'Value'.
+   */
+  public java.lang.String getValue()
+  {
+    return this._value;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
+   * Sets the value of field 'by'.
+   * 
+   * @param by
+   *          the value of field 'by'.
+   */
+  public void setBy(final jalview.binding.types.FeatureMatcherByType by)
+  {
+    this._by = by;
+  }
+
+  /**
+   * Sets the value of field 'condition'.
+   * 
+   * @param condition
+   *          the value of field 'condition'.
+   */
+  public void setCondition(final java.lang.String condition)
+  {
+    this._condition = condition;
+  }
+
+  /**
+   * Sets the value of field 'value'.
+   * 
+   * @param value
+   *          the value of field 'value'.
+   */
+  public void setValue(final java.lang.String value)
+  {
+    this._value = value;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcher
+   */
+  public static jalview.binding.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.binding.FeatureMatcher.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/FeatureMatcherSet.java b/src/jalview/binding/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..7ba5f0e
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * A feature match condition, which may be simple or compound
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSet implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Internal choice value storage
+   */
+  private java.lang.Object _choiceValue;
+
+  /**
+   * Field _matchCondition.
+   */
+  private jalview.binding.MatchCondition _matchCondition;
+
+  /**
+   * Field _compoundMatcher.
+   */
+  private jalview.binding.CompoundMatcher _compoundMatcher;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'choiceValue'. The field 'choiceValue' has the
+   * following description: Internal choice value storage
+   * 
+   * @return the value of field 'ChoiceValue'.
+   */
+  public java.lang.Object getChoiceValue()
+  {
+    return this._choiceValue;
+  }
+
+  /**
+   * Returns the value of field 'compoundMatcher'.
+   * 
+   * @return the value of field 'CompoundMatcher'.
+   */
+  public jalview.binding.CompoundMatcher getCompoundMatcher()
+  {
+    return this._compoundMatcher;
+  }
+
+  /**
+   * Returns the value of field 'matchCondition'.
+   * 
+   * @return the value of field 'MatchCondition'.
+   */
+  public jalview.binding.MatchCondition getMatchCondition()
+  {
+    return this._matchCondition;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'compoundMatcher'.
+   * 
+   * @param compoundMatcher
+   *          the value of field 'compoundMatcher'.
+   */
+  public void setCompoundMatcher(
+          final jalview.binding.CompoundMatcher compoundMatcher)
+  {
+    this._compoundMatcher = compoundMatcher;
+    this._choiceValue = compoundMatcher;
+  }
+
+  /**
+   * Sets the value of field 'matchCondition'.
+   * 
+   * @param matchCondition
+   *          the value of field 'matchCondition'.
+   */
+  public void setMatchCondition(
+          final jalview.binding.MatchCondition matchCondition)
+  {
+    this._matchCondition = matchCondition;
+    this._choiceValue = matchCondition;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcherSet
+   */
+  public static jalview.binding.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.binding.FeatureMatcherSet.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/Filter.java b/src/jalview/binding/Filter.java
new file mode 100644 (file)
index 0000000..687ae91
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class Filter.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class Filter implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _featureType.
+   */
+  private java.lang.String _featureType;
+
+  /**
+   * Field _matcherSet.
+   */
+  private jalview.binding.MatcherSet _matcherSet;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public Filter()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'featureType'.
+   * 
+   * @return the value of field 'FeatureType'.
+   */
+  public java.lang.String getFeatureType()
+  {
+    return this._featureType;
+  }
+
+  /**
+   * Returns the value of field 'matcherSet'.
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.binding.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'featureType'.
+   * 
+   * @param featureType
+   *          the value of field 'featureType'.
+   */
+  public void setFeatureType(final java.lang.String featureType)
+  {
+    this._featureType = featureType;
+  }
+
+  /**
+   * Sets the value of field 'matcherSet'.
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(final jalview.binding.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.Filter
+   */
+  public static jalview.binding.Filter unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.Filter) Unmarshaller
+            .unmarshal(jalview.binding.Filter.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index 6709487..67ee5a2 100644 (file)
@@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable
    */
   private java.util.Vector _colourList;
 
+  /**
+   * Field _filterList.
+   */
+  private java.util.Vector _filterList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable
   {
     super();
     this._colourList = new java.util.Vector();
+    this._filterList = new java.util.Vector();
   }
 
   // -----------/
@@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.addElement(vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.add(index, vFilter);
+  }
+
+  /**
    * Method enumerateColour.
    * 
    * @return an Enumeration over all Colour elements
@@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method enumerateFilter.
+   * 
+   * @return an Enumeration over all Filter elements
+   */
+  public java.util.Enumeration enumerateFilter()
+  {
+    return this._filterList.elements();
+  }
+
+  /**
    * Method getColour.
    * 
    * @param index
@@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method getFilter.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the Filter at the given index
+   */
+  public Filter getFilter(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    return (Filter) _filterList.get(index);
+  }
+
+  /**
+   * Method getFilter.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public Filter[] getFilter()
+  {
+    Filter[] array = new Filter[0];
+    return (Filter[]) this._filterList.toArray(array);
+  }
+
+  /**
+   * Method getFilterCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getFilterCount()
+  {
+    return this._filterList.size();
+  }
+
+  /**
    * Returns the value of field 'schemeName'.
    * 
    * @return the value of field 'SchemeName'.
@@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
-     */
+   */
   public void removeAllColour()
   {
     this._colourList.clear();
   }
 
   /**
+   */
+  public void removeAllFilter()
+  {
+    this._filterList.clear();
+  }
+
+  /**
    * Method removeColour.
    * 
    * @param vColour
@@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method removeFilter.
+   * 
+   * @param vFilter
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeFilter(final Filter vFilter)
+  {
+    boolean removed = _filterList.remove(vFilter);
+    return removed;
+  }
+
+  /**
+   * Method removeFilterAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public Filter removeFilterAt(final int index)
+  {
+    java.lang.Object obj = this._filterList.remove(index);
+    return (Filter) obj;
+  }
+
+  /**
    * 
    * 
    * @param index
@@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    this._filterList.set(index, vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param vFilterArray
+   */
+  public void setFilter(final Filter[] vFilterArray)
+  {
+    // -- copy array
+    _filterList.clear();
+
+    for (int i = 0; i < vFilterArray.length; i++)
+    {
+      this._filterList.add(vFilterArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'schemeName'.
    * 
    * @param schemeName
diff --git a/src/jalview/binding/MatchCondition.java b/src/jalview/binding/MatchCondition.java
new file mode 100644 (file)
index 0000000..44a3d3e
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatchCondition.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchCondition extends FeatureMatcher
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchCondition()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcher
+   */
+  public static jalview.binding.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.binding.MatchCondition.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/MatcherSet.java b/src/jalview/binding/MatcherSet.java
new file mode 100644 (file)
index 0000000..756d93a
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatcherSet.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSet extends FeatureMatcherSet
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.binding.FeatureMatcherSet
+   */
+  public static jalview.binding.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.binding.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.binding.MatcherSet.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/binding/types/ColourThreshTypeType.java b/src/jalview/binding/types/ColourThreshTypeType.java
new file mode 100644 (file)
index 0000000..024f2c0
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class ColourThreshTypeType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The NONE type
+     */
+    public static final int NONE_TYPE = 0;
+
+    /**
+     * The instance of the NONE type
+     */
+    public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE");
+
+    /**
+     * The ABOVE type
+     */
+    public static final int ABOVE_TYPE = 1;
+
+    /**
+     * The instance of the ABOVE type
+     */
+    public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE");
+
+    /**
+     * The BELOW type
+     */
+    public static final int BELOW_TYPE = 2;
+
+    /**
+     * The instance of the BELOW type
+     */
+    public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private ColourThreshTypeType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of ColourThreshTypeType
+     * 
+     * @return an Enumeration over all possible instances of
+     * ColourThreshTypeType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this ColourThreshTypeType
+     * 
+     * @return the type of this ColourThreshTypeType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("NONE", NONE);
+        members.put("ABOVE", ABOVE);
+        members.put("BELOW", BELOW);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * ColourThreshTypeType
+     * 
+     * @return the String representation of this ColourThreshTypeTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new ColourThreshTypeType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the ColourThreshTypeType value of parameter 'string'
+     */
+    public static jalview.binding.types.ColourThreshTypeType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid ColourThreshTypeType";
+            throw new IllegalArgumentException(err);
+        }
+        return (ColourThreshTypeType) obj;
+    }
+
+}
diff --git a/src/jalview/binding/types/FeatureMatcherByType.java b/src/jalview/binding/types/FeatureMatcherByType.java
new file mode 100644 (file)
index 0000000..2185bba
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class FeatureMatcherByType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The byLabel type
+     */
+    public static final int BYLABEL_TYPE = 0;
+
+    /**
+     * The instance of the byLabel type
+     */
+    public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel");
+
+    /**
+     * The byScore type
+     */
+    public static final int BYSCORE_TYPE = 1;
+
+    /**
+     * The instance of the byScore type
+     */
+    public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore");
+
+    /**
+     * The byAttribute type
+     */
+    public static final int BYATTRIBUTE_TYPE = 2;
+
+    /**
+     * The instance of the byAttribute type
+     */
+    public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private FeatureMatcherByType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of FeatureMatcherByType
+     * 
+     * @return an Enumeration over all possible instances of
+     * FeatureMatcherByType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this FeatureMatcherByType
+     * 
+     * @return the type of this FeatureMatcherByType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("byLabel", BYLABEL);
+        members.put("byScore", BYSCORE);
+        members.put("byAttribute", BYATTRIBUTE);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * FeatureMatcherByType
+     * 
+     * @return the String representation of this FeatureMatcherByTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new FeatureMatcherByType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the FeatureMatcherByType value of parameter 'string'
+     */
+    public static jalview.binding.types.FeatureMatcherByType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid FeatureMatcherByType";
+            throw new IllegalArgumentException(err);
+        }
+        return (FeatureMatcherByType) obj;
+    }
+
+}
diff --git a/src/jalview/binding/types/NoValueColour.java b/src/jalview/binding/types/NoValueColour.java
new file mode 100644 (file)
index 0000000..c1540f6
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.binding.types;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Graduated feature colour if no score (or attribute) value
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColour implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * The None type
+   */
+  public static final int NONE_TYPE = 0;
+
+  /**
+   * The instance of the None type
+   */
+  public static final NoValueColour NONE = new NoValueColour(NONE_TYPE,
+          "None");
+
+  /**
+   * The Min type
+   */
+  public static final int MIN_TYPE = 1;
+
+  /**
+   * The instance of the Min type
+   */
+  public static final NoValueColour MIN = new NoValueColour(MIN_TYPE,
+          "Min");
+
+  /**
+   * The Max type
+   */
+  public static final int MAX_TYPE = 2;
+
+  /**
+   * The instance of the Max type
+   */
+  public static final NoValueColour MAX = new NoValueColour(MAX_TYPE,
+          "Max");
+
+  /**
+   * Field _memberTable.
+   */
+  private static java.util.Hashtable _memberTable = init();
+
+  /**
+   * Field type.
+   */
+  private int type = -1;
+
+  /**
+   * Field stringValue.
+   */
+  private java.lang.String stringValue = null;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  private NoValueColour(final int type, final java.lang.String value)
+  {
+    super();
+    this.type = type;
+    this.stringValue = value;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method enumerate.Returns an enumeration of all possible instances of
+   * NoValueColour
+   * 
+   * @return an Enumeration over all possible instances of NoValueColour
+   */
+  public static java.util.Enumeration enumerate()
+  {
+    return _memberTable.elements();
+  }
+
+  /**
+   * Method getType.Returns the type of this NoValueColour
+   * 
+   * @return the type of this NoValueColour
+   */
+  public int getType()
+  {
+    return this.type;
+  }
+
+  /**
+   * Method init.
+   * 
+   * @return the initialized Hashtable for the member table
+   */
+  private static java.util.Hashtable init()
+  {
+    Hashtable members = new Hashtable();
+    members.put("None", NONE);
+    members.put("Min", MIN);
+    members.put("Max", MAX);
+    return members;
+  }
+
+  /**
+   * Method readResolve. will be called during deserialization to replace the
+   * deserialized object with the correct constant instance.
+   * 
+   * @return this deserialized object
+   */
+  private java.lang.Object readResolve()
+  {
+    return valueOf(this.stringValue);
+  }
+
+  /**
+   * Method toString.Returns the String representation of this NoValueColour
+   * 
+   * @return the String representation of this NoValueColour
+   */
+  public java.lang.String toString()
+  {
+    return this.stringValue;
+  }
+
+  /**
+   * Method valueOf.Returns a new NoValueColour based on the given String value.
+   * 
+   * @param string
+   * @return the NoValueColour value of parameter 'string'
+   */
+  public static jalview.binding.types.NoValueColour valueOf(
+          final java.lang.String string)
+  {
+    java.lang.Object obj = null;
+    if (string != null)
+    {
+      obj = _memberTable.get(string);
+    }
+    if (obj == null)
+    {
+      String err = "" + string + " is not a valid NoValueColour";
+      throw new IllegalArgumentException(err);
+    }
+    return (NoValueColour) obj;
+  }
+
+}
index dd05843..d992e4e 100644 (file)
@@ -52,29 +52,20 @@ public class AlignViewController implements AlignViewControllerI
    */
   private AlignViewControllerGuiI avcg;
 
-  @Override
-  protected void finalize() throws Throwable
-  {
-    viewport = null;
-    alignPanel = null;
-    avcg = null;
-  };
-
   public AlignViewController(AlignViewControllerGuiI alignFrame,
-          AlignViewportI viewport, AlignmentViewPanel alignPanel)
+          AlignViewportI vp, AlignmentViewPanel ap)
   {
     this.avcg = alignFrame;
-    this.viewport = viewport;
-    this.alignPanel = alignPanel;
+    this.viewport = vp;
+    this.alignPanel = ap;
   }
 
   @Override
-  public void setViewportAndAlignmentPanel(AlignViewportI viewport,
-          AlignmentViewPanel alignPanel)
+  public void setViewportAndAlignmentPanel(AlignViewportI vp,
+          AlignmentViewPanel ap)
   {
-    this.alignPanel = alignPanel;
-    this.viewport = viewport;
-
+    this.alignPanel = ap;
+    this.viewport = vp;
   }
 
   @Override
@@ -190,7 +181,7 @@ public class AlignViewController implements AlignViewControllerI
       if (changed)
       {
         viewport.setColumnSelection(cs);
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(false, false);
         int columnCount = invert
                 ? (sqcol.getEndRes() - sqcol.getStartRes() + 1)
                         - bs.cardinality()
@@ -215,7 +206,7 @@ public class AlignViewController implements AlignViewControllerI
       if (!extendCurrent)
       {
         cs.clear();
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(false, false);
       }
     }
     return false;
@@ -223,17 +214,21 @@ public class AlignViewController implements AlignViewControllerI
 
   /**
    * Sets a bit in the BitSet for each column (base 0) in the sequence
-   * collection which includes the specified feature type. Returns the number of
-   * sequences which have the feature in the selected range.
+   * collection which includes a visible feature of the specified feature type.
+   * Returns the number of sequences which have the feature visible in the
+   * selected range.
    * 
    * @param featureType
    * @param sqcol
    * @param bs
    * @return
    */
-  static int findColumnsWithFeature(String featureType,
+  int findColumnsWithFeature(String featureType,
           SequenceCollectionI sqcol, BitSet bs)
   {
+    FeatureRenderer fr = alignPanel == null ? null : alignPanel
+            .getFeatureRenderer();
+
     final int startColumn = sqcol.getStartRes() + 1; // converted to base 1
     final int endColumn = sqcol.getEndRes() + 1;
     List<SequenceI> seqs = sqcol.getSequences();
@@ -246,13 +241,19 @@ public class AlignViewController implements AlignViewControllerI
         List<SequenceFeature> sfs = sq.findFeatures(startColumn,
                 endColumn, featureType);
 
-        if (!sfs.isEmpty())
-        {
-          nseq++;
-        }
-
+        boolean found = false;
         for (SequenceFeature sf : sfs)
         {
+          if (fr.getColour(sf) == null)
+          {
+            continue;
+          }
+          if (!found)
+          {
+            nseq++;
+          }
+          found = true;
+
           int sfStartCol = sq.findIndex(sf.getBegin());
           int sfEndCol = sq.findIndex(sf.getEnd());
 
@@ -337,7 +338,7 @@ public class AlignViewController implements AlignViewControllerI
     AlignmentSorter.sortByFeature(typ, gps, start, stop, al, method);
     avcg.addHistoryItem(new OrderCommand(methodText, oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
 
   }
 
@@ -351,34 +352,34 @@ public class AlignViewController implements AlignViewControllerI
   public boolean parseFeaturesFile(String file, DataSourceType protocol,
           boolean relaxedIdMatching)
   {
-    boolean featuresFile = false;
+    boolean featuresAdded = false;
+    FeatureRenderer fr = alignPanel.getFeatureRenderer();
     try
     {
-      featuresFile = new FeaturesFile(false, file, protocol).parse(
-              viewport.getAlignment().getDataset(),
-              alignPanel.getFeatureRenderer().getFeatureColours(), false,
-              relaxedIdMatching);
+      featuresAdded = new FeaturesFile(false, file, protocol).parse(
+              viewport.getAlignment().getDataset(), fr.getFeatureColours(),
+              fr.getFeatureFilters(), false, relaxedIdMatching);
     } catch (Exception ex)
     {
       ex.printStackTrace();
     }
 
-    if (featuresFile)
+    if (featuresAdded)
     {
       avcg.refreshFeatureUI(true);
-      if (alignPanel.getFeatureRenderer() != null)
+      if (fr != null)
       {
         // update the min/max ranges where necessary
-        alignPanel.getFeatureRenderer().findAllFeatures(true);
+        fr.findAllFeatures(true);
       }
       if (avcg.getFeatureSettingsUI() != null)
       {
         avcg.getFeatureSettingsUI().discoverAllFeatureData();
       }
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
 
-    return featuresFile;
+    return featuresAdded;
 
   }
 
@@ -414,7 +415,7 @@ public class AlignViewController implements AlignViewControllerI
       if (changed)
       {
         viewport.setColumnSelection(cs);
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(false, false);
         int columnCount = invert
                 ? (sqcol.getEndRes() - sqcol.getStartRes() + 1)
                         - bs.cardinality()
@@ -438,7 +439,7 @@ public class AlignViewController implements AlignViewControllerI
       if (!extendCurrent)
       {
         cs.clear();
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(false, false);
       }
     }
     return false;
index 8c5e4ac..3ba35b6 100755 (executable)
@@ -28,10 +28,13 @@ import jalview.util.LinkedIdentityHashSet;
 import jalview.util.MessageManager;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -48,7 +51,7 @@ public class Alignment implements AlignmentI
 {
   private Alignment dataset;
 
-  protected List<SequenceI> sequences;
+  private List<SequenceI> sequences;
 
   protected List<SequenceGroup> groups;
 
@@ -197,6 +200,7 @@ public class Alignment implements AlignmentI
         return sequences.get(i);
       }
     }
+
     return null;
   }
 
@@ -705,7 +709,7 @@ public class Alignment implements AlignmentI
   public int getWidth()
   {
     int maxLength = -1;
-
+  
     for (int i = 0; i < sequences.size(); i++)
     {
       if (getSequenceAt(i).getLength() > maxLength)
@@ -713,9 +717,34 @@ public class Alignment implements AlignmentI
         maxLength = getSequenceAt(i).getLength();
       }
     }
-
+  
     return maxLength;
   }
+  /*
+  @Override
+  public int getWidth()
+  {
+    final Wrapper temp = new Wrapper();
+  
+    forEachSequence(new Consumer<SequenceI>()
+    {
+      @Override
+      public void accept(SequenceI s)
+      {
+        if (s.getLength() > temp.inner)
+        {
+          temp.inner = s.getLength();
+        }
+      }
+    }, 0, sequences.size() - 1);
+  
+    return temp.inner;
+  }
+  
+  public static class Wrapper
+  {
+    public int inner;
+  }*/
 
   /**
    * DOCUMENT ME!
@@ -1602,7 +1631,10 @@ public class Alignment implements AlignmentI
     AlignmentAnnotation annot = new AlignmentAnnotation(name, name,
             new Annotation[1], 0f, 0f, AlignmentAnnotation.BAR_GRAPH);
     annot.hasText = false;
-    annot.setCalcId(new String(calcId));
+    if (calcId != null)
+    {
+      annot.setCalcId(new String(calcId));
+    }
     annot.autoCalculated = autoCalc;
     if (seqRef != null)
     {
@@ -1617,40 +1649,21 @@ public class Alignment implements AlignmentI
   @Override
   public Iterable<AlignmentAnnotation> findAnnotation(String calcId)
   {
-    List<AlignmentAnnotation> aa = new ArrayList<>();
     AlignmentAnnotation[] alignmentAnnotation = getAlignmentAnnotation();
     if (alignmentAnnotation != null)
     {
-      for (AlignmentAnnotation a : alignmentAnnotation)
-      {
-        if (a.getCalcId() == calcId || (a.getCalcId() != null
-                && calcId != null && a.getCalcId().equals(calcId)))
-        {
-          aa.add(a);
-        }
-      }
+      return AlignmentAnnotation.findAnnotation(
+              Arrays.asList(getAlignmentAnnotation()), calcId);
     }
-    return aa;
+    return Arrays.asList(new AlignmentAnnotation[] {});
   }
 
   @Override
   public Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
           String calcId, String label)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
-    for (AlignmentAnnotation ann : getAlignmentAnnotation())
-    {
-      if ((calcId == null || (ann.getCalcId() != null
-              && ann.getCalcId().equals(calcId)))
-              && (seq == null || (ann.sequenceRef != null
-                      && ann.sequenceRef == seq))
-              && (label == null
-                      || (ann.label != null && ann.label.equals(label))))
-      {
-        aa.add(ann);
-      }
-    }
-    return aa;
+    return AlignmentAnnotation.findAnnotations(
+            Arrays.asList(getAlignmentAnnotation()), seq, calcId, label);
   }
 
   @Override
@@ -1913,4 +1926,117 @@ public class Alignment implements AlignmentI
   {
     hiddenCols = cols;
   }
+
+  @Override
+  public void setupJPredAlignment()
+  {
+    SequenceI repseq = getSequenceAt(0);
+    setSeqrep(repseq);
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideList(repseq.getInsertions());
+    setHiddenColumns(cs);
+  }
+
+  @Override
+  public HiddenColumns propagateInsertions(SequenceI profileseq,
+          AlignmentView input)
+  {
+    int profsqpos = 0;
+
+    char gc = getGapCharacter();
+    Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
+    HiddenColumns nview = (HiddenColumns) alandhidden[1];
+    SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
+    return propagateInsertions(profileseq, origseq, nview);
+  }
+
+  /**
+   * 
+   * @param profileseq
+   *          sequence in al which corresponds to origseq
+   * @param al
+   *          alignment which is to have gaps inserted into it
+   * @param origseq
+   *          sequence corresponding to profileseq which defines gap map for
+   *          modifying al
+   */
+  private HiddenColumns propagateInsertions(SequenceI profileseq,
+          SequenceI origseq, HiddenColumns hc)
+  {
+    // take the set of hidden columns, and the set of gaps in origseq,
+    // and remove all the hidden gaps from hiddenColumns
+
+    // first get the gaps as a Bitset
+    // then calculate hidden ^ not(gap)
+    BitSet gaps = origseq.gapBitset();
+    hc.andNot(gaps);
+
+    // for each sequence in the alignment, except the profile sequence,
+    // insert gaps corresponding to each hidden region but where each hidden
+    // column region is shifted backwards by the number of preceding visible
+    // gaps update hidden columns at the same time
+    HiddenColumns newhidden = new HiddenColumns();
+
+    int numGapsBefore = 0;
+    int gapPosition = 0;
+    Iterator<int[]> it = hc.iterator();
+    while (it.hasNext())
+    {
+      int[] region = it.next();
+
+      // get region coordinates accounting for gaps
+      // we can rely on gaps not being *in* hidden regions because we already
+      // removed those
+      while (gapPosition < region[0])
+      {
+        gapPosition++;
+        if (gaps.get(gapPosition))
+        {
+          numGapsBefore++;
+        }
+      }
+
+      int left = region[0] - numGapsBefore;
+      int right = region[1] - numGapsBefore;
+
+      newhidden.hideColumns(left, right);
+      padGaps(left, right, profileseq);
+    }
+    return newhidden;
+  }
+
+  /**
+   * Pad gaps in all sequences in alignment except profileseq
+   * 
+   * @param left
+   *          position of first gap to insert
+   * @param right
+   *          position of last gap to insert
+   * @param profileseq
+   *          sequence not to pad
+   */
+  private void padGaps(int left, int right, SequenceI profileseq)
+  {
+    char gc = getGapCharacter();
+
+    // make a string with number of gaps = length of hidden region
+    StringBuilder sb = new StringBuilder();
+    for (int g = 0; g < right - left + 1; g++)
+    {
+      sb.append(gc);
+    }
+
+    // loop over the sequences and pad with gaps where required
+    for (int s = 0, ns = getHeight(); s < ns; s++)
+    {
+      SequenceI sqobj = getSequenceAt(s);
+      if ((sqobj != profileseq) && (sqobj.getLength() >= left))
+      {
+        String sq = sqobj.getSequenceAsString();
+        sqobj.setSequence(
+                sq.substring(0, left) + sb.toString() + sq.substring(left));
+      }
+    }
+  }
+
 }
index e2b0046..c46b2d4 100755 (executable)
@@ -24,6 +24,7 @@ import jalview.analysis.Rna;
 import jalview.analysis.SecStrConsensus.SimpleBP;
 import jalview.analysis.WUSSParseException;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -241,19 +242,6 @@ public class AlignmentAnnotation
 
   private boolean isrna;
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see java.lang.Object#finalize()
-   */
-  @Override
-  protected void finalize() throws Throwable
-  {
-    sequenceRef = null;
-    groupRef = null;
-    super.finalize();
-  }
-
   public static int getGraphValueFromString(String string)
   {
     if (string.equalsIgnoreCase("BAR_GRAPH"))
@@ -678,7 +666,7 @@ public class AlignmentAnnotation
     this.calcId = annotation.calcId;
     if (annotation.properties != null)
     {
-      properties = new HashMap<String, String>();
+      properties = new HashMap<>();
       for (Map.Entry<String, String> val : annotation.properties.entrySet())
       {
         properties.put(val.getKey(), val.getValue());
@@ -714,7 +702,7 @@ public class AlignmentAnnotation
       if (annotation.sequenceMapping != null)
       {
         Integer p = null;
-        sequenceMapping = new HashMap<Integer, Annotation>();
+        sequenceMapping = new HashMap<>();
         Iterator<Integer> pos = annotation.sequenceMapping.keySet()
                 .iterator();
         while (pos.hasNext())
@@ -794,7 +782,7 @@ public class AlignmentAnnotation
       int epos = sequenceRef.findPosition(endRes);
       if (sequenceMapping != null)
       {
-        Map<Integer, Annotation> newmapping = new HashMap<Integer, Annotation>();
+        Map<Integer, Annotation> newmapping = new HashMap<>();
         Iterator<Integer> e = sequenceMapping.keySet().iterator();
         while (e.hasNext())
         {
@@ -922,7 +910,7 @@ public class AlignmentAnnotation
     {
       return;
     }
-    sequenceMapping = new HashMap<Integer, Annotation>();
+    sequenceMapping = new HashMap<>();
 
     int seqPos;
 
@@ -1137,7 +1125,7 @@ public class AlignmentAnnotation
     {
       return;
     }
-    hidden.makeVisibleAnnotation(this);
+    makeVisibleAnnotation(hidden);
   }
 
   public void setPadGaps(boolean padgaps, char gapchar)
@@ -1203,7 +1191,7 @@ public class AlignmentAnnotation
   /**
    * properties associated with the calcId
    */
-  protected Map<String, String> properties = new HashMap<String, String>();
+  protected Map<String, String> properties = new HashMap<>();
 
   /**
    * base colour for line graphs. If null, will be set automatically by
@@ -1249,7 +1237,7 @@ public class AlignmentAnnotation
             : false;
 
     // TODO build a better annotation element map and get rid of annotations[]
-    Map<Integer, Annotation> mapForsq = new HashMap<Integer, Annotation>();
+    Map<Integer, Annotation> mapForsq = new HashMap<>();
     if (sequenceMapping != null)
     {
       if (sp2sq != null)
@@ -1302,7 +1290,7 @@ public class AlignmentAnnotation
     if (mapping != null)
     {
       Map<Integer, Annotation> old = sequenceMapping;
-      Map<Integer, Annotation> remap = new HashMap<Integer, Annotation>();
+      Map<Integer, Annotation> remap = new HashMap<>();
       int index = -1;
       for (int mp[] : mapping.values())
       {
@@ -1360,7 +1348,7 @@ public class AlignmentAnnotation
   {
     if (properties == null)
     {
-      properties = new HashMap<String, String>();
+      properties = new HashMap<>();
     }
     properties.put(property, value);
   }
@@ -1485,4 +1473,180 @@ public class AlignmentAnnotation
   {
     return graphMin < graphMax;
   }
+
+  /**
+   * delete any columns in alignmentAnnotation that are hidden (including
+   * sequence associated annotation).
+   * 
+   * @param hiddenColumns
+   *          the set of hidden columns
+   */
+  public void makeVisibleAnnotation(HiddenColumns hiddenColumns)
+  {
+    if (annotations != null)
+    {
+      makeVisibleAnnotation(0, annotations.length, hiddenColumns);
+    }
+  }
+
+  /**
+   * delete any columns in alignmentAnnotation that are hidden (including
+   * sequence associated annotation).
+   * 
+   * @param start
+   *          remove any annotation to the right of this column
+   * @param end
+   *          remove any annotation to the left of this column
+   * @param hiddenColumns
+   *          the set of hidden columns
+   */
+  public void makeVisibleAnnotation(int start, int end,
+          HiddenColumns hiddenColumns)
+  {
+    if (annotations != null)
+    {
+      if (hiddenColumns.hasHiddenColumns())
+      {
+        removeHiddenAnnotation(start, end, hiddenColumns);
+      }
+      else
+      {
+        restrict(start, end);
+      }
+    }
+  }
+
+  /**
+   * The actual implementation of deleting hidden annotation columns
+   * 
+   * @param start
+   *          remove any annotation to the right of this column
+   * @param end
+   *          remove any annotation to the left of this column
+   * @param hiddenColumns
+   *          the set of hidden columns
+   */
+  private void removeHiddenAnnotation(int start, int end,
+          HiddenColumns hiddenColumns)
+  {
+    // mangle the alignmentAnnotation annotation array
+    ArrayList<Annotation[]> annels = new ArrayList<>();
+    Annotation[] els = null;
+
+    int w = 0;
+
+    Iterator<int[]> blocks = hiddenColumns.getVisContigsIterator(start,
+            end + 1, false);
+
+    int copylength;
+    int annotationLength;
+    while (blocks.hasNext())
+    {
+      int[] block = blocks.next();
+      annotationLength = block[1] - block[0] + 1;
+
+      if (blocks.hasNext())
+      {
+        // copy just the visible segment of the annotation row
+        copylength = annotationLength;
+      }
+      else
+      {
+        if (annotationLength + block[0] <= annotations.length)
+        {
+          // copy just the visible segment of the annotation row
+          copylength = annotationLength;
+        }
+        else
+        {
+          // copy to the end of the annotation row
+          copylength = annotations.length - block[0];
+        }
+      }
+
+      els = new Annotation[annotationLength];
+      annels.add(els);
+      System.arraycopy(annotations, block[0], els, 0, copylength);
+      w += annotationLength;
+    }
+
+    if (w != 0)
+    {
+      annotations = new Annotation[w];
+
+      w = 0;
+      for (Annotation[] chnk : annels)
+      {
+        System.arraycopy(chnk, 0, annotations, w, chnk.length);
+        w += chnk.length;
+      }
+    }
+  }
+
+  public static Iterable<AlignmentAnnotation> findAnnotations(
+          Iterable<AlignmentAnnotation> list, SequenceI seq, String calcId,
+          String label)
+  {
+
+    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
+    for (AlignmentAnnotation ann : list)
+    {
+      if ((calcId == null || (ann.getCalcId() != null
+              && ann.getCalcId().equals(calcId)))
+              && (seq == null || (ann.sequenceRef != null
+                      && ann.sequenceRef == seq))
+              && (label == null
+                      || (ann.label != null && ann.label.equals(label))))
+      {
+        aa.add(ann);
+      }
+    }
+    return aa;
+  }
+
+  /**
+   * Answer true if any annotation matches the calcId passed in (if not null).
+   * 
+   * @param list
+   *          annotation to search
+   * @param calcId
+   * @return
+   */
+  public static boolean hasAnnotation(List<AlignmentAnnotation> list,
+          String calcId)
+  {
+
+    if (calcId != null && !"".equals(calcId))
+    {
+      for (AlignmentAnnotation a : list)
+      {
+        if (a.getCalcId() == calcId)
+        {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  public static Iterable<AlignmentAnnotation> findAnnotation(
+          List<AlignmentAnnotation> list, String calcId)
+  {
+
+    List<AlignmentAnnotation> aa = new ArrayList<>();
+    if (calcId == null)
+    {
+      return aa;
+    }
+    for (AlignmentAnnotation a : list)
+    {
+
+      if (a.getCalcId() == calcId || (a.getCalcId() != null
+              && calcId != null && a.getCalcId().equals(calcId)))
+      {
+        aa.add(a);
+      }
+    }
+    return aa;
+  }
 }
index 084b80e..5fb16d6 100755 (executable)
@@ -580,6 +580,32 @@ public interface AlignmentI extends AnnotatedCollectionI
    */
   AlignedCodonFrame getMapping(SequenceI mapFrom, SequenceI mapTo);
 
+  /**
+   * Set the hidden columns collection on the alignment
+   * 
+   * @param cols
+   */
   public void setHiddenColumns(HiddenColumns cols);
 
+  /**
+   * Set the first sequence as representative and hide its insertions. Typically
+   * used when loading JPred files.
+   */
+  public void setupJPredAlignment();
+
+  /**
+   * Add gaps into the sequences aligned to profileseq under the given
+   * AlignmentView
+   * 
+   * @param profileseq
+   *          sequence in al which sequences are aligned to
+   * @param input
+   *          alignment view where sequence corresponding to profileseq is first
+   *          entry
+   * @return new HiddenColumns for new alignment view, with insertions into
+   *         profileseq marked as hidden.
+   */
+  public HiddenColumns propagateInsertions(SequenceI profileseq,
+          AlignmentView input);
+
 }
index b6224c2..17e9ea6 100644 (file)
@@ -20,7 +20,7 @@
  */
 package jalview.datamodel;
 
-import java.util.List;
+import java.util.Iterator;
 
 public class CigarArray extends CigarBase
 {
@@ -90,9 +90,7 @@ public class CigarArray extends CigarBase
           SequenceGroup selectionGroup)
   {
     this(constructSeqCigarArray(alignment, selectionGroup));
-    constructFromAlignment(alignment,
-            hidden != null ? hidden.getHiddenColumnsCopy() : null,
-            selectionGroup);
+    constructFromAlignment(alignment, hidden, selectionGroup);
   }
 
   private static int[] _calcStartEndBounds(AlignmentI alignment,
@@ -154,48 +152,38 @@ public class CigarArray extends CigarBase
    * @param selectionGroup
    */
   private void constructFromAlignment(AlignmentI alignment,
-          List<int[]> list, SequenceGroup selectionGroup)
+          HiddenColumns hidden, SequenceGroup selectionGroup)
   {
     int[] _startend = _calcStartEndBounds(alignment, selectionGroup);
-    int start = _startend[1], end = _startend[2];
+    int start = _startend[1];
+    int end = _startend[2];
     // now construct the CigarArray operations
-    if (list != null)
+    if (hidden != null)
     {
       int[] region;
-      int hideStart, hideEnd;
+      int hideStart;
+      int hideEnd;
       int last = start;
-      for (int j = 0; last < end & j < list.size(); j++)
+
+      Iterator<int[]> regions = hidden.getBoundedIterator(start, end);
+      while (regions.hasNext())
       {
-        region = list.get(j);
+        region = regions.next();
         hideStart = region[0];
         hideEnd = region[1];
-        // edit hidden regions to selection range
-        if (hideStart < last)
-        {
-          if (hideEnd > last)
-          {
-            hideStart = last;
-          }
-          else
-          {
-            continue;
-          }
-        }
 
-        if (hideStart > end)
+        // truncate region at start if last falls in region
+        if ((hideStart < last) && (hideEnd >= last))
         {
-          break;
+          hideStart = last;
         }
 
-        if (hideEnd > end)
+        // truncate region at end if end falls in region
+        if (hideEnd > end) // already checked that hideStart<=end
         {
           hideEnd = end;
         }
 
-        if (hideStart > hideEnd)
-        {
-          break;
-        }
         /**
          * form operations...
          */
@@ -206,8 +194,9 @@ public class CigarArray extends CigarBase
         addOperation(CigarArray.D, 1 + hideEnd - hideStart);
         last = hideEnd + 1;
       }
+
       // Final match if necessary.
-      if (last < end)
+      if (last <= end)
       {
         addOperation(CigarArray.M, end - last + 1);
       }
index f2ae4b7..a9b1372 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel;
 
 public interface ContiguousI
index f7837f7..98868ce 100755 (executable)
@@ -27,7 +27,20 @@ import java.util.List;
 
 public class DBRefEntry implements DBRefEntryI
 {
-  String source = "", version = "", accessionId = "";
+  /*
+   * the mapping to chromosome (genome) is held as an instance with
+   * source = speciesId
+   * version = assemblyId
+   * accessionId = "chromosome:" + chromosomeId
+   * map = mapping from sequence to reference assembly
+   */
+  public static final String CHROMOSOME = "chromosome";
+
+  String source = "";
+
+  String version = "";
+
+  String accessionId = "";
 
   /**
    * maps from associated sequence to the database sequence's coordinate system
@@ -331,4 +344,14 @@ public class DBRefEntry implements DBRefEntryI
     }
     return true;
   }
+
+  /**
+   * Mappings to chromosome are held with accessionId as "chromosome:id"
+   * 
+   * @return
+   */
+  public boolean isChromosome()
+  {
+    return accessionId != null && accessionId.startsWith(CHROMOSOME + ":");
+  }
 }
index 0ac14e5..7a30141 100755 (executable)
@@ -96,7 +96,7 @@ public class DBRefSource
    * List of databases whose sequences might have coding regions annotated
    */
   public static final String[] DNACODINGDBS = { EMBL, EMBLCDS, GENEDB,
-      ENSEMBL };
+      ENSEMBL, ENSEMBLGENOMES };
 
   public static final String[] CODINGDBS = { EMBLCDS, GENEDB, ENSEMBL };
 
@@ -105,7 +105,7 @@ public class DBRefSource
 
   public static String[] allSources()
   {
-    List<String> src = new ArrayList<String>();
+    List<String> src = new ArrayList<>();
     for (Field f : DBRefSource.class.getFields())
     {
       if (String.class.equals(f.getType()))
diff --git a/src/jalview/datamodel/GeneLociI.java b/src/jalview/datamodel/GeneLociI.java
new file mode 100644 (file)
index 0000000..f8c7ec5
--- /dev/null
@@ -0,0 +1,38 @@
+package jalview.datamodel;
+
+import jalview.util.MapList;
+
+/**
+ * An interface to model one or more contiguous regions on one chromosome
+ */
+public interface GeneLociI
+{
+  /**
+   * Answers the species identifier
+   * 
+   * @return
+   */
+  String getSpeciesId();
+
+  /**
+   * Answers the reference assembly identifier
+   * 
+   * @return
+   */
+  String getAssemblyId();
+
+  /**
+   * Answers the chromosome identifier e.g. "2", "Y", "II"
+   * 
+   * @return
+   */
+  String getChromosomeId();
+
+  /**
+   * Answers the mapping from sequence to chromosome loci. For a reverse strand
+   * mapping, the chromosomal ranges will have start > end.
+   * 
+   * @return
+   */
+  MapList getMap();
+}
index c0a43ee..a7e93da 100644 (file)
  */
 package jalview.datamodel;
 
-import jalview.util.Comparison;
-import jalview.util.ShiftList;
-
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.BitSet;
-import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Vector;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
+/**
+ * This class manages the collection of hidden columns associated with an
+ * alignment. To iterate over the collection, or over visible columns/regions,
+ * use an iterator obtained from one of:
+ * 
+ * - getBoundedIterator: iterates over the hidden regions, within some bounds,
+ * returning *absolute* positions
+ * 
+ * - getBoundedStartIterator: iterates over the start positions of hidden
+ * regions, within some bounds, returning *visible* positions
+ * 
+ * - getVisContigsIterator: iterates over visible regions in a range, returning
+ * *absolute* positions
+ * 
+ * - getVisibleColsIterator: iterates over the visible *columns*
+ * 
+ * For performance reasons, provide bounds where possible. Note that column
+ * numbering begins at 0 throughout this class.
+ * 
+ * @author kmourao
+ */
+
+/* Implementation notes:
+ * 
+ * Methods which change the hiddenColumns collection should use a writeLock to
+ * prevent other threads accessing the hiddenColumns collection while changes
+ * are being made. They should also reset the hidden columns cursor, and either
+ * update the hidden columns count, or set it to 0 (so that it will later be
+ * updated when needed).
+ * 
+ * 
+ * Methods which only need read access to the hidden columns collection should
+ * use a readLock to prevent other threads changing the hidden columns
+ * collection while it is in use.
+ */
 public class HiddenColumns
 {
+  private static final int HASH_MULTIPLIER = 31;
+
   private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
 
   /*
+   * Cursor which tracks the last used hidden columns region, and the number 
+   * of hidden columns up to (but not including) that region.
+   */
+  private HiddenColumnsCursor cursor = new HiddenColumnsCursor();
+
+  /*
+   * cache of the number of hidden columns: must be kept up to date by methods 
+   * which add or remove hidden columns
+   */
+  private int numColumns = 0;
+
+  /*
    * list of hidden column [start, end] ranges; the list is maintained in
    * ascending start column order
    */
-  private ArrayList<int[]> hiddenColumns;
+  private List<int[]> hiddenColumns = new ArrayList<>();
 
   /**
    * Constructor
@@ -51,19 +97,263 @@ public class HiddenColumns
    * Copy constructor
    * 
    * @param copy
+   *          the HiddenColumns object to copy from
    */
   public HiddenColumns(HiddenColumns copy)
   {
+    this(copy, Integer.MIN_VALUE, Integer.MAX_VALUE, 0);
+  }
+
+  /**
+   * Copy constructor within bounds and with offset. Copies hidden column
+   * regions fully contained between start and end, and offsets positions by
+   * subtracting offset.
+   * 
+   * @param copy
+   *          HiddenColumns instance to copy from
+   * @param start
+   *          lower bound to copy from
+   * @param end
+   *          upper bound to copy to
+   * @param offset
+   *          offset to subtract from each region boundary position
+   * 
+   */
+  public HiddenColumns(HiddenColumns copy, int start, int end, int offset)
+  {
     try
     {
       LOCK.writeLock().lock();
       if (copy != null)
       {
-        if (copy.hiddenColumns != null)
+        numColumns = 0;
+        Iterator<int[]> it = copy.getBoundedIterator(start, end);
+        while (it.hasNext())
+        {
+          int[] region = it.next();
+          // still need to check boundaries because iterator returns
+          // all overlapping regions and we need contained regions
+          if (region[0] >= start && region[1] <= end)
+          {
+            hiddenColumns.add(
+                    new int[]
+            { region[0] - offset, region[1] - offset });
+            numColumns += region[1] - region[0] + 1;
+          }
+        }
+        cursor = new HiddenColumnsCursor(hiddenColumns);
+      }
+    } finally
+    {
+      LOCK.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Adds the specified column range to the hidden columns collection
+   * 
+   * @param start
+   *          start of range to add (absolute position in alignment)
+   * @param end
+   *          end of range to add (absolute position in alignment)
+   */
+  public void hideColumns(int start, int end)
+  {
+    try
+    {
+      LOCK.writeLock().lock();
+
+      int previndex = 0;
+      int prevHiddenCount = 0;
+      int regionindex = 0;
+      if (!hiddenColumns.isEmpty())
+      {
+        // set up cursor reset values
+        HiddenCursorPosition cursorPos = cursor.findRegionForColumn(start, false);
+        regionindex = cursorPos.getRegionIndex();
+
+        if (regionindex > 0)
+        {
+          // get previous index and hidden count for updating the cursor later
+          previndex = regionindex - 1;
+          int[] prevRegion = hiddenColumns.get(previndex);
+          prevHiddenCount = cursorPos.getHiddenSoFar()
+                  - (prevRegion[1] - prevRegion[0] + 1);
+        }
+      }
+
+      // new range follows everything else; check first to avoid looping over
+      // whole hiddenColumns collection
+      if (hiddenColumns.isEmpty()
+              || start > hiddenColumns.get(hiddenColumns.size() - 1)[1])
+      {
+        hiddenColumns.add(new int[] { start, end });
+        numColumns += end - start + 1;
+      }
+      else
+      {
+        /*
+         * traverse existing hidden ranges and insert / amend / append as
+         * appropriate
+         */
+        boolean added = false;
+        if (regionindex > 0)
+        {
+          added = insertRangeAtRegion(regionindex - 1, start, end);
+        }
+        if (!added && regionindex < hiddenColumns.size())
+        {
+          insertRangeAtRegion(regionindex, start, end);
+        }
+      }
+
+      // reset the cursor to just before our insertion point: this saves
+      // a lot of reprocessing in large alignments
+      cursor = new HiddenColumnsCursor(hiddenColumns, previndex,
+              prevHiddenCount);
+    } finally
+    {
+      LOCK.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Insert [start, range] at the region at index i in hiddenColumns, if
+   * feasible
+   * 
+   * @param i
+   *          index to insert at
+   * @param start
+   *          start of range to insert
+   * @param end
+   *          end of range to insert
+   * @return true if range was successfully inserted
+   */
+  private boolean insertRangeAtRegion(int i, int start, int end)
+  {
+    boolean added = false;
+
+    int[] region = hiddenColumns.get(i);
+    if (end < region[0] - 1)
+    {
+      /*
+       * insert discontiguous preceding range
+       */
+      hiddenColumns.add(i, new int[] { start, end });
+      numColumns += end - start + 1;
+      added = true;
+    }
+    else if (end <= region[1])
+    {
+      /*
+       * new range overlaps existing, or is contiguous preceding it - adjust
+       * start column
+       */
+      int oldstart = region[0];
+      region[0] = Math.min(region[0], start);
+      numColumns += oldstart - region[0]; // new columns are between old and
+                                              // adjusted starts
+      added = true;
+    }
+    else if (start <= region[1] + 1)
+    {
+      /*
+       * new range overlaps existing, or is contiguous following it - adjust
+       * start and end columns
+       */
+      insertRangeAtOverlap(i, start, end, region);
+      added = true;
+    }
+    return added;
+  }
+
+  /**
+   * Insert a range whose start position overlaps an existing region and/or is
+   * contiguous to the right of the region
+   * 
+   * @param i
+   *          index to insert at
+   * @param start
+   *          start of range to insert
+   * @param end
+   *          end of range to insert
+   * @param region
+   *          the overlapped/continued region
+   */
+  private void insertRangeAtOverlap(int i, int start, int end, int[] region)
+  {
+    int oldstart = region[0];
+    int oldend = region[1];
+    region[0] = Math.min(region[0], start);
+    region[1] = Math.max(region[1], end);
+
+    numColumns += oldstart - region[0];
+
+    /*
+     * also update or remove any subsequent ranges 
+     * that are overlapped
+     */
+    int endi = i;
+    while (endi < hiddenColumns.size() - 1)
+    {
+      int[] nextRegion = hiddenColumns.get(endi + 1);
+      if (nextRegion[0] > end + 1)
+      {
+        /*
+         * gap to next hidden range - no more to update
+         */
+        break;
+      }
+      numColumns -= nextRegion[1] - nextRegion[0] + 1;
+      region[1] = Math.max(nextRegion[1], end);
+      endi++;
+    }
+    numColumns += region[1] - oldend;
+    hiddenColumns.subList(i + 1, endi + 1).clear();
+  }
+
+  /**
+   * hide a list of ranges
+   * 
+   * @param ranges
+   */
+  public void hideList(List<int[]> ranges)
+  {
+    try
+    {
+      LOCK.writeLock().lock();
+      for (int[] r : ranges)
+      {
+        hideColumns(r[0], r[1]);
+      }
+      cursor = new HiddenColumnsCursor(hiddenColumns);
+
+    } finally
+    {
+      LOCK.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Unhides, and adds to the selection list, all hidden columns
+   */
+  public void revealAllHiddenColumns(ColumnSelection sel)
+  {
+    try
+    {
+      LOCK.writeLock().lock();
+
+      for (int[] region : hiddenColumns)
+      {
+        for (int j = region[0]; j < region[1] + 1; j++)
         {
-          hiddenColumns = copy.copyHiddenRegionsToArrayList();
+          sel.addElement(j);
         }
       }
+      hiddenColumns.clear();
+      cursor = new HiddenColumnsCursor(hiddenColumns);
+      numColumns = 0;
+
     } finally
     {
       LOCK.writeLock().unlock();
@@ -71,16 +361,46 @@ public class HiddenColumns
   }
 
   /**
-   * This method is used to return all the HiddenColumn regions and is intended
-   * to remain private. External callers which need a copy of the regions can
-   * call getHiddenColumnsCopyAsList.
+   * Reveals, and marks as selected, the hidden column range with the given
+   * start column
    * 
-   * @return empty list or List of hidden column intervals
+   * @param start
+   *          the start column to look for
+   * @param sel
+   *          the column selection to add the hidden column range to
    */
-  private List<int[]> getHiddenRegions()
+  public void revealHiddenColumns(int start, ColumnSelection sel)
   {
-    return hiddenColumns == null ? Collections.<int[]> emptyList()
-            : hiddenColumns;
+    try
+    {
+      LOCK.writeLock().lock();
+
+      if (!hiddenColumns.isEmpty())
+      {
+        int regionIndex = cursor.findRegionForColumn(start, false)
+                .getRegionIndex();
+
+        if (regionIndex != -1 && regionIndex != hiddenColumns.size())
+        {
+          // regionIndex is the region which either contains start
+          // or lies to the right of start
+          int[] region = hiddenColumns.get(regionIndex);
+          if (start == region[0])
+          {
+            for (int j = region[0]; j < region[1] + 1; j++)
+            {
+              sel.addElement(j);
+            }
+            int colsToRemove = region[1] - region[0] + 1;
+            hiddenColumns.remove(regionIndex);
+            numColumns -= colsToRemove;
+          }
+        }
+      }
+    } finally
+    {
+      LOCK.writeLock().unlock();
+    }
   }
 
   /**
@@ -99,16 +419,22 @@ public class HiddenColumns
     {
       LOCK.readLock().lock();
       StringBuilder regionBuilder = new StringBuilder();
-      if (hiddenColumns != null)
+
+      boolean first = true;
+      for (int[] range : hiddenColumns)
       {
-        for (int[] range : hiddenColumns)
+        if (!first)
+        {
+          regionBuilder.append(delimiter);
+        }
+        else
         {
-          regionBuilder.append(delimiter).append(range[0]).append(between)
-                  .append(range[1]);
+          first = false;
         }
+        regionBuilder.append(range[0]).append(between).append(range[1]);
 
-        regionBuilder.deleteCharAt(0);
       }
+
       return regionBuilder.toString();
     } finally
     {
@@ -123,18 +449,20 @@ public class HiddenColumns
    */
   public int getSize()
   {
+    return numColumns;
+  }
+
+  /**
+   * Get the number of distinct hidden regions
+   * 
+   * @return number of regions
+   */
+  public int getNumberOfRegions()
+  {
     try
     {
       LOCK.readLock().lock();
-      int size = 0;
-      if (hasHiddenColumns())
-      {
-        for (int[] range : hiddenColumns)
-        {
-          size += range[1] - range[0] + 1;
-        }
-      }
-      return size;
+      return hiddenColumns.size();
     } finally
     {
       LOCK.readLock().unlock();
@@ -157,25 +485,23 @@ public class HiddenColumns
       /*
        * check hidden columns are either both null, or match
        */
-      if (this.hiddenColumns == null)
-      {
-        return (that.hiddenColumns == null);
-      }
-      if (that.hiddenColumns == null
-              || that.hiddenColumns.size() != this.hiddenColumns.size())
+
+      if (that.hiddenColumns.size() != this.hiddenColumns.size())
       {
         return false;
       }
-      int i = 0;
-      for (int[] thisRange : hiddenColumns)
+
+      Iterator<int[]> it = this.iterator();
+      Iterator<int[]> thatit = that.iterator();
+      while (it.hasNext())
       {
-        int[] thatRange = that.hiddenColumns.get(i++);
-        if (thisRange[0] != thatRange[0] || thisRange[1] != thatRange[1])
+        if (!(Arrays.equals(it.next(), thatit.next())))
         {
           return false;
         }
       }
       return true;
+
     } finally
     {
       LOCK.readLock().unlock();
@@ -189,23 +515,19 @@ public class HiddenColumns
    *          int column index in alignment view (count from zero)
    * @return alignment column index for column
    */
-  public int adjustForHiddenColumns(int column)
+  public int visibleToAbsoluteColumn(int column)
   {
     try
     {
       LOCK.readLock().lock();
       int result = column;
-      if (hiddenColumns != null)
+
+      if (!hiddenColumns.isEmpty())
       {
-        for (int i = 0; i < hiddenColumns.size(); i++)
-        {
-          int[] region = hiddenColumns.get(i);
-          if (result >= region[0])
-          {
-            result += region[1] - region[0] + 1;
-          }
-        }
+        result += cursor.findRegionForColumn(column, true)
+                .getHiddenSoFar();
       }
+
       return result;
     } finally
     {
@@ -216,54 +538,52 @@ public class HiddenColumns
   /**
    * Use this method to find out where a column will appear in the visible
    * alignment when hidden columns exist. If the column is not visible, then the
-   * left-most visible column will always be returned.
+   * index of the next visible column on the left will be returned (or 0 if
+   * there is no visible column on the left)
    * 
    * @param hiddenColumn
    *          the column index in the full alignment including hidden columns
    * @return the position of the column in the visible alignment
    */
-  public int findColumnPosition(int hiddenColumn)
+  public int absoluteToVisibleColumn(int hiddenColumn)
   {
     try
     {
       LOCK.readLock().lock();
       int result = hiddenColumn;
-      if (hiddenColumns != null)
-      {
-        int index = 0;
-        int[] region;
-        do
-        {
-          region = hiddenColumns.get(index++);
-          if (hiddenColumn > region[1])
-          {
-            result -= region[1] + 1 - region[0];
-          }
-        } while ((hiddenColumn > region[1])
-                && (index < hiddenColumns.size()));
 
-        if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
+      if (!hiddenColumns.isEmpty())
+      {
+        HiddenCursorPosition cursorPos = cursor
+                .findRegionForColumn(hiddenColumn, false);
+        int index = cursorPos.getRegionIndex();
+        int hiddenBeforeCol = cursorPos.getHiddenSoFar();
+    
+        // just subtract hidden cols count - this works fine if column is
+        // visible
+        result = hiddenColumn - hiddenBeforeCol;
+    
+        // now check in case column is hidden - it will be in the returned
+        // hidden region
+        if (index < hiddenColumns.size())
         {
-          // Here the hidden column is within a region, so
-          // we want to return the position of region[0]-1, adjusted for any
-          // earlier hidden columns.
-          // Calculate the difference between the actual hidden col position
-          // and region[0]-1, and then subtract from result to convert result
-          // from
-          // the adjusted hiddenColumn value to the adjusted region[0]-1 value
-
-          // However, if the region begins at 0 we cannot return region[0]-1
-          // just return 0
-          if (region[0] == 0)
-          {
-            return 0;
-          }
-          else
+          int[] region = hiddenColumns.get(index);
+          if (hiddenColumn >= region[0] && hiddenColumn <= region[1])
           {
-            return result - (hiddenColumn - region[0] + 1);
+            // actually col is hidden, return region[0]-1
+            // unless region[0]==0 in which case return 0
+            if (region[0] == 0)
+            {
+              result = 0;
+            }
+            else
+            {
+              result = region[0] - 1 - hiddenBeforeCol;
+            }
           }
         }
       }
+
       return result; // return the shifted position after removing hidden
                      // columns.
     } finally
@@ -274,113 +594,75 @@ public class HiddenColumns
 
   /**
    * Find the visible column which is a given visible number of columns to the
-   * left of another visible column. i.e. for a startColumn x, the column which
-   * is distance 1 away will be column x-1.
+   * left (negative visibleDistance) or right (positive visibleDistance) of
+   * startColumn. If startColumn is not visible, we use the visible column at
+   * the left boundary of the hidden region containing startColumn.
    * 
    * @param visibleDistance
-   *          the number of visible columns to offset by
+   *          the number of visible columns to offset by (left offset = negative
+   *          value; right offset = positive value)
    * @param startColumn
-   *          the column to start from
-   * @return the position of the column in the visible alignment
+   *          the position of the column to start from (absolute position)
+   * @return the position of the column which is <visibleDistance> away
+   *         (absolute position)
    */
-  public int subtractVisibleColumns(int visibleDistance, int startColumn)
+  public int offsetByVisibleColumns(int visibleDistance, int startColumn)
   {
     try
     {
-
       LOCK.readLock().lock();
-      int distance = visibleDistance;
-
-      // in case startColumn is in a hidden region, move it to the left
-      int start = adjustForHiddenColumns(findColumnPosition(startColumn));
-
-      // get index of hidden region to left of start
-      int index = getHiddenIndexLeft(start);
-      if (index == -1)
-      {
-        // no hidden regions to left of startColumn
-        return start - distance;
-      }
-
-      // walk backwards through the alignment subtracting the counts of visible
-      // columns from distance
-      int[] region;
-      int gap = 0;
-      int nextstart = start;
-
-      while ((index > -1) && (distance - gap > 0))
-      {
-        // subtract the gap to right of region from distance
-        distance -= gap;
-        start = nextstart;
-
-        // calculate the next gap
-        region = hiddenColumns.get(index);
-        gap = start - region[1];
-
-        // set start to just to left of current region
-        nextstart = region[0] - 1;
-        index--;
-      }
+      int start = absoluteToVisibleColumn(startColumn);
+      return visibleToAbsoluteColumn(start + visibleDistance);
 
-      if (distance - gap > 0)
-      {
-        // fell out of loop because there are no more hidden regions
-        distance -= gap;
-        return nextstart - distance;
-      }
-      return start - distance;
     } finally
     {
       LOCK.readLock().unlock();
     }
-
   }
 
   /**
-   * Use this method to determine the set of hiddenRegion start positions
+   * This method returns the rightmost limit of a region of an alignment with
+   * hidden columns. In otherwords, the next hidden column.
    * 
-   * @return list of column number in visible view where hidden regions start
+   * @param alPos
+   *          the absolute (visible) alignmentPosition to find the next hidden
+   *          column for
+   * @return the index of the next hidden column, or alPos if there is no next
+   *         hidden column
    */
-  public List<Integer> findHiddenRegionPositions()
+  public int getNextHiddenBoundary(boolean left, int alPos)
   {
     try
     {
       LOCK.readLock().lock();
-      List<Integer> positions = null;
-
-      if (hiddenColumns != null)
+      if (!hiddenColumns.isEmpty())
       {
-        positions = new ArrayList<>(hiddenColumns.size());
+        int index = cursor.findRegionForColumn(alPos, false)
+                .getRegionIndex();
 
-        positions.add(hiddenColumns.get(0)[0]);
-        for (int i = 1; i < hiddenColumns.size(); ++i)
+        if (left && index > 0)
         {
-
-          int result = 0;
-          if (hiddenColumns != null)
+          int[] region = hiddenColumns.get(index - 1);
+          return region[1];
+        }
+        else if (!left && index < hiddenColumns.size())
+        {
+          int[] region = hiddenColumns.get(index);
+          if (alPos < region[0])
           {
-            int index = 0;
-            int gaps = 0;
-            do
-            {
-              int[] region = hiddenColumns.get(index);
-              gaps += region[1] + 1 - region[0];
-              result = region[1] + 1;
-              index++;
-            } while (index <= i);
-
-            result -= gaps;
+            return region[0];
+          }
+          else if ((alPos <= region[1])
+                  && (index + 1 < hiddenColumns.size()))
+          {
+            // alPos is within a hidden region, return the next one
+            // if there is one
+            region = hiddenColumns.get(index + 1);
+            return region[0];
           }
-          positions.add(result);
         }
       }
-      else
-      {
-        positions = new ArrayList<>();
-      }
-
-      return positions;
+      return alPos;
     } finally
     {
       LOCK.readLock().unlock();
@@ -388,69 +670,54 @@ public class HiddenColumns
   }
 
   /**
-   * This method returns the rightmost limit of a region of an alignment with
-   * hidden columns. In otherwords, the next hidden column.
+   * Answers if a column in the alignment is visible
    * 
-   * @param index
-   *          int
+   * @param column
+   *          absolute position of column in the alignment
+   * @return true if column is visible
    */
-  public int getHiddenBoundaryRight(int alPos)
+  public boolean isVisible(int column)
   {
     try
     {
       LOCK.readLock().lock();
-      if (hiddenColumns != null)
+
+      if (!hiddenColumns.isEmpty())
       {
-        int index = 0;
-        do
+        int regionindex = cursor.findRegionForColumn(column, false)
+                .getRegionIndex();
+        if (regionindex > -1 && regionindex < hiddenColumns.size())
         {
-          int[] region = hiddenColumns.get(index);
-          if (alPos < region[0])
+          int[] region = hiddenColumns.get(regionindex);
+          // already know that column <= region[1] as cursor returns containing
+          // region or region to right
+          if (column >= region[0])
           {
-            return region[0];
+            return false;
           }
-
-          index++;
-        } while (index < hiddenColumns.size());
+        }
       }
+      return true;
 
-      return alPos;
     } finally
     {
       LOCK.readLock().unlock();
     }
-
   }
 
   /**
-   * This method returns the leftmost limit of a region of an alignment with
-   * hidden columns. In otherwords, the previous hidden column.
    * 
-   * @param index
-   *          int
+   * @return true if there are columns hidden
    */
-  public int getHiddenBoundaryLeft(int alPos)
+  public boolean hasHiddenColumns()
   {
     try
     {
       LOCK.readLock().lock();
 
-      if (hiddenColumns != null)
-      {
-        int index = hiddenColumns.size() - 1;
-        do
-        {
-          int[] region = hiddenColumns.get(index);
-          if (alPos > region[1])
-          {
-            return region[1];
-          }
-
-          index--;
-        } while (index > -1);
-      }
-
-      return alPos;
+      // we don't use getSize()>0 here because it has to iterate over
+      // the full hiddenColumns collection and so will be much slower
+      return (!hiddenColumns.isEmpty());
     } finally
     {
       LOCK.readLock().unlock();
@@ -458,258 +725,169 @@ public class HiddenColumns
   }
 
   /**
-   * This method returns the index of the hidden region to the left of a column
-   * position. If the column is in a hidden region it returns the index of the
-   * region to the left. If there is no hidden region to the left it returns -1.
    * 
-   * @param pos
-   *          int
+   * @return true if there is more than one hidden column region
    */
-  private int getHiddenIndexLeft(int pos)
+  public boolean hasMultiHiddenColumnRegions()
   {
     try
     {
-
       LOCK.readLock().lock();
-      if (hiddenColumns != null)
-      {
-        int index = hiddenColumns.size() - 1;
-        do
-        {
-          int[] region = hiddenColumns.get(index);
-          if (pos > region[1])
-          {
-            return index;
-          }
-
-          index--;
-        } while (index > -1);
-      }
-
-      return -1;
+      return !hiddenColumns.isEmpty() && hiddenColumns.size() > 1;
     } finally
     {
       LOCK.readLock().unlock();
     }
-
   }
 
+
   /**
-   * Adds the specified column range to the hidden columns
-   * 
-   * @param start
-   * @param end
+   * Returns a hashCode built from hidden column ranges
    */
-  public void hideColumns(int start, int end)
+  @Override
+  public int hashCode()
   {
-    boolean wasAlreadyLocked = false;
     try
     {
-      // check if the write lock was already locked by this thread,
-      // as this method can be called internally in loops within HiddenColumns
-      if (!LOCK.isWriteLockedByCurrentThread())
-      {
-        LOCK.writeLock().lock();
-      }
-      else
-      {
-        wasAlreadyLocked = true;
-      }
-
-      if (hiddenColumns == null)
-      {
-        hiddenColumns = new ArrayList<>();
-      }
+      LOCK.readLock().lock();
+      int hashCode = 1;
 
-      /*
-       * traverse existing hidden ranges and insert / amend / append as
-       * appropriate
-       */
-      for (int i = 0; i < hiddenColumns.size(); i++)
+      for (int[] hidden : hiddenColumns)
       {
-        int[] region = hiddenColumns.get(i);
-
-        if (end < region[0] - 1)
-        {
-          /*
-           * insert discontiguous preceding range
-           */
-          hiddenColumns.add(i, new int[] { start, end });
-          return;
-        }
-
-        if (end <= region[1])
-        {
-          /*
-           * new range overlaps existing, or is contiguous preceding it - adjust
-           * start column
-           */
-          region[0] = Math.min(region[0], start);
-          return;
-        }
-
-        if (start <= region[1] + 1)
-        {
-          /*
-           * new range overlaps existing, or is contiguous following it - adjust
-           * start and end columns
-           */
-          region[0] = Math.min(region[0], start);
-          region[1] = Math.max(region[1], end);
-
-          /*
-           * also update or remove any subsequent ranges 
-           * that are overlapped
-           */
-          while (i < hiddenColumns.size() - 1)
-          {
-            int[] nextRegion = hiddenColumns.get(i + 1);
-            if (nextRegion[0] > end + 1)
-            {
-              /*
-               * gap to next hidden range - no more to update
-               */
-              break;
-            }
-            region[1] = Math.max(nextRegion[1], end);
-            hiddenColumns.remove(i + 1);
-          }
-          return;
-        }
+        hashCode = HASH_MULTIPLIER * hashCode + hidden[0];
+        hashCode = HASH_MULTIPLIER * hashCode + hidden[1];
       }
-
-      /*
-       * remaining case is that the new range follows everything else
-       */
-      hiddenColumns.add(new int[] { start, end });
+      return hashCode;
     } finally
     {
-      if (!wasAlreadyLocked)
-      {
-        LOCK.writeLock().unlock();
-      }
+      LOCK.readLock().unlock();
     }
   }
 
-  public boolean isVisible(int column)
+  /**
+   * Hide columns corresponding to the marked bits
+   * 
+   * @param inserts
+   *          - columns mapped to bits starting from zero
+   */
+  public void hideColumns(BitSet inserts)
+  {
+    hideColumns(inserts, 0, inserts.length() - 1);
+  }
+
+  /**
+   * Hide columns corresponding to the marked bits, within the range
+   * [start,end]. Entries in tohide which are outside [start,end] are ignored.
+   * 
+   * @param tohide
+   *          columns mapped to bits starting from zero
+   * @param start
+   *          start of range to hide columns within
+   * @param end
+   *          end of range to hide columns within
+   */
+  private void hideColumns(BitSet tohide, int start, int end)
   {
     try
     {
-      LOCK.readLock().lock();
-
-      if (hiddenColumns != null)
+      LOCK.writeLock().lock();
+      for (int firstSet = tohide
+              .nextSetBit(start), lastSet = start; firstSet >= start
+                      && lastSet <= end; firstSet = tohide
+                      .nextSetBit(lastSet))
       {
-        for (int[] region : hiddenColumns)
+        lastSet = tohide.nextClearBit(firstSet);
+        if (lastSet <= end)
         {
-          if (column >= region[0] && column <= region[1])
-          {
-            return false;
-          }
+          hideColumns(firstSet, lastSet - 1);
+        }
+        else if (firstSet <= end)
+        {
+          hideColumns(firstSet, end);
         }
       }
-
-      return true;
+      cursor = new HiddenColumnsCursor(hiddenColumns);
     } finally
     {
-      LOCK.readLock().unlock();
-    }
-  }
-
-  private ArrayList<int[]> copyHiddenRegionsToArrayList()
-  {
-    int size = 0;
-    if (hiddenColumns != null)
-    {
-      size = hiddenColumns.size();
-    }
-    ArrayList<int[]> copy = new ArrayList<>(size);
-
-    for (int i = 0, j = size; i < j; i++)
-    {
-      int[] rh;
-      int[] cp;
-      rh = hiddenColumns.get(i);
-      if (rh != null)
-      {
-        cp = new int[rh.length];
-        System.arraycopy(rh, 0, cp, 0, rh.length);
-        copy.add(cp);
-      }
+      LOCK.writeLock().unlock();
     }
-
-    return copy;
   }
 
   /**
-   * Returns a copy of the vector of hidden regions, as an ArrayList. Before
-   * using this method please consider if you really need access to the hidden
-   * regions - a new (or existing!) method on HiddenColumns might be more
-   * appropriate.
+   * Hide columns corresponding to the marked bits, within the range
+   * [start,end]. Entries in tohide which are outside [start,end] are ignored.
+   * NB Existing entries in [start,end] are cleared.
    * 
-   * @return hidden regions as an ArrayList of [start,end] pairs
+   * @param tohide
+   *          columns mapped to bits starting from zero
+   * @param start
+   *          start of range to hide columns within
+   * @param end
+   *          end of range to hide columns within
    */
-  public ArrayList<int[]> getHiddenColumnsCopy()
+  public void clearAndHideColumns(BitSet tohide, int start, int end)
   {
-    try
-    {
-      LOCK.readLock().lock();
-      return copyHiddenRegionsToArrayList();
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
+    clearHiddenColumnsInRange(start, end);
+    hideColumns(tohide, start, end);
   }
 
   /**
-   * propagate shift in alignment columns to column selection
+   * Make all columns in the range [start,end] visible
    * 
    * @param start
-   *          beginning of edit
-   * @param left
-   *          shift in edit (+ve for removal, or -ve for inserts)
+   *          start of range to show columns
+   * @param end
+   *          end of range to show columns
    */
-  public List<int[]> compensateForEdit(int start, int change,
-          ColumnSelection sel)
+  private void clearHiddenColumnsInRange(int start, int end)
   {
     try
     {
       LOCK.writeLock().lock();
-      List<int[]> deletedHiddenColumns = null;
-
-      if (hiddenColumns != null)
+      
+      if (!hiddenColumns.isEmpty())
       {
-        deletedHiddenColumns = new ArrayList<>();
-        int hSize = hiddenColumns.size();
-        for (int i = 0; i < hSize; i++)
+        HiddenCursorPosition pos = cursor.findRegionForColumn(start, false);
+        int index = pos.getRegionIndex();
+
+        if (index != -1 && index != hiddenColumns.size())
         {
-          int[] region = hiddenColumns.get(i);
-          if (region[0] > start && start + change > region[1])
+          // regionIndex is the region which either contains start
+          // or lies to the right of start
+          int[] region = hiddenColumns.get(index);
+          if (region[0] < start && region[1] >= start)
           {
-            deletedHiddenColumns.add(region);
-
-            hiddenColumns.remove(i);
-            i--;
-            hSize--;
-            continue;
+            // region contains start, truncate so that it ends just before start
+            numColumns -= region[1] - start + 1;
+            region[1] = start - 1;
+            index++;
           }
 
-          if (region[0] > start)
+          int endi = index;
+          while (endi < hiddenColumns.size())
           {
-            region[0] -= change;
-            region[1] -= change;
-          }
+            region = hiddenColumns.get(endi);
 
-          if (region[0] < 0)
-          {
-            region[0] = 0;
+            if (region[1] > end)
+            {
+              if (region[0] <= end)
+              {
+                // region contains end, truncate so it starts just after end
+                numColumns -= end - region[0] + 1;
+                region[0] = end + 1;
+              }
+              break;
+            }
+
+            numColumns -= region[1] - region[0] + 1;
+            endi++;
           }
+          hiddenColumns.subList(index, endi).clear();
 
         }
 
-        this.revealHiddenColumns(0, sel);
+        cursor = new HiddenColumnsCursor(hiddenColumns);
       }
-
-      return deletedHiddenColumns;
     } finally
     {
       LOCK.writeLock().unlock();
@@ -717,47 +895,24 @@ public class HiddenColumns
   }
 
   /**
-   * propagate shift in alignment columns to column selection special version of
-   * compensateForEdit - allowing for edits within hidden regions
    * 
-   * @param start
-   *          beginning of edit
-   * @param left
-   *          shift in edit (+ve for removal, or -ve for inserts)
+   * @param updates
+   *          BitSet where hidden columns will be marked
    */
-  public void compensateForDelEdits(int start, int change)
+  protected void andNot(BitSet updates)
   {
     try
     {
       LOCK.writeLock().lock();
-      if (hiddenColumns != null)
-      {
-        for (int i = 0; i < hiddenColumns.size(); i++)
-        {
-          int[] region = hiddenColumns.get(i);
-          if (region[0] >= start)
-          {
-            region[0] -= change;
-          }
-          if (region[1] >= start)
-          {
-            region[1] -= change;
-          }
-          if (region[1] < region[0])
-          {
-            hiddenColumns.remove(i--);
-          }
 
-          if (region[0] < 0)
-          {
-            region[0] = 0;
-          }
-          if (region[1] < 0)
-          {
-            region[1] = 0;
-          }
-        }
+      BitSet hiddenBitSet = new BitSet();
+      for (int[] range : hiddenColumns)
+      {
+        hiddenBitSet.set(range[0], range[1] + 1);
       }
+      hiddenBitSet.andNot(updates);
+      hiddenColumns.clear();
+      hideColumns(hiddenBitSet);
     } finally
     {
       LOCK.writeLock().unlock();
@@ -765,135 +920,80 @@ public class HiddenColumns
   }
 
   /**
-   * return all visible segments between the given start and end boundaries
+   * Calculate the visible start and end index of an alignment.
    * 
-   * @param start
-   *          (first column inclusive from 0)
-   * @param end
-   *          (last column - not inclusive)
-   * @return int[] {i_start, i_end, ..} where intervals lie in
-   *         start<=i_start<=i_end<end
+   * @param width
+   *          full alignment width
+   * @return integer array where: int[0] = startIndex, and int[1] = endIndex
    */
-  public int[] getVisibleContigs(int start, int end)
+  public int[] getVisibleStartAndEndIndex(int width)
   {
     try
     {
       LOCK.readLock().lock();
-      if (hiddenColumns != null && hiddenColumns.size() > 0)
-      {
-        List<int[]> visiblecontigs = new ArrayList<>();
-        List<int[]> regions = getHiddenRegions();
 
-        int vstart = start;
-        int[] region;
-        int hideStart;
-        int hideEnd;
+      int firstVisible = 0;
+      int lastVisible = width - 1;
 
-        for (int j = 0; vstart < end && j < regions.size(); j++)
-        {
-          region = regions.get(j);
-          hideStart = region[0];
-          hideEnd = region[1];
-
-          if (hideEnd < vstart)
-          {
-            continue;
-          }
-          if (hideStart > vstart)
-          {
-            visiblecontigs.add(new int[] { vstart, hideStart - 1 });
-          }
-          vstart = hideEnd + 1;
-        }
+      if (!hiddenColumns.isEmpty())
+      {
+        // first visible col with index 0, convert to absolute index
+        firstVisible = visibleToAbsoluteColumn(0);
 
-        if (vstart < end)
+        // last visible column is either immediately to left of
+        // last hidden region, or is just the last column in the alignment
+        int[] lastregion = hiddenColumns.get(hiddenColumns.size() - 1);
+        if (lastregion[1] == width - 1)
         {
-          visiblecontigs.add(new int[] { vstart, end - 1 });
+          // last region is at very end of alignment
+          // last visible column immediately precedes it
+          lastVisible = lastregion[0] - 1;
         }
-        int[] vcontigs = new int[visiblecontigs.size() * 2];
-        for (int i = 0, j = visiblecontigs.size(); i < j; i++)
-        {
-          int[] vc = visiblecontigs.get(i);
-          visiblecontigs.set(i, null);
-          vcontigs[i * 2] = vc[0];
-          vcontigs[i * 2 + 1] = vc[1];
-        }
-        visiblecontigs.clear();
-        return vcontigs;
-      }
-      else
-      {
-        return new int[] { start, end - 1 };
       }
+      return new int[] { firstVisible, lastVisible };
+
     } finally
     {
       LOCK.readLock().unlock();
     }
   }
 
-  public String[] getVisibleSequenceStrings(int start, int end,
-          SequenceI[] seqs)
+  /**
+   * Finds the hidden region (if any) which starts or ends at res
+   * 
+   * @param res
+   *          visible residue position, unadjusted for hidden columns
+   * @return region as [start,end] or null if no matching region is found. If
+   *         res is adjacent to two regions, returns the left region.
+   */
+  public int[] getRegionWithEdgeAtRes(int res)
   {
     try
     {
       LOCK.readLock().lock();
-      int iSize = seqs.length;
-      String[] selections = new String[iSize];
-      if (hiddenColumns != null && hiddenColumns.size() > 0)
-      {
-        for (int i = 0; i < iSize; i++)
-        {
-          StringBuffer visibleSeq = new StringBuffer();
-          List<int[]> regions = getHiddenRegions();
-
-          int blockStart = start;
-          int blockEnd = end;
-          int[] region;
-          int hideStart;
-          int hideEnd;
+      int adjres = visibleToAbsoluteColumn(res);
 
-          for (int j = 0; j < regions.size(); j++)
-          {
-            region = regions.get(j);
-            hideStart = region[0];
-            hideEnd = region[1];
-
-            if (hideStart < start)
-            {
-              continue;
-            }
-
-            blockStart = Math.min(blockStart, hideEnd + 1);
-            blockEnd = Math.min(blockEnd, hideStart);
-
-            if (blockStart > blockEnd)
-            {
-              break;
-            }
-
-            visibleSeq.append(seqs[i].getSequence(blockStart, blockEnd));
-
-            blockStart = hideEnd + 1;
-            blockEnd = end;
-          }
-
-          if (end > blockStart)
-          {
-            visibleSeq.append(seqs[i].getSequence(blockStart, end));
-          }
+      int[] reveal = null;
 
-          selections[i] = visibleSeq.toString();
-        }
-      }
-      else
+      if (!hiddenColumns.isEmpty())
       {
-        for (int i = 0; i < iSize; i++)
+        // look for a region ending just before adjres
+        int regionindex = cursor.findRegionForColumn(adjres - 1, false)
+                .getRegionIndex();
+        if (regionindex < hiddenColumns.size()
+                && hiddenColumns.get(regionindex)[1] == adjres - 1)
+        {
+          reveal = hiddenColumns.get(regionindex);
+        }
+        // check if the region ends just after adjres
+        else if (regionindex < hiddenColumns.size()
+                && hiddenColumns.get(regionindex)[0] == adjres + 1)
         {
-          selections[i] = seqs[i].getSequenceAsString(start, end);
+          reveal = hiddenColumns.get(regionindex);
         }
       }
+      return reveal;
 
-      return selections;
     } finally
     {
       LOCK.readLock().unlock();
@@ -901,92 +1001,14 @@ public class HiddenColumns
   }
 
   /**
-   * Locate the first and last position visible for this sequence. if seq isn't
-   * visible then return the position of the left and right of the hidden
-   * boundary region, and the corresponding alignment column indices for the
-   * extent of the sequence
-   * 
-   * @param seq
-   * @return int[] { visible start, visible end, first seqpos, last seqpos,
-   *         alignment index for seq start, alignment index for seq end }
+   * Return an iterator over the hidden regions
    */
-  public int[] locateVisibleBoundsOfSequence(SequenceI seq)
+  public Iterator<int[]> iterator()
   {
     try
     {
       LOCK.readLock().lock();
-      int fpos = seq.getStart();
-      int lpos = seq.getEnd();
-      int start = 0;
-
-      if (hiddenColumns == null || hiddenColumns.size() == 0)
-      {
-        int ifpos = seq.findIndex(fpos) - 1;
-        int ilpos = seq.findIndex(lpos) - 1;
-        return new int[] { ifpos, ilpos, fpos, lpos, ifpos, ilpos };
-      }
-
-      // Simply walk along the sequence whilst watching for hidden column
-      // boundaries
-      List<int[]> regions = getHiddenRegions();
-      int spos = fpos;
-      int lastvispos = -1;
-      int rcount = 0;
-      int hideStart = seq.getLength();
-      int hideEnd = -1;
-      int visPrev = 0;
-      int visNext = 0;
-      int firstP = -1;
-      int lastP = -1;
-      boolean foundStart = false;
-      for (int p = 0, pLen = seq.getLength(); spos <= seq.getEnd()
-              && p < pLen; p++)
-      {
-        if (!Comparison.isGap(seq.getCharAt(p)))
-        {
-          // keep track of first/last column
-          // containing sequence data regardless of visibility
-          if (firstP == -1)
-          {
-            firstP = p;
-          }
-          lastP = p;
-          // update hidden region start/end
-          while (hideEnd < p && rcount < regions.size())
-          {
-            int[] region = regions.get(rcount++);
-            visPrev = visNext;
-            visNext += region[0] - visPrev;
-            hideStart = region[0];
-            hideEnd = region[1];
-          }
-          if (hideEnd < p)
-          {
-            hideStart = seq.getLength();
-          }
-          // update visible boundary for sequence
-          if (p < hideStart)
-          {
-            if (!foundStart)
-            {
-              fpos = spos;
-              start = p;
-              foundStart = true;
-            }
-            lastvispos = p;
-            lpos = spos;
-          }
-          // look for next sequence position
-          spos++;
-        }
-      }
-      if (foundStart)
-      {
-        return new int[] { findColumnPosition(start),
-            findColumnPosition(lastvispos), fpos, lpos, firstP, lastP };
-      }
-      // otherwise, sequence was completely hidden
-      return new int[] { visPrev, visNext, 0, 0, firstP, lastP };
+      return new RangeIterator(hiddenColumns);
     } finally
     {
       LOCK.readLock().unlock();
@@ -994,120 +1016,54 @@ public class HiddenColumns
   }
 
   /**
-   * delete any columns in alignmentAnnotation that are hidden (including
-   * sequence associated annotation).
+   * Return a bounded iterator over the hidden regions
    * 
-   * @param alignmentAnnotation
+   * @param start
+   *          position to start from (inclusive, absolute column position)
+   * @param end
+   *          position to end at (inclusive, absolute column position)
+   * @return
    */
-  public void makeVisibleAnnotation(AlignmentAnnotation alignmentAnnotation)
+  public Iterator<int[]> getBoundedIterator(int start, int end)
   {
-    makeVisibleAnnotation(-1, -1, alignmentAnnotation);
+    try
+    {
+      LOCK.readLock().lock();
+      return new RangeIterator(start, end, hiddenColumns);
+    } finally
+    {
+      LOCK.readLock().unlock();
+    }
   }
 
   /**
-   * delete any columns in alignmentAnnotation that are hidden (including
-   * sequence associated annotation).
+   * Return a bounded iterator over the *visible* start positions of hidden
+   * regions
    * 
    * @param start
-   *          remove any annotation to the right of this column
+   *          position to start from (inclusive, visible column position)
    * @param end
-   *          remove any annotation to the left of this column
-   * @param alignmentAnnotation
-   *          the annotation to operate on
+   *          position to end at (inclusive, visible column position)
    */
-  public void makeVisibleAnnotation(int start, int end,
-          AlignmentAnnotation alignmentAnnotation)
+  public Iterator<Integer> getStartRegionIterator(int start, int end)
   {
     try
     {
       LOCK.readLock().lock();
-      if (alignmentAnnotation.annotations == null)
-      {
-        return;
-      }
-      if (start == end && end == -1)
-      {
-        start = 0;
-        end = alignmentAnnotation.annotations.length;
-      }
-      if (hiddenColumns != null && hiddenColumns.size() > 0)
-      {
-        // then mangle the alignmentAnnotation annotation array
-        Vector<Annotation[]> annels = new Vector<>();
-        Annotation[] els = null;
-        List<int[]> regions = getHiddenRegions();
-        int blockStart = start;
-        int blockEnd = end;
-        int[] region;
-        int hideStart;
-        int hideEnd;
-        int w = 0;
-
-        for (int j = 0; j < regions.size(); j++)
-        {
-          region = regions.get(j);
-          hideStart = region[0];
-          hideEnd = region[1];
-
-          if (hideStart < start)
-          {
-            continue;
-          }
-
-          blockStart = Math.min(blockStart, hideEnd + 1);
-          blockEnd = Math.min(blockEnd, hideStart);
-
-          if (blockStart > blockEnd)
-          {
-            break;
-          }
-
-          annels.addElement(els = new Annotation[blockEnd - blockStart]);
-          System.arraycopy(alignmentAnnotation.annotations, blockStart, els,
-                  0, els.length);
-          w += els.length;
-          blockStart = hideEnd + 1;
-          blockEnd = end;
-        }
 
-        if (end > blockStart)
-        {
-          annels.addElement(els = new Annotation[end - blockStart + 1]);
-          if ((els.length
-                  + blockStart) <= alignmentAnnotation.annotations.length)
-          {
-            // copy just the visible segment of the annotation row
-            System.arraycopy(alignmentAnnotation.annotations, blockStart,
-                    els, 0, els.length);
-          }
-          else
-          {
-            // copy to the end of the annotation row
-            System.arraycopy(alignmentAnnotation.annotations, blockStart,
-                    els, 0,
-                    (alignmentAnnotation.annotations.length - blockStart));
-          }
-          w += els.length;
-        }
-        if (w == 0)
-        {
-          return;
-        }
+      // get absolute position of column in alignment
+      int absoluteStart = visibleToAbsoluteColumn(start);
 
-        alignmentAnnotation.annotations = new Annotation[w];
-        w = 0;
+      // Get cursor position and supply it to the iterator:
+      // Since we want visible region start, we look for a cursor for the
+      // (absoluteStart-1), then if absoluteStart is the start of a visible
+      // region we'll get the cursor pointing to the region before, which is
+      // what we want
+      HiddenCursorPosition pos = cursor
+              .findRegionForColumn(absoluteStart - 1, false);
 
-        for (Annotation[] chnk : annels)
-        {
-          System.arraycopy(chnk, 0, alignmentAnnotation.annotations, w,
-                  chnk.length);
-          w += chnk.length;
-        }
-      }
-      else
-      {
-        alignmentAnnotation.restrict(start, end);
-      }
+      return new StartRegionIterator(pos, start, end,
+              hiddenColumns);
     } finally
     {
       LOCK.readLock().unlock();
@@ -1115,15 +1071,21 @@ public class HiddenColumns
   }
 
   /**
+   * Return an iterator over visible *columns* (not regions) between the given
+   * start and end boundaries
    * 
-   * @return true if there are columns hidden
+   * @param start
+   *          first column (inclusive)
+   * @param end
+   *          last column (inclusive)
    */
-  public boolean hasHiddenColumns()
+  public Iterator<Integer> getVisibleColsIterator(int start, int end)
   {
     try
     {
       LOCK.readLock().lock();
-      return hiddenColumns != null && hiddenColumns.size() > 0;
+      return new RangeElementsIterator(
+              new VisibleContigsIterator(start, end + 1, hiddenColumns));
     } finally
     {
       LOCK.readLock().unlock();
@@ -1131,577 +1093,36 @@ public class HiddenColumns
   }
 
   /**
+   * return an iterator over visible segments between the given start and end
+   * boundaries
    * 
-   * @return true if there are more than one set of columns hidden
+   * @param start
+   *          first column, inclusive from 0
+   * @param end
+   *          last column - not inclusive
+   * @param useVisibleCoords
+   *          if true, start and end are visible column positions, not absolute
+   *          positions*
    */
-  public boolean hasManyHiddenColumns()
+  public VisibleContigsIterator getVisContigsIterator(int start,
+          int end,
+          boolean useVisibleCoords)
   {
+    int adjstart = start;
+    int adjend = end;
+    if (useVisibleCoords)
+    {
+      adjstart = visibleToAbsoluteColumn(start);
+      adjend = visibleToAbsoluteColumn(end);
+    }
+
     try
     {
       LOCK.readLock().lock();
-      return hiddenColumns != null && hiddenColumns.size() > 1;
+      return new VisibleContigsIterator(adjstart, adjend, hiddenColumns);
     } finally
     {
       LOCK.readLock().unlock();
     }
   }
-
-  /**
-   * mark the columns corresponding to gap characters as hidden in the column
-   * selection
-   * 
-   * @param sr
-   */
-  public void hideInsertionsFor(SequenceI sr)
-  {
-    try
-    {
-      LOCK.writeLock().lock();
-      List<int[]> inserts = sr.getInsertions();
-      for (int[] r : inserts)
-      {
-        hideColumns(r[0], r[1]);
-      }
-    } finally
-    {
-      LOCK.writeLock().unlock();
-    }
-  }
-
-  /**
-   * Unhides, and adds to the selection list, all hidden columns
-   */
-  public void revealAllHiddenColumns(ColumnSelection sel)
-  {
-    try
-    {
-      LOCK.writeLock().lock();
-      if (hiddenColumns != null)
-      {
-        for (int i = 0; i < hiddenColumns.size(); i++)
-        {
-          int[] region = hiddenColumns.get(i);
-          for (int j = region[0]; j < region[1] + 1; j++)
-          {
-            sel.addElement(j);
-          }
-        }
-      }
-
-      hiddenColumns = null;
-    } finally
-    {
-      LOCK.writeLock().unlock();
-    }
-  }
-
-  /**
-   * Reveals, and marks as selected, the hidden column range with the given
-   * start column
-   * 
-   * @param start
-   */
-  public void revealHiddenColumns(int start, ColumnSelection sel)
-  {
-    try
-    {
-      LOCK.writeLock().lock();
-      for (int i = 0; i < hiddenColumns.size(); i++)
-      {
-        int[] region = hiddenColumns.get(i);
-        if (start == region[0])
-        {
-          for (int j = region[0]; j < region[1] + 1; j++)
-          {
-            sel.addElement(j);
-          }
-
-          hiddenColumns.remove(region);
-          break;
-        }
-      }
-      if (hiddenColumns.size() == 0)
-      {
-        hiddenColumns = null;
-      }
-    } finally
-    {
-      LOCK.writeLock().unlock();
-    }
-  }
-
-  /**
-   * removes intersection of position,length ranges in deletions from the
-   * start,end regions marked in intervals.
-   * 
-   * @param shifts
-   * @param intervals
-   * @return
-   */
-  private boolean pruneIntervalList(final List<int[]> shifts,
-          ArrayList<int[]> intervals)
-  {
-    boolean pruned = false;
-    int i = 0;
-    int j = intervals.size() - 1;
-    int s = 0;
-    int t = shifts.size() - 1;
-    int[] hr = intervals.get(i);
-    int[] sr = shifts.get(s);
-    while (i <= j && s <= t)
-    {
-      boolean trailinghn = hr[1] >= sr[0];
-      if (!trailinghn)
-      {
-        if (i < j)
-        {
-          hr = intervals.get(++i);
-        }
-        else
-        {
-          i++;
-        }
-        continue;
-      }
-      int endshift = sr[0] + sr[1]; // deletion ranges - -ve means an insert
-      if (endshift < hr[0] || endshift < sr[0])
-      { // leadinghc disjoint or not a deletion
-        if (s < t)
-        {
-          sr = shifts.get(++s);
-        }
-        else
-        {
-          s++;
-        }
-        continue;
-      }
-      boolean leadinghn = hr[0] >= sr[0];
-      boolean leadinghc = hr[0] < endshift;
-      boolean trailinghc = hr[1] < endshift;
-      if (leadinghn)
-      {
-        if (trailinghc)
-        { // deleted hidden region.
-          intervals.remove(i);
-          pruned = true;
-          j--;
-          if (i <= j)
-          {
-            hr = intervals.get(i);
-          }
-          continue;
-        }
-        if (leadinghc)
-        {
-          hr[0] = endshift; // clip c terminal region
-          leadinghn = !leadinghn;
-          pruned = true;
-        }
-      }
-      if (!leadinghn)
-      {
-        if (trailinghc)
-        {
-          if (trailinghn)
-          {
-            hr[1] = sr[0] - 1;
-            pruned = true;
-          }
-        }
-        else
-        {
-          // sr contained in hr
-          if (s < t)
-          {
-            sr = shifts.get(++s);
-          }
-          else
-          {
-            s++;
-          }
-          continue;
-        }
-      }
-    }
-    return pruned; // true if any interval was removed or modified by
-    // operations.
-  }
-
-  /**
-   * remove any hiddenColumns or selected columns and shift remaining based on a
-   * series of position, range deletions.
-   * 
-   * @param deletions
-   */
-  public void pruneDeletions(List<int[]> shifts)
-  {
-    try
-    {
-      LOCK.writeLock().lock();
-      // delete any intervals intersecting.
-      if (hiddenColumns != null)
-      {
-        pruneIntervalList(shifts, hiddenColumns);
-        if (hiddenColumns != null && hiddenColumns.size() == 0)
-        {
-          hiddenColumns = null;
-        }
-      }
-    } finally
-    {
-      LOCK.writeLock().unlock();
-    }
-  }
-
-  /**
-   * Add gaps into the sequences aligned to profileseq under the given
-   * AlignmentView
-   * 
-   * @param profileseq
-   * @param al
-   *          - alignment to have gaps inserted into it
-   * @param input
-   *          - alignment view where sequence corresponding to profileseq is
-   *          first entry
-   * @return new HiddenColumns for new alignment view, with insertions into
-   *         profileseq marked as hidden.
-   */
-  public static HiddenColumns propagateInsertions(SequenceI profileseq,
-          AlignmentI al, AlignmentView input)
-  {
-    int profsqpos = 0;
-
-    char gc = al.getGapCharacter();
-    Object[] alandhidden = input.getAlignmentAndHiddenColumns(gc);
-    HiddenColumns nview = (HiddenColumns) alandhidden[1];
-    SequenceI origseq = ((SequenceI[]) alandhidden[0])[profsqpos];
-    nview.propagateInsertions(profileseq, al, origseq);
-    return nview;
-  }
-
-  /**
-   * 
-   * @param profileseq
-   *          - sequence in al which corresponds to origseq
-   * @param al
-   *          - alignment which is to have gaps inserted into it
-   * @param origseq
-   *          - sequence corresponding to profileseq which defines gap map for
-   *          modifying al
-   */
-  private void propagateInsertions(SequenceI profileseq, AlignmentI al,
-          SequenceI origseq)
-  {
-    char gc = al.getGapCharacter();
-    // recover mapping between sequence's non-gap positions and positions
-    // mapping to view.
-    pruneDeletions(ShiftList.parseMap(origseq.gapMap()));
-    int[] viscontigs = getVisibleContigs(0, profileseq.getLength());
-    int spos = 0;
-    int offset = 0;
-
-    // add profile to visible contigs
-    for (int v = 0; v < viscontigs.length; v += 2)
-    {
-      if (viscontigs[v] > spos)
-      {
-        StringBuffer sb = new StringBuffer();
-        for (int s = 0, ns = viscontigs[v] - spos; s < ns; s++)
-        {
-          sb.append(gc);
-        }
-        for (int s = 0, ns = al.getHeight(); s < ns; s++)
-        {
-          SequenceI sqobj = al.getSequenceAt(s);
-          if (sqobj != profileseq)
-          {
-            String sq = al.getSequenceAt(s).getSequenceAsString();
-            if (sq.length() <= spos + offset)
-            {
-              // pad sequence
-              int diff = spos + offset - sq.length() - 1;
-              if (diff > 0)
-              {
-                // pad gaps
-                sq = sq + sb;
-                while ((diff = spos + offset - sq.length() - 1) > 0)
-                {
-                  // sq = sq
-                  // + ((diff >= sb.length()) ? sb.toString() : sb
-                  // .substring(0, diff));
-                  if (diff >= sb.length())
-                  {
-                    sq += sb.toString();
-                  }
-                  else
-                  {
-                    char[] buf = new char[diff];
-                    sb.getChars(0, diff, buf, 0);
-                    sq += buf.toString();
-                  }
-                }
-              }
-              sq += sb.toString();
-            }
-            else
-            {
-              al.getSequenceAt(s).setSequence(sq.substring(0, spos + offset)
-                      + sb.toString() + sq.substring(spos + offset));
-            }
-          }
-        }
-        // offset+=sb.length();
-      }
-      spos = viscontigs[v + 1] + 1;
-    }
-    if ((offset + spos) < profileseq.getLength())
-    {
-      // pad the final region with gaps.
-      StringBuffer sb = new StringBuffer();
-      for (int s = 0, ns = profileseq.getLength() - spos
-              - offset; s < ns; s++)
-      {
-        sb.append(gc);
-      }
-      for (int s = 0, ns = al.getHeight(); s < ns; s++)
-      {
-        SequenceI sqobj = al.getSequenceAt(s);
-        if (sqobj == profileseq)
-        {
-          continue;
-        }
-        String sq = sqobj.getSequenceAsString();
-        // pad sequence
-        int diff = origseq.getLength() - sq.length();
-        while (diff > 0)
-        {
-          // sq = sq
-          // + ((diff >= sb.length()) ? sb.toString() : sb
-          // .substring(0, diff));
-          if (diff >= sb.length())
-          {
-            sq += sb.toString();
-          }
-          else
-          {
-            char[] buf = new char[diff];
-            sb.getChars(0, diff, buf, 0);
-            sq += buf.toString();
-          }
-          diff = origseq.getLength() - sq.length();
-        }
-      }
-    }
-  }
-
-  /**
-   * remove any hiddenColumns or selected columns and shift remaining based on a
-   * series of position, range deletions.
-   * 
-   * @param deletions
-   */
-  private void pruneDeletions(ShiftList deletions)
-  {
-    if (deletions != null)
-    {
-      final List<int[]> shifts = deletions.getShifts();
-      if (shifts != null && shifts.size() > 0)
-      {
-        pruneDeletions(shifts);
-
-        // and shift the rest.
-        this.compensateForEdits(deletions);
-      }
-    }
-  }
-
-  /**
-   * Adjust hidden column boundaries based on a series of column additions or
-   * deletions in visible regions.
-   * 
-   * @param shiftrecord
-   * @return
-   */
-  private ShiftList compensateForEdits(ShiftList shiftrecord)
-  {
-    if (shiftrecord != null)
-    {
-      final List<int[]> shifts = shiftrecord.getShifts();
-      if (shifts != null && shifts.size() > 0)
-      {
-        int shifted = 0;
-        for (int i = 0, j = shifts.size(); i < j; i++)
-        {
-          int[] sh = shifts.get(i);
-          compensateForDelEdits(shifted + sh[0], sh[1]);
-          shifted -= sh[1];
-        }
-      }
-      return shiftrecord.getInverse();
-    }
-    return null;
-  }
-
-  /**
-   * Returns a hashCode built from hidden column ranges
-   */
-  @Override
-  public int hashCode()
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      int hashCode = 1;
-      if (hiddenColumns != null)
-      {
-        for (int[] hidden : hiddenColumns)
-        {
-          hashCode = 31 * hashCode + hidden[0];
-          hashCode = 31 * hashCode + hidden[1];
-        }
-      }
-      return hashCode;
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
-  /**
-   * Hide columns corresponding to the marked bits
-   * 
-   * @param inserts
-   *          - columns map to bits starting from zero
-   */
-  public void hideMarkedBits(BitSet inserts)
-  {
-    try
-    {
-      LOCK.writeLock().lock();
-      for (int firstSet = inserts
-              .nextSetBit(0), lastSet = 0; firstSet >= 0; firstSet = inserts
-                      .nextSetBit(lastSet))
-      {
-        lastSet = inserts.nextClearBit(firstSet);
-        hideColumns(firstSet, lastSet - 1);
-      }
-    } finally
-    {
-      LOCK.writeLock().unlock();
-    }
-  }
-
-  /**
-   * 
-   * @param inserts
-   *          BitSet where hidden columns will be marked
-   */
-  public void markHiddenRegions(BitSet inserts)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      if (hiddenColumns == null)
-      {
-        return;
-      }
-      for (int[] range : hiddenColumns)
-      {
-        inserts.set(range[0], range[1] + 1);
-      }
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
-  /**
-   * Calculate the visible start and end index of an alignment.
-   * 
-   * @param width
-   *          full alignment width
-   * @return integer array where: int[0] = startIndex, and int[1] = endIndex
-   */
-  public int[] getVisibleStartAndEndIndex(int width)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      int[] alignmentStartEnd = new int[] { 0, width - 1 };
-      int startPos = alignmentStartEnd[0];
-      int endPos = alignmentStartEnd[1];
-
-      int[] lowestRange = new int[] { -1, -1 };
-      int[] higestRange = new int[] { -1, -1 };
-
-      if (hiddenColumns == null)
-      {
-        return new int[] { startPos, endPos };
-      }
-
-      for (int[] hiddenCol : hiddenColumns)
-      {
-        lowestRange = (hiddenCol[0] <= startPos) ? hiddenCol : lowestRange;
-        higestRange = (hiddenCol[1] >= endPos) ? hiddenCol : higestRange;
-      }
-
-      if (lowestRange[0] == -1 && lowestRange[1] == -1)
-      {
-        startPos = alignmentStartEnd[0];
-      }
-      else
-      {
-        startPos = lowestRange[1] + 1;
-      }
-
-      if (higestRange[0] == -1 && higestRange[1] == -1)
-      {
-        endPos = alignmentStartEnd[1];
-      }
-      else
-      {
-        endPos = higestRange[0] - 1;
-      }
-      return new int[] { startPos, endPos };
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-
-  }
-
-  /**
-   * Finds the hidden region (if any) which starts or ends at res
-   * 
-   * @param res
-   *          visible residue position, unadjusted for hidden columns
-   * @return region as [start,end] or null if no matching region is found
-   */
-  public int[] getRegionWithEdgeAtRes(int res)
-  {
-    try
-    {
-      LOCK.readLock().lock();
-      int adjres = adjustForHiddenColumns(res);
-
-      int[] reveal = null;
-      if (hiddenColumns != null)
-      {
-        for (int[] region : hiddenColumns)
-        {
-          if (adjres + 1 == region[0] || adjres - 1 == region[1])
-          {
-            reveal = region;
-            break;
-          }
-        }
-      }
-      return reveal;
-    } finally
-    {
-      LOCK.readLock().unlock();
-    }
-  }
-
 }
diff --git a/src/jalview/datamodel/HiddenColumnsCursor.java b/src/jalview/datamodel/HiddenColumnsCursor.java
new file mode 100644 (file)
index 0000000..2e9d798
--- /dev/null
@@ -0,0 +1,213 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HiddenColumnsCursor
+{
+  // absolute position of first hidden column
+  private int firstColumn;
+
+  private List<int[]> hiddenColumns = new ArrayList<>();
+
+  private HiddenCursorPosition cursorPos = new HiddenCursorPosition(0, 0);
+
+  protected HiddenColumnsCursor()
+  {
+
+  }
+
+  protected HiddenColumnsCursor(List<int[]> hiddenCols)
+  {
+    resetCursor(hiddenCols, 0, 0);
+  }
+
+  protected HiddenColumnsCursor(List<int[]> hiddenCols, int index,
+          int hiddencount)
+  {
+    resetCursor(hiddenCols, index, hiddencount);
+  }
+
+  /**
+   * Reset the cursor with a new hidden columns collection, where we know in
+   * advance the index and hidden columns count of a particular location.
+   * 
+   * @param hiddenCols
+   *          new hidden columns collection
+   * @param index
+   *          cursor index to reset to
+   * @param hiddencount
+   *          hidden columns count to reset to
+   */
+  private void resetCursor(List<int[]> hiddenCols, int index,
+          int hiddencount)
+  {
+    hiddenColumns = hiddenCols;
+    if (!hiddenCols.isEmpty())
+    {
+      firstColumn = hiddenColumns.get(0)[0];
+      cursorPos = new HiddenCursorPosition(index,
+              hiddencount);
+    }
+  }
+
+  /**
+   * Get the cursor pointing to the hidden region that column is within (if
+   * column is hidden) or which is to the right of column (if column is
+   * visible). If no hidden columns are to the right, returns a cursor pointing
+   * to an imaginary hidden region beyond the end of the hidden columns
+   * collection (this ensures the count of previous hidden columns is correct).
+   * If hidden columns is empty returns null.
+   * 
+   * @param column
+   *          index of column in visible or absolute coordinates
+   * @param useVisible
+   *          true if column is in visible coordinates, false if absolute
+   * @return cursor pointing to hidden region containing the column (if hidden)
+   *         or to the right of the column (if visible)
+   */
+  protected HiddenCursorPosition findRegionForColumn(int column,
+          boolean useVisible)
+  {
+    if (hiddenColumns.isEmpty())
+    {
+      return null;
+    }
+
+    // used to add in hiddenColumns offset when working with visible columns
+    int offset = (useVisible ? 1 : 0);
+
+    HiddenCursorPosition pos = cursorPos;
+    int index = pos.getRegionIndex();
+    int hiddenCount = pos.getHiddenSoFar();
+
+    if (column < firstColumn)
+    {
+      pos = new HiddenCursorPosition(0, 0);
+    }
+
+    // column is after current region
+    else if ((index < hiddenColumns.size())
+            && (hiddenColumns.get(index)[0] <= column
+                    + offset * hiddenCount))
+    {
+      // iterate from where we are now, if we're lucky we'll be close by
+      // (but still better than iterating from 0)
+      // stop when we find the region *before* column
+      // i.e. the next region starts after column or if not, ends after column
+      pos = searchForward(pos, column, useVisible);
+    }
+
+    // column is before current region
+    else
+    {
+      pos = searchBackward(pos, column, useVisible);
+    }
+    cursorPos = pos;
+    return pos;
+  }
+
+  /**
+   * Search forwards through the hidden columns collection to find the hidden
+   * region immediately before a column
+   * 
+   * @param pos
+   *          current position
+   * @param column
+   *          column to locate
+   * @param useVisible
+   *          whether using visible or absolute coordinates
+   * @return position of region before column
+   */
+  private HiddenCursorPosition searchForward(HiddenCursorPosition pos,
+          int column, boolean useVisible)
+  {
+    HiddenCursorPosition p = pos;
+    if (useVisible)
+    {
+      while ((p.getRegionIndex() < hiddenColumns.size())
+              && hiddenColumns.get(p.getRegionIndex())[0] <= column
+                      + p.getHiddenSoFar())
+      {
+        p = stepForward(p);
+      }
+    }
+    else
+    {
+      while ((p.getRegionIndex() < hiddenColumns.size())
+              && hiddenColumns.get(p.getRegionIndex())[1] < column)
+      {
+        p = stepForward(p);
+      }
+    }
+    return p;
+  }
+
+  /**
+   * Move to the next (rightwards) hidden region after a given cursor position
+   * 
+   * @param p
+   *          current position of cursor
+   * @return new position of cursor at next region
+   */
+  private HiddenCursorPosition stepForward(HiddenCursorPosition p)
+  {
+    int[] region = hiddenColumns.get(p.getRegionIndex());
+
+    // increment the index, and add this region's hidden columns to the hidden
+    // column count
+    return new HiddenCursorPosition(p.getRegionIndex() + 1,
+            p.getHiddenSoFar() + region[1] - region[0] + 1);
+  }
+
+  /**
+   * Search backwards through the hidden columns collection to find the hidden
+   * region immediately before (left of) a given column
+   * 
+   * @param pos
+   *          current position
+   * @param column
+   *          column to locate
+   * @param useVisible
+   *          whether using visible or absolute coordinates
+   * @return position of region immediately to left of column
+   */
+  private HiddenCursorPosition searchBackward(HiddenCursorPosition p,
+          int column, boolean useVisible)
+  {
+    int i = p.getRegionIndex();
+    int h = p.getHiddenSoFar();
+
+    // used to add in hiddenColumns offset when working with visible columns
+    int offset = (useVisible ? 1 : 0);
+
+    while ((i > 0) && (hiddenColumns.get(i - 1)[1] >= column + offset * h))
+    {
+      i--;
+      int[] region = hiddenColumns.get(i);
+      h -= region[1] - region[0] + 1;
+    }
+    return new HiddenCursorPosition(i, h);
+  }
+
+}
diff --git a/src/jalview/datamodel/HiddenCursorPosition.java b/src/jalview/datamodel/HiddenCursorPosition.java
new file mode 100644 (file)
index 0000000..bdca6d0
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+public final class HiddenCursorPosition
+{
+  // index of last visited region
+  private final int regionIndex;
+
+  // number of hidden columns before last visited region
+  private final int hiddenSoFar;
+
+  public HiddenCursorPosition(int index, int hiddencount)
+  {
+    regionIndex = index;
+    hiddenSoFar = hiddencount;
+  }
+
+  public int getRegionIndex()
+  {
+    return regionIndex;
+  }
+
+  public int getHiddenSoFar()
+  {
+    return hiddenSoFar;
+  }
+}
index 328b96a..b5184fb 100644 (file)
@@ -693,19 +693,6 @@ public class Mapping
     to = tto;
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see java.lang.Object#finalize()
-   */
-  @Override
-  protected void finalize() throws Throwable
-  {
-    map = null;
-    to = null;
-    super.finalize();
-  }
-
   /**
    * Returns an iterator which can serve up the aligned codon column positions
    * and their corresponding peptide products
index 7886713..8b6f617 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel;
 
 /**
diff --git a/src/jalview/datamodel/RangeElementsIterator.java b/src/jalview/datamodel/RangeElementsIterator.java
new file mode 100644 (file)
index 0000000..9ca6b2a
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Iterator over each element in a set of ranges i.e. if ranges is {[3,6],
+ * [12,15]} it will iterate over {3,4,5,6,12,13,14,15}. Uses a local copy of the
+ * set of ranges.
+ * 
+ * @author kmourao
+ *
+ */
+public class RangeElementsIterator implements Iterator<Integer>
+{
+  private int last;
+
+  private int current;
+
+  private int next;
+
+  private Iterator<int[]> rangeIterator;
+
+  private int[] nextRange = null;
+
+  RangeElementsIterator(Iterator<int[]> it)
+  {
+    rangeIterator = it;
+    if (rangeIterator.hasNext())
+    {
+      nextRange = rangeIterator.next();
+      next = nextRange[0];
+      last = nextRange[1];
+    }
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return rangeIterator.hasNext() || next <= last;
+  }
+
+  @Override
+  public Integer next()
+  {
+    if (!hasNext())
+    {
+      throw new NoSuchElementException();
+    }
+
+    current = next;
+
+    // recalculate next
+    next++;
+
+    // if there are more ranges need to check if next is in a range
+    checkNextRange();
+    return current;
+  }
+
+  /**
+   * Check how next position relates to next range, and update next position if
+   * necessary
+   */
+  private void checkNextRange()
+  {
+    if (nextRange != null && next > nextRange[1])
+    {
+      if (rangeIterator.hasNext())
+      {
+        nextRange = rangeIterator.next();
+        next = nextRange[0];
+        last = nextRange[1];
+      }
+      else
+      {
+        nextRange = null;
+      }
+
+    }
+  }
+
+  @Override
+  public void remove()
+  {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/src/jalview/datamodel/RangeIterator.java b/src/jalview/datamodel/RangeIterator.java
new file mode 100644 (file)
index 0000000..5d45236
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An iterator which iterates over a list of ranges. Works with a copy of the
+ * collection of ranges.
+ */
+public class RangeIterator implements Iterator<int[]>
+{
+  // current index in rangeList
+  private int currentPosition = 0;
+
+  // current range in rangeList
+  private int[] currentRange;
+
+  // local copy or reference to rangeList
+  private List<int[]> localRanges;
+
+  /**
+   * Unbounded constructor
+   * 
+   * @param rangeList
+   *          list of ranges to iterate over
+   */
+  RangeIterator(List<int[]> rangeList)
+  {
+    if (!rangeList.isEmpty())
+    {
+      int last = rangeList.get(rangeList.size() - 1)[1];
+      init(0, last, rangeList);
+    }
+    else
+    {
+      init(0, 0, rangeList);
+    }
+  }
+
+  /**
+   * Construct an iterator over rangeList bounded at [lowerBound,upperBound]
+   * 
+   * @param lowerBound
+   *          lower bound to iterate from
+   * @param upperBound
+   *          upper bound to iterate to
+   * @param rangeList
+   *          list of ranges to iterate over
+   */
+  RangeIterator(int lowerBound, int upperBound,
+          List<int[]> rangeList)
+  {
+    init(lowerBound, upperBound, rangeList);
+  }
+
+  /**
+   * Construct an iterator over rangeList bounded at [lowerBound,upperBound]
+   * 
+   * @param lowerBound
+   *          lower bound to iterate from
+   * @param upperBound
+   *          upper bound to iterate to
+   */
+  private void init(int lowerBound, int upperBound,
+          List<int[]> rangeList)
+  {
+    int start = lowerBound;
+    int end = upperBound;
+
+    if (rangeList != null)
+    {
+      localRanges = new ArrayList<>();
+
+      // iterate until a range overlaps with [start,end]
+      int i = 0;
+      while ((i < rangeList.size()) && (rangeList.get(i)[1] < start))
+      {
+        i++;
+      }
+
+      // iterate from start to end, adding each range. Positions are
+      // absolute, and all ranges which *overlap* [start,end] are added.
+      while (i < rangeList.size() && (rangeList.get(i)[0] <= end))
+      {
+        int[] rh = rangeList.get(i);
+        int[] cp = new int[2];
+        System.arraycopy(rh, 0, cp, 0, rh.length);
+        localRanges.add(cp);
+        i++;
+      }
+    }
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return (localRanges != null) && (currentPosition < localRanges.size());
+  }
+
+  @Override
+  public int[] next()
+  {
+    currentRange = localRanges.get(currentPosition);
+    currentPosition++;
+    return currentRange;
+  }
+
+  @Override
+  public void remove()
+  {
+    localRanges.remove(--currentPosition);
+  }
+}
index cde50e5..d1e3fcc 100755 (executable)
@@ -267,9 +267,12 @@ public class SearchResults implements SearchResultsI
   {
     int count = 0;
     BitSet mask = new BitSet();
+    int startRes = sqcol.getStartRes();
+    int endRes = sqcol.getEndRes();
+
     for (SequenceI s : sqcol.getSequences())
     {
-      int[] cols = getResults(s, sqcol.getStartRes(), sqcol.getEndRes());
+      int[] cols = getResults(s, startRes, endRes);
       if (cols != null)
       {
         for (int pair = 0; pair < cols.length; pair += 2)
index 2f1da7f..33de452 100755 (executable)
@@ -34,12 +34,11 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.Enumeration;
+import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Vector;
 
-import com.stevesoft.pat.Regex;
-
 import fr.orsay.lri.varna.models.rna.RNA;
 
 /**
@@ -51,11 +50,6 @@ import fr.orsay.lri.varna.models.rna.RNA;
  */
 public class Sequence extends ASequence implements SequenceI
 {
-  private static final Regex limitrx = new Regex(
-          "[/][0-9]{1,}[-][0-9]{1,}$");
-
-  private static final Regex endrx = new Regex("[0-9]{1,}$");
-
   SequenceI datasetSequence;
 
   String name;
@@ -84,12 +78,7 @@ public class Sequence extends ASequence implements SequenceI
    */
   Vector<AlignmentAnnotation> annotation;
 
-  /**
-   * The index of the sequence in a MSA
-   */
-  int index = -1;
-
-  private SequenceFeatures sequenceFeatureStore;
+  private SequenceFeaturesI sequenceFeatureStore;
 
   /*
    * A cursor holding the approximate current view position to the sequence,
@@ -151,6 +140,10 @@ public class Sequence extends ASequence implements SequenceI
     checkValidRange();
   }
 
+  /**
+   * If 'name' ends in /i-j, where i >= j > 0 are integers, extracts i and j as
+   * start and end respectively and removes the suffix from the name
+   */
   void parseId()
   {
     if (name == null)
@@ -159,17 +152,37 @@ public class Sequence extends ASequence implements SequenceI
               "POSSIBLE IMPLEMENTATION ERROR: null sequence name passed to constructor.");
       name = "";
     }
-    // Does sequence have the /start-end signature?
-    if (limitrx.search(name))
+    int slashPos = name.lastIndexOf('/');
+    if (slashPos > -1 && slashPos < name.length() - 1)
     {
-      name = limitrx.left();
-      endrx.search(limitrx.stringMatched());
-      setStart(Integer.parseInt(limitrx.stringMatched().substring(1,
-              endrx.matchedFrom() - 1)));
-      setEnd(Integer.parseInt(endrx.stringMatched()));
+      String suffix = name.substring(slashPos + 1);
+      String[] range = suffix.split("-");
+      if (range.length == 2)
+      {
+        try
+        {
+          int from = Integer.valueOf(range[0]);
+          int to = Integer.valueOf(range[1]);
+          if (from > 0 && to >= from)
+          {
+            name = name.substring(0, slashPos);
+            setStart(from);
+            setEnd(to);
+            checkValidRange();
+          }
+        } catch (NumberFormatException e)
+        {
+          // leave name unchanged if suffix is invalid
+        }
+      }
     }
   }
 
+  /**
+   * Ensures that 'end' is not before the end of the sequence, that is,
+   * (end-start+1) is at least as long as the count of ungapped positions. Note
+   * that end is permitted to be beyond the end of the sequence data.
+   */
   void checkValidRange()
   {
     // Note: JAL-774 :
@@ -178,7 +191,7 @@ public class Sequence extends ASequence implements SequenceI
       int endRes = 0;
       for (int j = 0; j < sequence.length; j++)
       {
-        if (!jalview.util.Comparison.isGap(sequence[j]))
+        if (!Comparison.isGap(sequence[j]))
         {
           endRes++;
         }
@@ -396,7 +409,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     if (pdbIds == null)
     {
-      pdbIds = new Vector<PDBEntry>();
+      pdbIds = new Vector<>();
       pdbIds.add(entry);
       return true;
     }
@@ -432,7 +445,7 @@ public class Sequence extends ASequence implements SequenceI
   @Override
   public Vector<PDBEntry> getAllPDBEntries()
   {
-    return pdbIds == null ? new Vector<PDBEntry>() : pdbIds;
+    return pdbIds == null ? new Vector<>() : pdbIds;
   }
 
   /**
@@ -453,15 +466,15 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
-   * DOCUMENT ME!
+   * Sets the sequence name. If the name ends in /start-end, then the start-end
+   * values are parsed out and set, and the suffix is removed from the name.
    * 
-   * @param name
-   *          DOCUMENT ME!
+   * @param theName
    */
   @Override
-  public void setName(String name)
+  public void setName(String theName)
   {
-    this.name = name;
+    this.name = theName;
     this.parseId();
   }
 
@@ -645,10 +658,10 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   /**
-   * DOCUMENT ME!
+   * Sets the sequence description, and also parses out any special formats of
+   * interest
    * 
    * @param desc
-   *          DOCUMENT ME!
    */
   @Override
   public void setDescription(String desc)
@@ -656,10 +669,67 @@ public class Sequence extends ASequence implements SequenceI
     this.description = desc;
   }
 
+  @Override
+  public void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map)
+  {
+    addDBRef(new DBRefEntry(speciesId, assemblyId, DBRefEntry.CHROMOSOME
+            + ":" + chromosomeId, new Mapping(map)));
+  }
+
   /**
-   * DOCUMENT ME!
+   * Returns the gene loci mapping for the sequence (may be null)
    * 
-   * @return DOCUMENT ME!
+   * @return
+   */
+  @Override
+  public GeneLociI getGeneLoci()
+  {
+    DBRefEntry[] refs = getDBRefs();
+    if (refs != null)
+    {
+      for (final DBRefEntry ref : refs)
+      {
+        if (ref.isChromosome())
+        {
+          return new GeneLociI()
+          {
+            @Override
+            public String getSpeciesId()
+            {
+              return ref.getSource();
+            }
+
+            @Override
+            public String getAssemblyId()
+            {
+              return ref.getVersion();
+            }
+
+            @Override
+            public String getChromosomeId()
+            {
+              // strip off "chromosome:" prefix to chrId
+              return ref.getAccessionId().substring(
+                      DBRefEntry.CHROMOSOME.length() + 1);
+            }
+
+            @Override
+            public MapList getMap()
+            {
+              return ref.getMap().getMap();
+            }
+          };
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Answers the description
+   * 
+   * @return
    */
   @Override
   public String getDescription()
@@ -745,7 +815,7 @@ public class Sequence extends ASequence implements SequenceI
    * @param curs
    * @return
    */
-  protected int findIndex(int pos, SequenceCursor curs)
+  protected int findIndex(final int pos, SequenceCursor curs)
   {
     if (!isValidCursor(curs))
     {
@@ -771,8 +841,13 @@ public class Sequence extends ASequence implements SequenceI
     while (newPos != pos)
     {
       col += delta; // shift one column left or right
-      if (col < 0 || col == sequence.length)
+      if (col < 0)
+      {
+        break;
+      }
+      if (col == sequence.length)
       {
+        col--; // return last column if we failed to reach pos
         break;
       }
       if (!Comparison.isGap(sequence[col]))
@@ -782,7 +857,14 @@ public class Sequence extends ASequence implements SequenceI
     }
 
     col++; // convert back to base 1
-    updateCursor(pos, col, curs.firstColumnPosition);
+
+    /*
+     * only update cursor if we found the target position
+     */
+    if (newPos == pos)
+    {
+      updateCursor(pos, col, curs.firstColumnPosition);
+    }
 
     return col;
   }
@@ -1059,6 +1141,27 @@ public class Sequence extends ASequence implements SequenceI
     return map;
   }
 
+  /**
+   * Build a bitset corresponding to sequence gaps
+   * 
+   * @return a BitSet where set values correspond to gaps in the sequence
+   */
+  @Override
+  public BitSet gapBitset()
+  {
+    BitSet gaps = new BitSet(sequence.length);
+    int j = 0;
+    while (j < sequence.length)
+    {
+      if (jalview.util.Comparison.isGap(sequence[j]))
+      {
+        gaps.set(j);
+      }
+      j++;
+    }
+    return gaps;
+  }
+
   @Override
   public int[] findPositionMap()
   {
@@ -1082,7 +1185,7 @@ public class Sequence extends ASequence implements SequenceI
   @Override
   public List<int[]> getInsertions()
   {
-    ArrayList<int[]> map = new ArrayList<int[]>();
+    ArrayList<int[]> map = new ArrayList<>();
     int lastj = -1, j = 0;
     int pos = start;
     int seqlen = sequence.length;
@@ -1148,7 +1251,7 @@ public class Sequence extends ASequence implements SequenceI
   }
 
   @Override
-  public void deleteChars(int i, int j)
+  public void deleteChars(final int i, final int j)
   {
     int newstart = start, newend = end;
     if (i >= sequence.length || i < 0)
@@ -1160,62 +1263,75 @@ public class Sequence extends ASequence implements SequenceI
     boolean createNewDs = false;
     // TODO: take a (second look) at the dataset creation validation method for
     // the very large sequence case
-    int eindex = -1, sindex = -1;
-    boolean ecalc = false, scalc = false;
+    int startIndex = findIndex(start) - 1;
+    int endIndex = findIndex(end) - 1;
+    int startDeleteColumn = -1; // for dataset sequence deletions
+    int deleteCount = 0;
+
     for (int s = i; s < j; s++)
     {
-      if (jalview.schemes.ResidueProperties.aaIndex[sequence[s]] != 23)
+      if (Comparison.isGap(sequence[s]))
+      {
+        continue;
+      }
+      deleteCount++;
+      if (startDeleteColumn == -1)
+      {
+        startDeleteColumn = findPosition(s) - start;
+      }
+      if (createNewDs)
       {
-        if (createNewDs)
+        newend--;
+      }
+      else
+      {
+        if (startIndex == s)
         {
-          newend--;
+          /*
+           * deleting characters from start of sequence; new start is the
+           * sequence position of the next column (position to the right
+           * if the column position is gapped)
+           */
+          newstart = findPosition(j);
+          break;
         }
         else
         {
-          if (!scalc)
-          {
-            sindex = findIndex(start) - 1;
-            scalc = true;
-          }
-          if (sindex == s)
+          if (endIndex < j)
           {
-            // delete characters including start of sequence
-            newstart = findPosition(j);
-            break; // don't need to search for any more residue characters.
+            /*
+             * deleting characters at end of sequence; new end is the sequence
+             * position of the column before the deletion; subtract 1 if this is
+             * gapped since findPosition returns the next sequence position
+             */
+            newend = findPosition(i - 1);
+            if (Comparison.isGap(sequence[i - 1]))
+            {
+              newend--;
+            }
+            break;
           }
           else
           {
-            // delete characters after start.
-            if (!ecalc)
-            {
-              eindex = findIndex(end) - 1;
-              ecalc = true;
-            }
-            if (eindex < j)
-            {
-              // delete characters at end of sequence
-              newend = findPosition(i - 1);
-              break; // don't need to search for any more residue characters.
-            }
-            else
-            {
-              createNewDs = true;
-              newend--; // decrease end position by one for the deleted residue
-              // and search further
-            }
+            createNewDs = true;
+            newend--;
           }
         }
       }
     }
-    // deletion occured in the middle of the sequence
+
     if (createNewDs && this.datasetSequence != null)
     {
-      // construct a new sequence
+      /*
+       * if deletion occured in the middle of the sequence,
+       * construct a new dataset sequence and delete the residues
+       * that were deleted from the aligned sequence
+       */
       Sequence ds = new Sequence(datasetSequence);
+      ds.deleteChars(startDeleteColumn, startDeleteColumn + deleteCount);
+      datasetSequence = ds;
       // TODO: remove any non-inheritable properties ?
       // TODO: create a sequence mapping (since there is a relation here ?)
-      ds.deleteChars(i, j);
-      datasetSequence = ds;
     }
     start = newstart;
     end = newend;
@@ -1380,7 +1496,7 @@ public class Sequence extends ASequence implements SequenceI
   {
     if (this.annotation == null)
     {
-      this.annotation = new Vector<AlignmentAnnotation>();
+      this.annotation = new Vector<>();
     }
     if (!this.annotation.contains(annotation))
     {
@@ -1547,7 +1663,7 @@ public class Sequence extends ASequence implements SequenceI
       return null;
     }
 
-    Vector<AlignmentAnnotation> subset = new Vector<AlignmentAnnotation>();
+    Vector<AlignmentAnnotation> subset = new Vector<>();
     Enumeration<AlignmentAnnotation> e = annotation.elements();
     while (e.hasMoreElements())
     {
@@ -1665,30 +1781,6 @@ public class Sequence extends ASequence implements SequenceI
     }
   }
 
-  /**
-   * @return The index (zero-based) on this sequence in the MSA. It returns
-   *         {@code -1} if this information is not available.
-   */
-  @Override
-  public int getIndex()
-  {
-    return index;
-  }
-
-  /**
-   * Defines the position of this sequence in the MSA. Use the value {@code -1}
-   * if this information is undefined.
-   * 
-   * @param The
-   *          position for this sequence. This value is zero-based (zero for
-   *          this first sequence)
-   */
-  @Override
-  public void setIndex(int value)
-  {
-    index = value;
-  }
-
   @Override
   public void setRNA(RNA r)
   {
@@ -1705,7 +1797,7 @@ public class Sequence extends ASequence implements SequenceI
   public List<AlignmentAnnotation> getAlignmentAnnotations(String calcId,
           String label)
   {
-    List<AlignmentAnnotation> result = new ArrayList<AlignmentAnnotation>();
+    List<AlignmentAnnotation> result = new ArrayList<>();
     if (this.annotation != null)
     {
       for (AlignmentAnnotation ann : annotation)
@@ -1761,7 +1853,7 @@ public class Sequence extends ASequence implements SequenceI
     }
     synchronized (dbrefs)
     {
-      List<DBRefEntry> primaries = new ArrayList<DBRefEntry>();
+      List<DBRefEntry> primaries = new ArrayList<>();
       DBRefEntry[] tmp = new DBRefEntry[1];
       for (DBRefEntry ref : dbrefs)
       {
@@ -1827,7 +1919,9 @@ public class Sequence extends ASequence implements SequenceI
      * and we may have included adjacent or enclosing features;
      * remove any that are not enclosing, non-contact features
      */
-    if (endPos > this.end || Comparison.isGap(sequence[toColumn - 1]))
+    boolean endColumnIsGapped = toColumn > 0 && toColumn <= sequence.length
+            && Comparison.isGap(sequence[toColumn - 1]);
+    if (endPos > this.end || endColumnIsGapped)
     {
       ListIterator<SequenceFeature> it = result.listIterator();
       while (it.hasNext())
@@ -1901,4 +1995,73 @@ public class Sequence extends ASequence implements SequenceI
 
     return count;
   }
+
+  @Override
+  public String getSequenceStringFromIterator(Iterator<int[]> it)
+  {
+    StringBuilder newSequence = new StringBuilder();
+    while (it.hasNext())
+    {
+      int[] block = it.next();
+      if (it.hasNext())
+      {
+        newSequence.append(getSequence(block[0], block[1] + 1));
+      }
+      else
+      {
+        newSequence.append(getSequence(block[0], block[1]));
+      }
+    }
+
+    return newSequence.toString();
+  }
+
+  @Override
+  public int firstResidueOutsideIterator(Iterator<int[]> regions)
+  {
+    int start = 0;
+
+    if (!regions.hasNext())
+    {
+      return findIndex(getStart()) - 1;
+    }
+
+    // Simply walk along the sequence whilst watching for region
+    // boundaries
+    int hideStart = getLength();
+    int hideEnd = -1;
+    boolean foundStart = false;
+
+    // step through the non-gapped positions of the sequence
+    for (int i = getStart(); i <= getEnd() && (!foundStart); i++)
+    {
+      // get alignment position of this residue in the sequence
+      int p = findIndex(i) - 1;
+
+      // update region start/end
+      while (hideEnd < p && regions.hasNext())
+      {
+        int[] region = regions.next();
+        hideStart = region[0];
+        hideEnd = region[1];
+      }
+      if (hideEnd < p)
+      {
+        hideStart = getLength();
+      }
+      // update boundary for sequence
+      if (p < hideStart)
+      {
+        start = p;
+        foundStart = true;
+      }
+    }
+
+    if (foundStart)
+    {
+      return start;
+    }
+    // otherwise, sequence was completely hidden
+    return 0;
+  }
 }
index b5929bf..24752bf 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel;
 
 /**
index 9c4087e..34565c6 100755 (executable)
  */
 package jalview.datamodel;
 
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureAttributes;
 import jalview.datamodel.features.FeatureLocationI;
+import jalview.datamodel.features.FeatureSourceI;
+import jalview.datamodel.features.FeatureSources;
+import jalview.util.StringUtils;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.Vector;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * A class that models a single contiguous feature on a sequence. If flag
+ * 'contactFeature' is true, the start and end positions are interpreted instead
+ * as two contact points.
  */
 public class SequenceFeature implements FeatureLocationI
 {
@@ -51,6 +57,8 @@ public class SequenceFeature implements FeatureLocationI
   // private key for ENA location designed not to conflict with real GFF data
   private static final String LOCATION = "!Location";
 
+  private static final String ROW_DATA = "<tr><td>%s</td><td>%s</td><td>%s</td></tr>";
+
   /*
    * ATTRIBUTES is reserved for the GFF 'column 9' data, formatted as
    * name1=value1;name2=value2,value3;...etc
@@ -84,6 +92,12 @@ public class SequenceFeature implements FeatureLocationI
 
   public Vector<String> links;
 
+  /*
+   * the identifier (if known) for the FeatureSource held in FeatureSources,
+   * as a provider of metadata about feature attributes 
+   */
+  private String source;
+
   /**
    * Constructs a duplicate feature. Note: Uses makes a shallow copy of the
    * otherDetails map, so the new and original SequenceFeature may reference the
@@ -155,9 +169,11 @@ public class SequenceFeature implements FeatureLocationI
     this(newType, sf.getDescription(), newBegin, newEnd, newScore,
             newGroup);
 
+    this.source = sf.source;
+
     if (sf.otherDetails != null)
     {
-      otherDetails = new HashMap<String, Object>();
+      otherDetails = new HashMap<>();
       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
       {
         otherDetails.put(entry.getKey(), entry.getValue());
@@ -165,7 +181,7 @@ public class SequenceFeature implements FeatureLocationI
     }
     if (sf.links != null && sf.links.size() > 0)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
       {
         links.addElement(sf.links.elementAt(i));
@@ -332,7 +348,7 @@ public class SequenceFeature implements FeatureLocationI
   {
     if (links == null)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
     }
 
     if (!links.contains(labelLink))
@@ -366,6 +382,30 @@ public class SequenceFeature implements FeatureLocationI
   }
 
   /**
+   * Answers the value of the specified attribute as string, or null if no such
+   * value. If more than one attribute name is provided, tries to resolve as keys
+   * to nested maps. For example, if attribute "CSQ" holds a map of key-value
+   * pairs, then getValueAsString("CSQ", "Allele") returns the value of "Allele"
+   * in that map.
+   * 
+   * @param key
+   * @return
+   */
+  public String getValueAsString(String... key)
+  {
+    if (otherDetails == null)
+    {
+      return null;
+    }
+    Object value = otherDetails.get(key[0]);
+    if (key.length > 1 && value instanceof Map<?, ?>)
+    {
+      value = ((Map) value).get(key[1]);
+    }
+    return value == null ? null : value.toString();
+  }
+
+  /**
    * Returns a property value for the given key if known, else the specified
    * default value
    * 
@@ -394,13 +434,35 @@ public class SequenceFeature implements FeatureLocationI
     {
       if (otherDetails == null)
       {
-        otherDetails = new HashMap<String, Object>();
+        otherDetails = new HashMap<>();
       }
 
       otherDetails.put(key, value);
+      recordAttribute(key, value);
     }
   }
 
+  /**
+   * Notifies the addition of a feature attribute. This lets us keep track of
+   * which attributes are present on each feature type, and also the range of
+   * numerical-valued attributes.
+   * 
+   * @param key
+   * @param value
+   */
+  protected void recordAttribute(String key, Object value)
+  {
+    String attDesc = null;
+    if (source != null)
+    {
+      attDesc = FeatureSources.getInstance().getSource(source)
+              .getAttributeName(key);
+    }
+
+    FeatureAttributes.getInstance().addAttribute(this.type, attDesc, value,
+            key);
+  }
+
   /*
    * The following methods are added to maintain the castor Uniprot mapping file
    * for the moment.
@@ -535,4 +597,142 @@ public class SequenceFeature implements FeatureLocationI
   {
     return begin == 0 && end == 0;
   }
+
+  /**
+   * Answers an html-formatted report of feature details
+   * 
+   * @return
+   */
+  public String getDetailsReport()
+  {
+    FeatureSourceI metadata = FeatureSources.getInstance()
+            .getSource(source);
+
+    StringBuilder sb = new StringBuilder(128);
+    sb.append("<br>");
+    sb.append("<table>");
+    sb.append(String.format(ROW_DATA, "Type", type, ""));
+    sb.append(String.format(ROW_DATA, "Start/end", begin == end ? begin
+            : begin + (isContactFeature() ? ":" : "-") + end, ""));
+    String desc = StringUtils.stripHtmlTags(description);
+    sb.append(String.format(ROW_DATA, "Description", desc, ""));
+    if (!Float.isNaN(score) && score != 0f)
+    {
+      sb.append(String.format(ROW_DATA, "Score", score, ""));
+    }
+    if (featureGroup != null)
+    {
+      sb.append(String.format(ROW_DATA, "Group", featureGroup, ""));
+    }
+
+    if (otherDetails != null)
+    {
+      TreeMap<String, Object> ordered = new TreeMap<>(
+              String.CASE_INSENSITIVE_ORDER);
+      ordered.putAll(otherDetails);
+
+      for (Entry<String, Object> entry : ordered.entrySet())
+      {
+        String key = entry.getKey();
+        if (ATTRIBUTES.equals(key))
+        {
+          continue; // to avoid double reporting
+        }
+
+        Object value = entry.getValue();
+        if (value instanceof Map<?, ?>)
+        {
+          /*
+           * expand values in a Map attribute across separate lines
+           * copy to a TreeMap for alphabetical ordering
+           */
+          Map<String, Object> values = (Map<String, Object>) value;
+          SortedMap<String, Object> sm = new TreeMap<>(
+                  String.CASE_INSENSITIVE_ORDER);
+          sm.putAll(values);
+          for (Entry<?, ?> e : sm.entrySet())
+          {
+            sb.append(String.format(ROW_DATA, key, e.getKey().toString(), e
+                    .getValue().toString()));
+          }
+        }
+        else
+        {
+          // tried <td title="key"> but it failed to provide a tooltip :-(
+          String attDesc = null;
+          if (metadata != null)
+          {
+            attDesc = metadata.getAttributeName(key);
+          }
+          String s = entry.getValue().toString();
+          if (isValueInteresting(key, s, metadata))
+          {
+            sb.append(String.format(ROW_DATA, key, attDesc == null ? ""
+                    : attDesc, s));
+          }
+        }
+      }
+    }
+    sb.append("</table>");
+
+    String text = sb.toString();
+    return text;
+  }
+
+  /**
+   * Answers true if we judge the value is worth displaying, by some heuristic
+   * rules, else false
+   * 
+   * @param key
+   * @param value
+   * @param metadata
+   * @return
+   */
+  boolean isValueInteresting(String key, String value,
+          FeatureSourceI metadata)
+  {
+    /*
+     * currently suppressing zero values as well as null or empty
+     */
+    if (value == null || "".equals(value) || ".".equals(value)
+            || "0".equals(value))
+    {
+      return false;
+    }
+
+    if (metadata == null)
+    {
+      return true;
+    }
+
+    FeatureAttributeType attType = metadata.getAttributeType(key);
+    if (attType != null
+            && (attType == FeatureAttributeType.Float || attType
+                    .equals(FeatureAttributeType.Integer)))
+    {
+      try
+      {
+        float fval = Float.valueOf(value);
+        if (fval == 0f)
+        {
+          return false;
+        }
+      } catch (NumberFormatException e)
+      {
+        // ignore
+      }
+    }
+
+    return true; // default to interesting
+  }
+
+  /**
+   * Sets the feature source identifier
+   * 
+   * @param theSource
+   */
+  public void setSource(String theSource)
+  {
+    source = theSource;
+  }
 }
index e2f15e1..944f263 100755 (executable)
@@ -30,6 +30,7 @@ import java.awt.Color;
 import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
@@ -1188,9 +1189,10 @@ public class SequenceGroup implements AnnotatedCollectionI
     {
       if (consensus.annotations[i] != null)
       {
-        if (consensus.annotations[i].description.charAt(0) == '[')
+        String desc = consensus.annotations[i].description;
+        if (desc.length() > 1 && desc.charAt(0) == '[')
         {
-          seqs.append(consensus.annotations[i].description.charAt(1));
+          seqs.append(desc.charAt(1));
         }
         else
         {
@@ -1316,39 +1318,16 @@ public class SequenceGroup implements AnnotatedCollectionI
   @Override
   public Iterable<AlignmentAnnotation> findAnnotation(String calcId)
   {
-    List<AlignmentAnnotation> aa = new ArrayList<>();
-    if (calcId == null)
-    {
-      return aa;
-    }
-    for (AlignmentAnnotation a : getAlignmentAnnotation())
-    {
-      if (calcId.equals(a.getCalcId()))
-      {
-        aa.add(a);
-      }
-    }
-    return aa;
+    return AlignmentAnnotation.findAnnotation(
+            Arrays.asList(getAlignmentAnnotation()), calcId);
   }
 
   @Override
   public Iterable<AlignmentAnnotation> findAnnotations(SequenceI seq,
           String calcId, String label)
   {
-    ArrayList<AlignmentAnnotation> aa = new ArrayList<>();
-    for (AlignmentAnnotation ann : getAlignmentAnnotation())
-    {
-      if ((calcId == null || (ann.getCalcId() != null
-              && ann.getCalcId().equals(calcId)))
-              && (seq == null || (ann.sequenceRef != null
-                      && ann.sequenceRef == seq))
-              && (label == null
-                      || (ann.label != null && ann.label.equals(label))))
-      {
-        aa.add(ann);
-      }
-    }
-    return aa;
+    return AlignmentAnnotation.findAnnotations(
+            Arrays.asList(getAlignmentAnnotation()), seq, calcId, label);
   }
 
   /**
@@ -1359,17 +1338,8 @@ public class SequenceGroup implements AnnotatedCollectionI
    */
   public boolean hasAnnotation(String calcId)
   {
-    if (calcId != null && !"".equals(calcId))
-    {
-      for (AlignmentAnnotation a : getAlignmentAnnotation())
-      {
-        if (a.getCalcId() == calcId)
-        {
-          return true;
-        }
-      }
-    }
-    return false;
+    return AlignmentAnnotation
+            .hasAnnotation(Arrays.asList(getAlignmentAnnotation()), calcId);
   }
 
   /**
index 6e6d1aa..8dce31e 100755 (executable)
 package jalview.datamodel;
 
 import jalview.datamodel.features.SequenceFeaturesI;
+import jalview.util.MapList;
 
 import java.util.BitSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Vector;
 
@@ -192,13 +194,14 @@ public interface SequenceI extends ASequenceI
   public int findIndex(int pos);
 
   /**
-   * Returns the sequence position for an alignment position.
+   * Returns the sequence position for an alignment (column) position. If at a
+   * gap, returns the position of the next residue to the right. If beyond the
+   * end of the sequence, returns 1 more than the last residue position.
    * 
    * @param i
    *          column index in alignment (from 0..<length)
    * 
-   * @return TODO: JAL-2562 - residue number for residue (left of and) nearest
-   *         ith column
+   * @return
    */
   public int findPosition(int i);
 
@@ -222,6 +225,13 @@ public interface SequenceI extends ASequenceI
   public int[] gapMap();
 
   /**
+   * Build a bitset corresponding to sequence gaps
+   * 
+   * @return a BitSet where set values correspond to gaps in the sequence
+   */
+  public BitSet gapBitset();
+
+  /**
    * Returns an int array where indices correspond to each position in sequence
    * char array and the element value gives the result of findPosition for that
    * index in the sequence.
@@ -449,17 +459,6 @@ public interface SequenceI extends ASequenceI
   public void transferAnnotation(SequenceI entry, Mapping mp);
 
   /**
-   * @param index
-   *          The sequence index in the MSA
-   */
-  public void setIndex(int index);
-
-  /**
-   * @return The index of the sequence in the alignment
-   */
-  public int getIndex();
-
-  /**
    * @return The RNA of the sequence in the alignment
    */
 
@@ -534,4 +533,44 @@ public interface SequenceI extends ASequenceI
    * @param c2
    */
   public int replace(char c1, char c2);
+
+  /**
+   * Answers the GeneLociI, or null if not known
+   * 
+   * @return
+   */
+  GeneLociI getGeneLoci();
+
+  /**
+   * Sets the mapping to gene loci for the sequence
+   * 
+   * @param speciesId
+   * @param assemblyId
+   * @param chromosomeId
+   * @param map
+   */
+  void setGeneLoci(String speciesId, String assemblyId,
+          String chromosomeId, MapList map);
+
+
+  /**
+   * Returns the sequence string constructed from the substrings of a sequence
+   * defined by the int[] ranges provided by an iterator. E.g. the iterator
+   * could iterate over all visible regions of the alignment
+   * 
+   * @param it
+   *          the iterator to use
+   * @return a String corresponding to the sequence
+   */
+  public String getSequenceStringFromIterator(Iterator<int[]> it);
+
+  /**
+   * Locate the first position in this sequence which is not contained in an
+   * iterator region. If no such position exists, return 0
+   * 
+   * @param it
+   *          iterator over regions
+   * @return first residue not contained in regions
+   */
+  public int firstResidueOutsideIterator(Iterator<int[]> it);
 }
diff --git a/src/jalview/datamodel/StartRegionIterator.java b/src/jalview/datamodel/StartRegionIterator.java
new file mode 100644 (file)
index 0000000..c21f04a
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An iterator which iterates over visible start positions of hidden column
+ * regions in a range.
+ */
+public class StartRegionIterator implements Iterator<Integer>
+{
+  // start position to iterate from
+  private int start;
+
+  // end position to iterate to
+  private int end;
+
+  // current index in hiddenColumns
+  private int currentPosition = 0;
+
+  // local copy or reference to hiddenColumns
+  private List<Integer> positions = null;
+
+  /**
+   * Construct an iterator over hiddenColums bounded at [lowerBound,upperBound]
+   * 
+   * @param lowerBound
+   *          lower bound to iterate from
+   * @param upperBound
+   *          upper bound to iterate to
+   * @param useCopyCols
+   *          whether to make a local copy of hiddenColumns for iteration (set
+   *          to true if calling from outwith the HiddenColumns class)
+   */
+  StartRegionIterator(int lowerBound, int upperBound,
+          List<int[]> hiddenColumns)
+  {
+    this(null, lowerBound, upperBound, hiddenColumns);
+  }
+
+  /**
+   * Construct an iterator over hiddenColums bounded at [lowerBound,upperBound]
+   * 
+   * @param pos
+   *          a hidden cursor position to start from - may be null
+   * @param lowerBound
+   *          lower bound to iterate from - will be ignored if pos != null
+   * @param upperBound
+   *          upper bound to iterate to
+   * @param hiddenColumns
+   *          the hidden columns collection to use
+   */
+  StartRegionIterator(HiddenCursorPosition pos, int lowerBound,
+          int upperBound, List<int[]> hiddenColumns)
+  {
+    start = lowerBound;
+    end = upperBound;
+
+    if (hiddenColumns != null)
+    {
+      positions = new ArrayList<>(hiddenColumns.size());
+
+      // navigate to start, keeping count of hidden columns
+      int i = 0;
+      int hiddenSoFar = 0;
+      
+      if (pos != null)
+      {
+        // use the cursor position provided
+        i = pos.getRegionIndex();
+        hiddenSoFar = pos.getHiddenSoFar();
+      }
+      else
+      {
+        // navigate to start
+        while ((i < hiddenColumns.size())
+                && (hiddenColumns.get(i)[0] < start + hiddenSoFar))
+        {
+          int[] region = hiddenColumns.get(i);
+          hiddenSoFar += region[1] - region[0] + 1;
+          i++;
+        }
+      }
+
+      // iterate from start to end, adding start positions of each
+      // hidden region. Positions are visible columns count, not absolute
+      while (i < hiddenColumns.size()
+              && (hiddenColumns.get(i)[0] <= end + hiddenSoFar))
+      {
+        int[] region = hiddenColumns.get(i);
+        positions.add(region[0] - hiddenSoFar);
+        hiddenSoFar += region[1] - region[0] + 1;
+        i++;
+      }
+    }
+    else
+    {
+      positions = new ArrayList<>();
+    }
+
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return (currentPosition < positions.size());
+  }
+
+  /**
+   * Get next hidden region start position
+   * 
+   * @return the start position in *visible* coordinates
+   */
+  @Override
+  public Integer next()
+  {
+    int result = positions.get(currentPosition);
+    currentPosition++;
+    return result;
+  }
+}
+
index e9437a7..4ca51b5 100644 (file)
@@ -32,17 +32,17 @@ public class VisibleColsCollection implements AlignmentColsCollectionI
 
   HiddenColumns hidden;
 
-  public VisibleColsCollection(int s, int e, AlignmentI al)
+  public VisibleColsCollection(int s, int e, HiddenColumns h)
   {
     start = s;
     end = e;
-    hidden = al.getHiddenColumns();
+    hidden = h;
   }
 
   @Override
   public Iterator<Integer> iterator()
   {
-    return new VisibleColsIterator(start, end, hidden);
+    return hidden.getVisibleColsIterator(start, end);
   }
 
   @Override
diff --git a/src/jalview/datamodel/VisibleColsIterator.java b/src/jalview/datamodel/VisibleColsIterator.java
deleted file mode 100644 (file)
index 9de468d..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.datamodel;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-
-/**
- * An iterator which iterates over all visible columns in an alignment
- * 
- * @author kmourao
- *
- */
-public class VisibleColsIterator implements Iterator<Integer>
-{
-  private int last;
-
-  private int current;
-
-  private int next;
-
-  private List<int[]> hidden;
-
-  private int lasthiddenregion;
-
-  public VisibleColsIterator(int firstcol, int lastcol,
-          HiddenColumns hiddenCols)
-  {
-    last = lastcol;
-    current = firstcol;
-    next = firstcol;
-    hidden = hiddenCols.getHiddenColumnsCopy();
-    lasthiddenregion = -1;
-
-    if (hidden != null)
-    {
-      int i = 0;
-      for (i = 0; i < hidden.size(); ++i)
-      {
-        if (current >= hidden.get(i)[0] && current <= hidden.get(i)[1])
-        {
-          // current is hidden, move to right
-          current = hidden.get(i)[1] + 1;
-          next = current;
-        }
-        if (current < hidden.get(i)[0])
-        {
-          break;
-        }
-      }
-      lasthiddenregion = i - 1;
-
-      for (i = hidden.size() - 1; i >= 0; --i)
-      {
-        if (last >= hidden.get(i)[0] && last <= hidden.get(i)[1])
-        {
-          // last is hidden, move to left
-          last = hidden.get(i)[0] - 1;
-        }
-        if (last > hidden.get(i)[1])
-        {
-          break;
-        }
-      }
-    }
-  }
-
-  @Override
-  public boolean hasNext()
-  {
-    return next <= last;
-  }
-
-  @Override
-  public Integer next()
-  {
-    if (next > last)
-    {
-      throw new NoSuchElementException();
-    }
-    current = next;
-    if ((hidden != null) && (lasthiddenregion + 1 < hidden.size()))
-    {
-      // still some more hidden regions
-      if (next + 1 < hidden.get(lasthiddenregion + 1)[0])
-      {
-        // next+1 is still before the next hidden region
-        next++;
-      }
-      else if ((next + 1 >= hidden.get(lasthiddenregion + 1)[0])
-              && (next + 1 <= hidden.get(lasthiddenregion + 1)[1]))
-      {
-        // next + 1 is in the next hidden region
-        next = hidden.get(lasthiddenregion + 1)[1] + 1;
-        lasthiddenregion++;
-      }
-    }
-    else
-    {
-      // finished with hidden regions, just increment normally
-      next++;
-    }
-    return current;
-  }
-
-  @Override
-  public void remove()
-  {
-    throw new UnsupportedOperationException();
-  }
-}
diff --git a/src/jalview/datamodel/VisibleContigsIterator.java b/src/jalview/datamodel/VisibleContigsIterator.java
new file mode 100644 (file)
index 0000000..0185978
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An iterator which iterates over visible regions in a range. Provides a
+ * special "endsAtHidden" indicator to allow callers to determine if the final
+ * visible column is adjacent to a hidden region.
+ */
+public class VisibleContigsIterator implements Iterator<int[]>
+{
+  private List<int[]> vcontigs = new ArrayList<>();
+
+  private int currentPosition = 0;
+
+  private boolean endsAtHidden = false;
+
+  VisibleContigsIterator(int start, int end,
+          List<int[]> hiddenColumns)
+  {
+    if (hiddenColumns != null && hiddenColumns.size() > 0)
+    {
+      int vstart = start;
+      int hideStart;
+      int hideEnd;
+
+      for (int[] region : hiddenColumns)
+      {
+        endsAtHidden = false;
+        hideStart = region[0];
+        hideEnd = region[1];
+
+        // navigate to start
+        if (hideEnd < vstart)
+        {
+          continue;
+        }
+        if (hideStart > vstart)
+        {
+          if (end - 1 > hideStart - 1)
+          {
+            int[] contig = new int[] { vstart, hideStart - 1 };
+            vcontigs.add(contig);
+            endsAtHidden = true;
+          }
+          else
+          {
+            int[] contig = new int[] { vstart, end - 1 };
+            vcontigs.add(contig);
+          }
+        }
+        vstart = hideEnd + 1;
+
+        // exit if we're past the end
+        if (vstart >= end)
+        {
+          break;
+        }
+      }
+
+      if (vstart < end)
+      {
+        int[] contig = new int[] { vstart, end - 1 };
+        vcontigs.add(contig);
+        endsAtHidden = false;
+      }
+    }
+    else
+    {
+      int[] contig = new int[] { start, end - 1 };
+      vcontigs.add(contig);
+    }
+  }
+
+  @Override
+  public boolean hasNext()
+  {
+    return (currentPosition < vcontigs.size());
+  }
+
+  @Override
+  public int[] next()
+  {
+    int[] result = vcontigs.get(currentPosition);
+    currentPosition++;
+    return result;
+  }
+
+  public boolean endsAtHidden()
+  {
+    return endsAtHidden;
+  }
+}
+
diff --git a/src/jalview/datamodel/features/FeatureAttributeType.java b/src/jalview/datamodel/features/FeatureAttributeType.java
new file mode 100644 (file)
index 0000000..fd3069d
--- /dev/null
@@ -0,0 +1,12 @@
+package jalview.datamodel.features;
+
+/**
+ * A class to model the datatype of feature attributes.
+ * 
+ * @author gmcarstairs
+ *
+ */
+public enum FeatureAttributeType
+{
+  String, Integer, Float, Character, Flag;
+}
diff --git a/src/jalview/datamodel/features/FeatureAttributes.java b/src/jalview/datamodel/features/FeatureAttributes.java
new file mode 100644 (file)
index 0000000..e359b62
--- /dev/null
@@ -0,0 +1,360 @@
+package jalview.datamodel.features;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * A singleton class to hold the set of attributes known for each feature type
+ */
+public class FeatureAttributes
+{
+  public enum Datatype
+  {
+    Character, Number, Mixed
+  }
+
+  private static FeatureAttributes instance = new FeatureAttributes();
+
+  /*
+   * map, by feature type, of a map, by attribute name, of
+   * attribute description and min-max range (if known)
+   */
+  private Map<String, Map<String[], AttributeData>> attributes;
+
+  /*
+   * a case-insensitive comparator so that attributes are ordered e.g.
+   * AC
+   * af
+   * CSQ:AFR_MAF
+   * CSQ:Allele
+   */
+  private Comparator<String[]> comparator = new Comparator<String[]>()
+  {
+    @Override
+    public int compare(String[] o1, String[] o2)
+    {
+      int i = 0;
+      while (i < o1.length || i < o2.length)
+      {
+        if (o2.length <= i)
+        {
+          return o1.length <= i ? 0 : 1;
+        }
+        if (o1.length <= i)
+        {
+          return -1;
+        }
+        int comp = String.CASE_INSENSITIVE_ORDER.compare(o1[i], o2[i]);
+        if (comp != 0)
+        {
+          return comp;
+        }
+        i++;
+      }
+      return 0; // same length and all matched
+    }
+  };
+
+  private class AttributeData
+  {
+    /*
+     * description(s) for this attribute, if known
+     * (different feature source might have differing descriptions)
+     */
+    List<String> description;
+
+    /*
+     * minimum value (of any numeric values recorded)
+     */
+    float min = 0f;
+
+    /*
+     * maximum value (of any numeric values recorded)
+     */
+    float max = 0f;
+
+    /*
+     * flag is set true if any numeric value is detected for this attribute
+     */
+    boolean hasValue = false;
+
+    Datatype type;
+
+    /**
+     * Note one instance of this attribute, recording unique, non-null names,
+     * and the min/max of any numerical values
+     * 
+     * @param desc
+     * @param value
+     */
+    void addInstance(String desc, String value)
+    {
+      addDescription(desc);
+
+      if (value != null)
+      {
+        try
+        {
+          float f = Float.valueOf(value);
+          min = hasValue ? Float.min(min, f) : f;
+          max = hasValue ? Float.max(max, f) : f;
+          hasValue = true;
+          type = (type == null || type == Datatype.Number) ? Datatype.Number
+                  : Datatype.Mixed;
+        } catch (NumberFormatException e)
+        {
+          // not a number, ignore for min-max purposes
+          type = (type == null || type == Datatype.Character)
+                  ? Datatype.Character
+                  : Datatype.Mixed;
+        }
+      }
+    }
+
+    /**
+     * Answers the description of the attribute, if recorded and unique, or null if either no, or more than description is recorded
+     * @return
+     */
+    public String getDescription()
+    {
+      if (description != null && description.size() == 1)
+      {
+        return description.get(0);
+      }
+      return null;
+    }
+
+    public Datatype getType()
+    {
+      return type;
+    }
+
+    /**
+     * Adds the given description to the list of known descriptions (without
+     * duplication)
+     * 
+     * @param desc
+     */
+    public void addDescription(String desc)
+    {
+      if (desc != null)
+      {
+        if (description == null)
+        {
+          description = new ArrayList<>();
+        }
+        if (!description.contains(desc))
+        {
+          description.add(desc);
+        }
+      }
+    }
+  }
+
+  /**
+   * Answers the singleton instance of this class
+   * 
+   * @return
+   */
+  public static FeatureAttributes getInstance()
+  {
+    return instance;
+  }
+
+  private FeatureAttributes()
+  {
+    attributes = new HashMap<>();
+  }
+
+  /**
+   * Answers the attribute names known for the given feature type, in
+   * alphabetical order (not case sensitive), or an empty set if no attributes
+   * are known. An attribute name is typically 'simple' e.g. "AC", but may be
+   * 'compound' e.g. {"CSQ", "Allele"} where a feature has map-valued attributes
+   * 
+   * @param featureType
+   * @return
+   */
+  public List<String[]> getAttributes(String featureType)
+  {
+    if (!attributes.containsKey(featureType))
+    {
+      return Collections.<String[]> emptyList();
+    }
+
+    return new ArrayList<>(attributes.get(featureType).keySet());
+  }
+
+  /**
+   * Answers true if at least one attribute is known for the given feature type,
+   * else false
+   * 
+   * @param featureType
+   * @return
+   */
+  public boolean hasAttributes(String featureType)
+  {
+    if (attributes.containsKey(featureType))
+    {
+      if (!attributes.get(featureType).isEmpty())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Records the given attribute name and description for the given feature
+   * type, and updates the min-max for any numeric value
+   * 
+   * @param featureType
+   * @param description
+   * @param value
+   * @param attName
+   */
+  public void addAttribute(String featureType, String description,
+          Object value, String... attName)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+
+    /*
+     * if attribute value is a map, drill down one more level to
+     * record its sub-fields
+     */
+    if (value instanceof Map<?, ?>)
+    {
+      for (Entry<?, ?> entry : ((Map<?, ?>) value).entrySet())
+      {
+        String[] attNames = new String[attName.length + 1];
+        System.arraycopy(attName, 0, attNames, 0, attName.length);
+        attNames[attName.length] = entry.getKey().toString();
+        addAttribute(featureType, description, entry.getValue(), attNames);
+      }
+      return;
+    }
+
+    String valueAsString = value.toString();
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<>(comparator);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addInstance(description, valueAsString);
+  }
+
+  /**
+   * Answers the description of the given attribute for the given feature type,
+   * if known and unique, else null
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public String getDescription(String featureType, String... attName)
+  {
+    String desc = null;
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null)
+      {
+        desc = attData.getDescription();
+      }
+    }
+    return desc;
+  }
+
+  /**
+   * Answers the [min, max] value range of the given attribute for the given
+   * feature type, if known, else null. Attributes which only have text values
+   * would normally return null, however text values which happen to be numeric
+   * could result in a 'min-max' range.
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public float[] getMinMax(String featureType, String... attName)
+  {
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null && attData.hasValue)
+      {
+        return new float[] { attData.min, attData.max };
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Records the given attribute description for the given feature type
+   * 
+   * @param featureType
+   * @param attName
+   * @param description
+   */
+  public void addDescription(String featureType, String description,
+          String... attName)
+  {
+    if (featureType == null || attName == null)
+    {
+      return;
+    }
+  
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts == null)
+    {
+      atts = new TreeMap<>(comparator);
+      attributes.put(featureType, atts);
+    }
+    AttributeData attData = atts.get(attName);
+    if (attData == null)
+    {
+      attData = new AttributeData();
+      atts.put(attName, attData);
+    }
+    attData.addDescription(description);
+  }
+
+  /**
+   * Answers the datatype of the feature, which is one of Character, Number or
+   * Mixed (or null if not known), as discovered from values recorded.
+   * 
+   * @param featureType
+   * @param attName
+   * @return
+   */
+  public Datatype getDatatype(String featureType, String... attName)
+  {
+    Map<String[], AttributeData> atts = attributes.get(featureType);
+    if (atts != null)
+    {
+      AttributeData attData = atts.get(attName);
+      if (attData != null)
+      {
+        return attData.getType();
+      }
+    }
+    return null;
+  }
+}
index e651c13..378b8db 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..f844141
--- /dev/null
@@ -0,0 +1,417 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+import jalview.util.matcher.Matcher;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An immutable class that models one or more match conditions, each of which is
+ * applied to the value obtained by lookup given the match key.
+ * <p>
+ * For example, the value provider could be a SequenceFeature's attributes map,
+ * and the conditions might be
+ * <ul>
+ * <li>CSQ contains "pathological"</li>
+ * <li>AND</li>
+ * <li>AF <= 1.0e-5</li>
+ * </ul>
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureMatcher implements FeatureMatcherI
+{
+  private static final String SCORE = "Score";
+
+  private static final String LABEL = "Label";
+
+  private static final String SPACE = " ";
+
+  private static final String QUOTE = "'";
+
+  /*
+   * a dummy matcher that comes in useful for the 'add a filter' gui row
+   */
+  public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
+          .byLabel(Condition.values()[0], "");
+
+  private static final String COLON = ":";
+
+  /*
+   * if true, match is against feature description
+   */
+  final private boolean byLabel;
+
+  /*
+   * if true, match is against feature score
+   */
+  final private boolean byScore;
+
+  /*
+   * if not null, match is against feature attribute [sub-attribute]
+   */
+  final private String[] key;
+
+  final private MatcherI matcher;
+
+  /**
+   * A helper method that converts a 'compound' attribute name from its display
+   * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
+   * 
+   * @param attribute
+   * @return
+   */
+  public static String[] fromAttributeDisplayName(String attribute)
+  {
+    return attribute == null ? null : attribute.split(COLON);
+  }
+
+  /**
+   * A helper method that converts a 'compound' attribute name to its display
+   * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
+   * 
+   * @param attName
+   * @return
+   */
+  public static String toAttributeDisplayName(String[] attName)
+  {
+    return attName == null ? "" : String.join(COLON, attName);
+  }
+
+  /**
+   * A factory constructor that converts a stringified object (as output by
+   * toStableString) to an object instance. Returns null if parsing fails.
+   * <p>
+   * Leniency in parsing (for manually created feature files):
+   * <ul>
+   * <li>keywords Score and Label, and the condition, are not
+   * case-sensitive</li>
+   * <li>quotes around value and pattern are optional if string does not include
+   * a space</li>
+   * </ul>
+   * 
+   * @param descriptor
+   * @return
+   */
+  public static FeatureMatcher fromString(final String descriptor)
+  {
+    String invalidFormat = "Invalid matcher format: " + descriptor;
+
+    /*
+     * expect 
+     * value condition pattern
+     * where value is Label or Space or attributeName or attName1:attName2
+     * and pattern is a float value as string, or a text string
+     * attribute names or patterns may be quoted (must be if include space)
+     */
+    String attName = null;
+    boolean byScore = false;
+    boolean byLabel = false;
+    Condition cond = null;
+    String pattern = null;
+
+    /*
+     * parse first field (Label / Score / attribute)
+     * optionally in quotes (required if attName includes space)
+     */
+    String leftToParse = descriptor;
+    String firstField = null;
+
+    if (descriptor.startsWith(QUOTE))
+    {
+      // 'Label' / 'Score' / 'attName'
+      int nextQuotePos = descriptor.indexOf(QUOTE, 1);
+      if (nextQuotePos == -1)
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+      firstField = descriptor.substring(1, nextQuotePos);
+      leftToParse = descriptor.substring(nextQuotePos + 1).trim();
+    }
+    else
+    {
+      // Label / Score / attName (unquoted)
+      int nextSpacePos = descriptor.indexOf(SPACE);
+      if (nextSpacePos == -1)
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+      firstField = descriptor.substring(0, nextSpacePos);
+      leftToParse = descriptor.substring(nextSpacePos + 1).trim();
+    }
+    String lower = firstField.toLowerCase();
+    if (lower.startsWith(LABEL.toLowerCase()))
+    {
+      byLabel = true;
+    }
+    else if (lower.startsWith(SCORE.toLowerCase()))
+    {
+      byScore = true;
+    }
+    else
+    {
+      attName = firstField;
+    }
+
+    /*
+     * next field is the comparison condition
+     * most conditions require a following pattern (optionally quoted)
+     * although some conditions e.g. Present do not
+     */
+    int nextSpacePos = leftToParse.indexOf(SPACE);
+    if (nextSpacePos == -1)
+    {
+      /*
+       * no value following condition - only valid for some conditions
+       */
+      cond = Condition.fromString(leftToParse);
+      if (cond == null || cond.needsAPattern())
+      {
+        System.err.println(invalidFormat);
+        return null;
+      }
+    }
+    else
+    {
+      /*
+       * condition and pattern
+       */
+      cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
+      leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
+      if (leftToParse.startsWith(QUOTE))
+      {
+        // pattern in quotes
+        if (leftToParse.endsWith(QUOTE))
+        {
+          pattern = leftToParse.substring(1, leftToParse.length() - 1);
+        }
+        else
+        {
+          // unbalanced quote
+          System.err.println(invalidFormat);
+          return null;
+        }
+      }
+      else
+      {
+        // unquoted pattern
+        pattern = leftToParse;
+      }
+    }
+
+    /*
+     * we have parsed out value, condition and pattern
+     * so can now make the FeatureMatcher
+     */
+    try
+    {
+      if (byLabel)
+      {
+        return FeatureMatcher.byLabel(cond, pattern);
+      }
+      else if (byScore)
+      {
+        return FeatureMatcher.byScore(cond, pattern);
+      }
+      else
+      {
+        String[] attNames = FeatureMatcher
+                .fromAttributeDisplayName(attName);
+        return FeatureMatcher.byAttribute(cond, pattern, attNames);
+      }
+    } catch (NumberFormatException e)
+    {
+      // numeric condition with non-numeric pattern
+      return null;
+    }
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature label (description)
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byLabel(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), true, false,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the feature score
+   * 
+   * @param cond
+   * @param pattern
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byScore(Condition cond, String pattern)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, true,
+            null);
+  }
+
+  /**
+   * A factory constructor method for a matcher that applies its match condition
+   * to the named feature attribute [and optional sub-attribute]
+   * 
+   * @param cond
+   * @param pattern
+   * @param attName
+   * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
+   */
+  public static FeatureMatcher byAttribute(Condition cond, String pattern,
+          String... attName)
+  {
+    return new FeatureMatcher(new Matcher(cond, pattern), false, false,
+            attName);
+  }
+
+  private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
+          String[] theKey)
+  {
+    key = theKey;
+    matcher = m;
+    byLabel = forLabel;
+    byScore = forScore;
+  }
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    String value = byLabel ? feature.getDescription()
+            : (byScore ? String.valueOf(feature.getScore())
+                    : feature.getValueAsString(key));
+    return matcher.matches(value);
+  }
+
+  @Override
+  public String[] getAttribute()
+  {
+    return key;
+  }
+
+  @Override
+  public MatcherI getMatcher()
+  {
+    return matcher;
+  }
+
+  /**
+   * Answers a string description of this matcher, suitable for display, debugging
+   * or logging. The format may change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    if (byLabel)
+    {
+      sb.append(MessageManager.getString("label.label"));
+    }
+    else if (byScore)
+    {
+      sb.append(MessageManager.getString("label.score"));
+    }
+    else
+    {
+      sb.append(String.join(COLON, key));
+    }
+
+    Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.toString().toLowerCase());
+    if (condition.isNumeric())
+    {
+      sb.append(SPACE).append(matcher.getPattern());
+    }
+    else if (condition.needsAPattern())
+    {
+      sb.append(" '").append(matcher.getPattern()).append(QUOTE);
+    }
+
+    return sb.toString();
+  }
+
+  @Override
+  public boolean isByLabel()
+  {
+    return byLabel;
+  }
+
+  @Override
+  public boolean isByScore()
+  {
+    return byScore;
+  }
+
+  @Override
+  public boolean isByAttribute()
+  {
+    return getAttribute() != null;
+  }
+
+  /**
+   * {@inheritDoc} The output of this method should be parseable by method
+   * <code>fromString<code> to restore the original object.
+   */
+  @Override
+  public String toStableString()
+  {
+    StringBuilder sb = new StringBuilder();
+    if (byLabel)
+    {
+      sb.append(LABEL); // no i18n here unlike toString() !
+    }
+    else if (byScore)
+    {
+      sb.append(SCORE);
+    }
+    else
+    {
+      /*
+       * enclose attribute name in quotes if it includes space
+       */
+      String displayName = toAttributeDisplayName(key);
+      if (displayName.contains(SPACE))
+      {
+        sb.append(QUOTE).append(displayName).append(QUOTE);
+      }
+      else
+      {
+        sb.append(displayName);
+      }
+    }
+  
+    Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.getStableName());
+    String pattern = matcher.getPattern();
+    if (condition.needsAPattern())
+    {
+      /*
+       * enclose pattern in quotes if it includes space
+       */
+      if (pattern.contains(SPACE))
+      {
+        sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
+      }
+      else
+      {
+        sb.append(SPACE).append(pattern);
+      }
+    }
+  
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java
new file mode 100644 (file)
index 0000000..f1f8585
--- /dev/null
@@ -0,0 +1,65 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.MatcherI;
+
+/**
+ * An interface for an object that can apply a match condition to a
+ * SequenceFeature object
+ * 
+ * @author gmcarstairs
+ */
+public interface FeatureMatcherI
+{
+  /**
+   * Answers true if the value provided for this matcher's key passes this
+   * matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Answers the attribute key this matcher operates on (or null if match is by
+   * Label or Score)
+   * 
+   * @return
+   */
+  String[] getAttribute();
+
+  /**
+   * Answers true if match is against feature label (description), else false
+   * 
+   * @return
+   */
+  boolean isByLabel();
+
+  /**
+   * Answers true if match is against feature score, else false
+   * 
+   * @return
+   */
+  boolean isByScore();
+
+  /**
+   * Answers true if match is against a feature attribute (text or range)
+   * 
+   * @return
+   */
+  boolean isByAttribute();
+
+  /**
+   * Answers the match condition that is applied
+   * 
+   * @return
+   */
+  MatcherI getMatcher();
+
+  /**
+   * Answers a string representation of this object suitable for use when
+   * persisting data, in a format that can be reliably read back. Any changes to
+   * the format should be backwards compatible.
+   */
+  String toStableString();
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..b51f2f0
--- /dev/null
@@ -0,0 +1,294 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that models one or more match conditions, which may be combined with
+ * AND or OR (but not a mixture)
+ * 
+ * @author gmcarstairs
+ */
+public class FeatureMatcherSet implements FeatureMatcherSetI
+{
+  private static final String OR = "OR";
+
+  private static final String AND = "AND";
+
+  private static final String SPACE = " ";
+
+  private static final String CLOSE_BRACKET = ")";
+
+  private static final String OPEN_BRACKET = "(";
+
+  private static final String OR_I18N = MessageManager
+          .getString("label.or");
+
+  private static final String AND_18N = MessageManager
+          .getString("label.and");
+
+  List<FeatureMatcherI> matchConditions;
+
+  boolean andConditions;
+
+  /**
+   * A factory constructor that converts a stringified object (as output by
+   * toStableString) to an object instance.
+   * 
+   * Format:
+   * <ul>
+   * <li>(condition1) AND (condition2) AND (condition3)</li>
+   * <li>or</li>
+   * <li>(condition1) OR (condition2) OR (condition3)</li>
+   * </ul>
+   * where OR and AND are not case-sensitive, and may not be mixed. Brackets are
+   * optional if there is only one condition.
+   * 
+   * @param descriptor
+   * @return
+   * @see FeatureMatcher#fromString(String)
+   */
+  public static FeatureMatcherSet fromString(final String descriptor)
+  {
+    String invalid = "Invalid descriptor: " + descriptor;
+    boolean firstCondition = true;
+    FeatureMatcherSet result = new FeatureMatcherSet();
+
+    String leftToParse = descriptor.trim();
+
+    while (leftToParse.length() > 0)
+    {
+      /*
+       * inspect AND or OR condition, check not mixed
+       */
+      boolean and = true;
+      if (!firstCondition)
+      {
+        int spacePos = leftToParse.indexOf(SPACE);
+        if (spacePos == -1)
+        {
+          // trailing junk after a match condition
+          System.err.println(invalid);
+          return null;
+        }
+        String conjunction = leftToParse.substring(0, spacePos);
+        leftToParse = leftToParse.substring(spacePos + 1).trim();
+        if (conjunction.equalsIgnoreCase(AND))
+        {
+          and = true;
+        }
+        else if (conjunction.equalsIgnoreCase(OR))
+        {
+          and = false;
+        }
+        else
+        {
+          // not an AND or an OR - invalid
+          System.err.println(invalid);
+          return null;
+        }
+      }
+
+      /*
+       * now extract the next condition and AND or OR it
+       */
+      String nextCondition = leftToParse;
+      if (leftToParse.startsWith(OPEN_BRACKET))
+      {
+        int closePos = leftToParse.indexOf(CLOSE_BRACKET);
+        if (closePos == -1)
+        {
+          System.err.println(invalid);
+          return null;
+        }
+        nextCondition = leftToParse.substring(1, closePos);
+        leftToParse = leftToParse.substring(closePos + 1).trim();
+      }
+      else
+      {
+        leftToParse = "";
+      }
+
+      FeatureMatcher fm = FeatureMatcher.fromString(nextCondition);
+      if (fm == null)
+      {
+        System.err.println(invalid);
+        return null;
+      }
+      try
+      {
+        if (and)
+        {
+          result.and(fm);
+        }
+        else
+        {
+          result.or(fm);
+        }
+        firstCondition = false;
+      } catch (IllegalStateException e)
+      {
+        // thrown if OR and AND are mixed
+        System.err.println(invalid);
+        return null;
+      }
+
+    }
+    return result;
+  }
+
+  /**
+   * Constructor
+   */
+  public FeatureMatcherSet()
+  {
+    matchConditions = new ArrayList<>();
+  }
+
+  @Override
+  public boolean matches(SequenceFeature feature)
+  {
+    /*
+     * no conditions matches anything
+     */
+    if (matchConditions.isEmpty())
+    {
+      return true;
+    }
+
+    /*
+     * AND until failure
+     */
+    if (andConditions)
+    {
+      for (FeatureMatcherI m : matchConditions)
+      {
+        if (!m.matches(feature))
+        {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    /*
+     * OR until match
+     */
+    for (FeatureMatcherI m : matchConditions)
+    {
+      if (m.matches(feature))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void and(FeatureMatcherI m)
+  {
+    if (!andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an AND to OR conditions");
+    }
+    matchConditions.add(m);
+    andConditions = true;
+  }
+
+  @Override
+  public void or(FeatureMatcherI m)
+  {
+    if (andConditions && matchConditions.size() > 1)
+    {
+      throw new IllegalStateException("Can't add an OR to AND conditions");
+    }
+    matchConditions.add(m);
+    andConditions = false;
+  }
+
+  @Override
+  public boolean isAnded()
+  {
+    return andConditions;
+  }
+
+  @Override
+  public Iterable<FeatureMatcherI> getMatchers()
+  {
+    return matchConditions;
+  }
+
+  /**
+   * Answers a string representation of this object suitable for display, and
+   * possibly internationalized. The format is not guaranteed stable and may
+   * change in future.
+   */
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean first = true;
+    boolean multiple = matchConditions.size() > 1;
+    for (FeatureMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        String joiner = andConditions ? AND_18N : OR_I18N;
+        sb.append(SPACE).append(joiner.toLowerCase()).append(SPACE);
+      }
+      first = false;
+      if (multiple)
+      {
+        sb.append(OPEN_BRACKET).append(matcher.toString())
+                .append(CLOSE_BRACKET);
+      }
+      else
+      {
+        sb.append(matcher.toString());
+      }
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public boolean isEmpty()
+  {
+    return matchConditions == null || matchConditions.isEmpty();
+  }
+
+  /**
+   * {@inheritDoc} The output of this method should be parseable by method
+   * <code>fromString<code> to restore the original object.
+   */
+  @Override
+  public String toStableString()
+  {
+    StringBuilder sb = new StringBuilder();
+    boolean moreThanOne = matchConditions.size() > 1;
+    boolean first = true;
+
+    for (FeatureMatcherI matcher : matchConditions)
+    {
+      if (!first)
+      {
+        String joiner = andConditions ? AND : OR;
+        sb.append(SPACE).append(joiner).append(SPACE);
+      }
+      first = false;
+      if (moreThanOne)
+      {
+        sb.append(OPEN_BRACKET).append(matcher.toStableString())
+                .append(CLOSE_BRACKET);
+      }
+      else
+      {
+        sb.append(matcher.toStableString());
+      }
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/datamodel/features/FeatureMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java
new file mode 100644 (file)
index 0000000..90c2986
--- /dev/null
@@ -0,0 +1,68 @@
+package jalview.datamodel.features;
+
+import jalview.datamodel.SequenceFeature;
+
+/**
+ * An interface to describe a set of one or more feature matchers, where all
+ * matchers are combined with either AND or OR
+ * 
+ * @author gmcarstairs
+ *
+ */
+public interface FeatureMatcherSetI
+{
+  /**
+   * Answers true if the feature provided passes this matcher's match condition
+   * 
+   * @param feature
+   * @return
+   */
+  boolean matches(SequenceFeature feature);
+
+  /**
+   * Adds (ANDs) match condition m to this object's matcher set
+   * 
+   * @param m
+   * @throws IllegalStateException
+   *           if an attempt is made to AND to existing OR-ed conditions
+   */
+  void and(FeatureMatcherI m);
+
+  /**
+   * Answers true if any second condition is AND-ed with this one, false if it
+   * is OR-ed
+   * 
+   * @return
+   */
+  boolean isAnded();
+
+  /**
+   * Adds (ORs) the given condition to this object's match conditions
+   * 
+   * @param m
+   * @throws IllegalStateException
+   *           if an attempt is made to OR to existing AND-ed conditions
+   */
+  void or(FeatureMatcherI m);
+
+  /**
+   * Answers an iterator over the combined match conditions
+   * 
+   * @return
+   */
+  Iterable<FeatureMatcherI> getMatchers();
+
+  /**
+   * Answers true if this object contains no conditions
+   * 
+   * @return
+   */
+  boolean isEmpty();
+
+  /**
+   * Answers a string representation of this object suitable for use when
+   * persisting data, in a format that can be reliably read back. Any changes to
+   * the format should be backwards compatible.
+   */
+  String toStableString();
+}
diff --git a/src/jalview/datamodel/features/FeatureSource.java b/src/jalview/datamodel/features/FeatureSource.java
new file mode 100644 (file)
index 0000000..a1be1dc
--- /dev/null
@@ -0,0 +1,78 @@
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class to model one source of feature data, including metadata about
+ * attributes of features
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSource implements FeatureSourceI
+{
+  private String name;
+
+  private Map<String, String> attributeNames;
+  
+  private Map<String, FeatureAttributeType> attributeTypes;
+  
+  /**
+   * Constructor
+   * 
+   * @param theName
+   */
+  public FeatureSource(String theName)
+  {
+    this.name = theName;
+    attributeNames = new HashMap<>();
+    attributeTypes = new HashMap<>();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getName()
+  {
+    return name;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getAttributeName(String attributeId)
+  {
+    return attributeNames.get(attributeId);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public FeatureAttributeType getAttributeType(String attributeId)
+  {
+    return attributeTypes.get(attributeId);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setAttributeName(String id, String attName)
+  {
+    attributeNames.put(id, attName);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void setAttributeType(String id, FeatureAttributeType type)
+  {
+    attributeTypes.put(id, type);
+  }
+
+}
diff --git a/src/jalview/datamodel/features/FeatureSourceI.java b/src/jalview/datamodel/features/FeatureSourceI.java
new file mode 100644 (file)
index 0000000..c873593
--- /dev/null
@@ -0,0 +1,45 @@
+package jalview.datamodel.features;
+
+public interface FeatureSourceI
+{
+  /**
+   * Answers a name for the feature source (not necessarily unique)
+   * 
+   * @return
+   */
+  String getName();
+
+  /**
+   * Answers the 'long name' of an attribute given its id (short name or
+   * abbreviation), or null if not known
+   * 
+   * @param attributeId
+   * @return
+   */
+  String getAttributeName(String attributeId);
+
+  /**
+   * Sets the 'long name' of an attribute given its id (short name or
+   * abbreviation).
+   * 
+   * @param id
+   * @param name
+   */
+  void setAttributeName(String id, String name);
+
+  /**
+   * Answers the datatype of the attribute with given id, or null if not known
+   * 
+   * @param attributeId
+   * @return
+   */
+  FeatureAttributeType getAttributeType(String attributeId);
+
+  /**
+   * Sets the datatype of the attribute with given id
+   * 
+   * @param id
+   * @param type
+   */
+  void setAttributeType(String id, FeatureAttributeType type);
+}
diff --git a/src/jalview/datamodel/features/FeatureSources.java b/src/jalview/datamodel/features/FeatureSources.java
new file mode 100644 (file)
index 0000000..1be1b82
--- /dev/null
@@ -0,0 +1,58 @@
+package jalview.datamodel.features;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A singleton to hold metadata about feature attributes, keyed by a unique
+ * feature source identifier
+ * 
+ * @author gmcarstairs
+ *
+ */
+public class FeatureSources
+{
+  private static FeatureSources instance = new FeatureSources();
+
+  private Map<String, FeatureSourceI> sources;
+
+  /**
+   * Answers the singleton instance of this class
+   * 
+   * @return
+   */
+  public static FeatureSources getInstance()
+  {
+    return instance;
+  }
+
+  private FeatureSources()
+  {
+    sources = new HashMap<>();
+  }
+
+  /**
+   * Answers the FeatureSource with the given unique identifier, or null if not
+   * known
+   * 
+   * @param sourceId
+   * @return
+   */
+  public FeatureSourceI getSource(String sourceId)
+  {
+    return sources.get(sourceId);
+  }
+
+  /**
+   * Adds the given source under the given key. This will replace any existing
+   * source with the same id, it is the caller's responsibility to ensure keys
+   * are unique if necessary.
+   * 
+   * @param sourceId
+   * @param source
+   */
+  public void addSource(String sourceId, FeatureSource source)
+  {
+    sources.put(sourceId, source);
+  }
+}
index 51bee57..02ce1c5 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
index b8160d3..ae58a69 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
index 007f3b1..b991750 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
index 26ffee1..b7d702d 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
index 52da8c7..fcf1b53 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.ContiguousI;
@@ -149,6 +169,14 @@ public class SequenceFeatures implements SequenceFeaturesI
     }
 
     Set<String> featureTypes = getFeatureTypes(ontologyTerm);
+    if (featureTypes.isEmpty())
+    {
+      /*
+       * no features of the specified type or any sub-type
+       */
+      return new ArrayList<>();
+    }
+
     return getAllFeatures(featureTypes.toArray(new String[featureTypes
             .size()]));
   }
index 58beca2..80c4f9a 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
index 4a359ff..4c2ae24 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.xdb.uniprot;
 
 /**
@@ -7,7 +27,11 @@ public class UniprotFeature
 {
   private String type;
 
-  private String description;
+  private String description = null;
+
+  private String original = null;
+
+  private String variation = null;
 
   private String status;
 
@@ -27,7 +51,19 @@ public class UniprotFeature
 
   public String getDescription()
   {
-    return description;
+    if (description == null && variation == null && original == null)
+    {
+      return null;
+    }
+    return (description == null ? "" : description)
+            + (variation != null
+                    ? (description != null ? " " : "") + "Variation: '"
+                            + variation + "'"
+                    : "")
+            + (original != null
+                    ? ((description != null || variation != null) ? " "
+                            : "") + "Original: '" + original + "'"
+                    : "");
   }
 
   public void setDescription(String d)
@@ -75,4 +111,24 @@ public class UniprotFeature
     this.begin = p;
     this.end = p;
   }
+
+  public String getOriginal()
+  {
+    return original;
+  }
+
+  public void setOriginal(String original)
+  {
+    this.original = original;
+  }
+
+  public String getVariation()
+  {
+    return variation;
+  }
+
+  public void setVariation(String variant)
+  {
+    this.variation = variant;
+  }
 }
index 6d031b7..952f01e 100644 (file)
@@ -24,9 +24,6 @@ import jalview.datamodel.SequenceFeature;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 
-import java.util.HashMap;
-import java.util.Map;
-
 import com.stevesoft.pat.Regex;
 
 /**
@@ -47,13 +44,6 @@ public class EnsemblCdna extends EnsemblSeqProxy
   private static final Regex ACCESSION_REGEX = new Regex(
           "(ENS([A-Z]{3}|)[TG][0-9]{11}$)" + "|" + "(CCDS[0-9.]{3,}$)");
 
-  private static Map<String, String> params = new HashMap<String, String>();
-
-  static
-  {
-    params.put("object_type", "transcript");
-  }
-
   /*
    * fetch exon features on genomic sequence (to identify the cdna regions)
    * and cds and variation features (to retain)
@@ -139,13 +129,13 @@ public class EnsemblCdna extends EnsemblSeqProxy
   }
 
   /**
-   * Parameter object_type=cdna added to ensure cdna and not peptide is returned
-   * (JAL-2529)
+   * Parameter object_type=Transcaript added to ensure cdna and not peptide is
+   * returned (JAL-2529)
    */
   @Override
-  protected Map<String, String> getAdditionalParameters()
+  protected String getObjectType()
   {
-    return params;
+    return OBJECT_TYPE_TRANSCRIPT;
   }
 
 }
diff --git a/src/jalview/ext/ensembl/EnsemblData.java b/src/jalview/ext/ensembl/EnsemblData.java
new file mode 100644 (file)
index 0000000..47fe0fc
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.ext.ensembl;
+
+/**
+ * A data class to model the data and rest version of one Ensembl domain,
+ * currently for rest.ensembl.org and rest.ensemblgenomes.org
+ * 
+ * @author gmcarstairs
+ */
+class EnsemblData
+{
+  /*
+   * The http domain this object is holding data values for
+   */
+  String domain;
+
+  /*
+   * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
+   * ok, a major version change may break stuff 
+   */
+  String expectedRestVersion;
+
+  /*
+   * Major / minor / point version e.g. "4.5.1"
+   * @see http://rest.ensembl.org/info/rest/?content-type=application/json
+   */
+  String restVersion;
+
+  /*
+   * data version
+   * @see http://rest.ensembl.org/info/data/?content-type=application/json
+   */
+  String dataVersion;
+
+  /*
+   * true when http://rest.ensembl.org/info/ping/?content-type=application/json
+   * returns response code 200 and not {"error":"Database is unavailable"}
+   */
+  boolean restAvailable;
+
+  /*
+   * absolute time when availability was last checked
+   */
+  long lastAvailableCheckTime;
+
+  /*
+   * absolute time when version numbers were last checked
+   */
+  long lastVersionCheckTime;
+
+  // flag set to true if REST major version is not the one expected
+  boolean restMajorVersionMismatch;
+
+  /*
+   * absolute time to wait till if we overloaded the REST service
+   */
+  long retryAfter;
+
+  /**
+   * Constructor given expected REST version number e.g 4.5 or 3.4.3
+   * 
+   * @param restExpected
+   */
+  EnsemblData(String theDomain, String restExpected)
+  {
+    domain = theDomain;
+    expectedRestVersion = restExpected;
+    lastAvailableCheckTime = -1;
+    lastVersionCheckTime = -1;
+  }
+
+}
index 7570822..cb6f548 100644 (file)
@@ -82,7 +82,7 @@ class EnsemblFeatures extends EnsemblRestClient
   public AlignmentI getSequenceRecords(String query) throws IOException
   {
     // TODO: use a vararg String... for getSequenceRecords instead?
-    List<String> queries = new ArrayList<String>();
+    List<String> queries = new ArrayList<>();
     queries.add(query);
     FileParse fp = getSequenceReader(queries);
     if (fp == null || !fp.isValid())
@@ -109,9 +109,17 @@ class EnsemblFeatures extends EnsemblRestClient
     urlstring.append("?content-type=text/x-gff3");
 
     /*
+     * specify object_type=gene in case is shared by transcript and/or protein;
+     * currently only fetching features for gene sequences;
+     * refactor in future if needed to fetch for transcripts
+     */
+    urlstring.append("&").append(OBJECT_TYPE).append("=")
+            .append(OBJECT_TYPE_GENE);
+
+    /*
      * specify  features to retrieve
      * @see http://rest.ensembl.org/documentation/info/overlap_id
-     * could make the list a configurable entry in jalview.properties
+     * could make the list a configurable entry in .jalview_properties
      */
     for (EnsemblFeatureType feature : featuresWanted)
     {
index 365c1c2..7e6f653 100644 (file)
@@ -23,6 +23,8 @@ package jalview.ext.ensembl;
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsModelI;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -98,6 +100,12 @@ public class EnsemblGene extends EnsemblSeqProxy
     return EnsemblSeqType.GENOMIC;
   }
 
+  @Override
+  protected String getObjectType()
+  {
+    return OBJECT_TYPE_GENE;
+  }
+
   /**
    * Returns an alignment containing the gene(s) for the given gene or
    * transcript identifier, or external identifier (e.g. Uniprot id). If given a
@@ -144,8 +152,14 @@ public class EnsemblGene extends EnsemblSeqProxy
       {
         continue;
       }
+      
       if (geneAlignment.getHeight() == 1)
       {
+        // ensure id has 'correct' case for the Ensembl identifier
+        geneId = geneAlignment.getSequenceAt(0).getName();
+
+        findGeneLoci(geneAlignment.getSequenceAt(0), geneId);
+
         getTranscripts(geneAlignment, geneId);
       }
       if (al == null)
@@ -161,66 +175,104 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Converts a query, which may contain one or more gene or transcript
-   * identifiers, into a non-redundant list of gene identifiers.
+   * Calls the /lookup/id REST service, parses the response for gene
+   * coordinates, and if successful, adds these to the sequence. If this fails,
+   * fall back on trying to parse the sequence description in case it is in
+   * Ensembl-gene format e.g. chromosome:GRCh38:17:45051610:45109016:1.
+   * 
+   * @param seq
+   * @param geneId
+   */
+  void findGeneLoci(SequenceI seq, String geneId)
+  {
+    GeneLociI geneLoci = new EnsemblLookup(getDomain()).getGeneLoci(geneId);
+    if (geneLoci != null)
+    {
+      seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+              geneLoci.getChromosomeId(), geneLoci.getMap());
+    }
+    else
+    {
+      parseChromosomeLocations(seq);
+    }
+  }
+
+  /**
+   * Parses and saves fields of an Ensembl-style description e.g.
+   * chromosome:GRCh38:17:45051610:45109016:1
+   * 
+   * @param seq
+   */
+  boolean parseChromosomeLocations(SequenceI seq)
+  {
+    String description = seq.getDescription();
+    if (description == null)
+    {
+      return false;
+    }
+    String[] tokens = description.split(":");
+    if (tokens.length == 6 && tokens[0].startsWith(DBRefEntry.CHROMOSOME))
+    {
+      String ref = tokens[1];
+      String chrom = tokens[2];
+      try
+      {
+        int chStart = Integer.parseInt(tokens[3]);
+        int chEnd = Integer.parseInt(tokens[4]);
+        boolean forwardStrand = "1".equals(tokens[5]);
+        String species = ""; // not known here
+        int[] from = new int[] { seq.getStart(), seq.getEnd() };
+        int[] to = new int[] { forwardStrand ? chStart : chEnd,
+            forwardStrand ? chEnd : chStart };
+        MapList map = new MapList(from, to, 1, 1);
+        seq.setGeneLoci(species, ref, chrom, map);
+        return true;
+      } catch (NumberFormatException e)
+      {
+        System.err.println("Bad integers in description " + description);
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Converts a query, which may contain one or more gene, transcript, or
+   * external (to Ensembl) identifiers, into a non-redundant list of gene
+   * identifiers.
    * 
    * @param accessions
    * @return
    */
   List<String> getGeneIds(String accessions)
   {
-    List<String> geneIds = new ArrayList<String>();
+    List<String> geneIds = new ArrayList<>();
 
     for (String acc : accessions.split(getAccessionSeparator()))
     {
-      if (isGeneIdentifier(acc))
-      {
-        if (!geneIds.contains(acc))
-        {
-          geneIds.add(acc);
-        }
-      }
-
       /*
-       * if given a transcript id, look up its gene parent
+       * First try lookup as an Ensembl (gene or transcript) identifier
        */
-      else if (isTranscriptIdentifier(acc))
+      String geneId = new EnsemblLookup(getDomain()).getGeneId(acc);
+      if (geneId != null)
       {
-        String geneId = new EnsemblLookup(getDomain()).getParent(acc);
-        if (geneId != null && !geneIds.contains(geneId))
+        if (!geneIds.contains(geneId))
         {
           geneIds.add(geneId);
         }
       }
-      else if (isProteinIdentifier(acc))
-      {
-        String tscriptId = new EnsemblLookup(getDomain()).getParent(acc);
-        if (tscriptId != null)
-        {
-          String geneId = new EnsemblLookup(getDomain())
-                  .getParent(tscriptId);
-
-          if (geneId != null && !geneIds.contains(geneId))
-          {
-            geneIds.add(geneId);
-          }
-        }
-        // NOTE - acc is lost if it resembles an ENS.+ ID but isn't actually
-        // resolving to one... e.g. ENSMICP00000009241
-      }
-      /*
-       * if given a gene or other external name, lookup and fetch 
-       * the corresponding gene for all model organisms 
-       */
       else
       {
+        /*
+         * if given a gene or other external name, lookup and fetch 
+         * the corresponding gene for all model organisms 
+         */
         List<String> ids = new EnsemblSymbol(getDomain(), getDbSource(),
-                getDbVersion()).getIds(acc);
-        for (String geneId : ids)
+                getDbVersion()).getGeneIds(acc);
+        for (String id : ids)
         {
-          if (!geneIds.contains(geneId))
+          if (!geneIds.contains(id))
           {
-            geneIds.add(geneId);
+            geneIds.add(id);
           }
         }
       }
@@ -229,30 +281,6 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
-   * Attempts to get Ensembl stable identifiers for model organisms for a gene
-   * name by calling the xrefs symbol REST service to resolve the gene name.
-   * 
-   * @param query
-   * @return
-   */
-  protected String getGeneIdentifiersForName(String query)
-  {
-    List<String> ids = new EnsemblSymbol(getDomain(), getDbSource(),
-            getDbVersion()).getIds(query);
-    if (ids != null)
-    {
-      for (String id : ids)
-      {
-        if (isGeneIdentifier(id))
-        {
-          return id;
-        }
-      }
-    }
-    return null;
-  }
-
-  /**
    * Constructs all transcripts for the gene, as identified by "transcript"
    * features whose Parent is the requested gene. The coding transcript
    * sequences (i.e. with introns omitted) are added to the alignment.
@@ -352,7 +380,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     int transcriptLength = 0;
     final char[] geneChars = gene.getSequence();
     int offset = gene.getStart(); // to convert to 0-based positions
-    List<int[]> mappedFrom = new ArrayList<int[]>();
+    List<int[]> mappedFrom = new ArrayList<>();
 
     for (SequenceFeature sf : splices)
     {
@@ -394,13 +422,15 @@ public class EnsemblGene extends EnsemblSeqProxy
      * transfer features to the new sequence; we use EnsemblCdna to do this,
      * to filter out unwanted features types (see method retainFeature)
      */
-    List<int[]> mapTo = new ArrayList<int[]>();
+    List<int[]> mapTo = new ArrayList<>();
     mapTo.add(new int[] { 1, transcriptLength });
     MapList mapping = new MapList(mappedFrom, mapTo, 1, 1);
     EnsemblCdna cdna = new EnsemblCdna(getDomain());
     cdna.transferFeatures(gene.getFeatures().getPositionalFeatures(),
             transcript.getDatasetSequence(), mapping, parentId);
 
+    mapTranscriptToChromosome(transcript, gene, mapping);
+
     /*
      * fetch and save cross-references
      */
@@ -415,6 +445,42 @@ public class EnsemblGene extends EnsemblSeqProxy
   }
 
   /**
+   * If the gene has a mapping to chromosome coordinates, derive the transcript
+   * chromosome regions and save on the transcript sequence
+   * 
+   * @param transcript
+   * @param gene
+   * @param mapping
+   *          the mapping from gene to transcript positions
+   */
+  protected void mapTranscriptToChromosome(SequenceI transcript,
+          SequenceI gene, MapList mapping)
+  {
+    GeneLociI loci = gene.getGeneLoci();
+    if (loci == null)
+    {
+      return;
+    }
+
+    MapList geneMapping = loci.getMap();
+
+    List<int[]> exons = mapping.getFromRanges();
+    List<int[]> transcriptLoci = new ArrayList<>();
+
+    for (int[] exon : exons)
+    {
+      transcriptLoci.add(geneMapping.locateInTo(exon[0], exon[1]));
+    }
+
+    List<int[]> transcriptRange = Arrays.asList(new int[] {
+        transcript.getStart(), transcript.getEnd() });
+    MapList mapList = new MapList(transcriptRange, transcriptLoci, 1, 1);
+
+    transcript.setGeneLoci(loci.getSpeciesId(), loci.getAssemblyId(),
+            loci.getChromosomeId(), mapList);
+  }
+
+  /**
    * Returns the 'transcript_id' property of the sequence feature (or null)
    * 
    * @param feature
@@ -428,6 +494,12 @@ public class EnsemblGene extends EnsemblSeqProxy
   /**
    * Returns a list of the transcript features on the sequence whose Parent is
    * the gene for the accession id.
+   * <p>
+   * Transcript features are those of type "transcript", or any of its sub-types
+   * in the Sequence Ontology e.g. "mRNA", "processed_transcript". We also
+   * include "NMD_transcript_variant", because this type behaves like a
+   * transcript identifier in Ensembl, although strictly speaking it is not in
+   * the SO.
    * 
    * @param accId
    * @param geneSequence
@@ -436,22 +508,21 @@ public class EnsemblGene extends EnsemblSeqProxy
   protected List<SequenceFeature> getTranscriptFeatures(String accId,
           SequenceI geneSequence)
   {
-    List<SequenceFeature> transcriptFeatures = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> transcriptFeatures = new ArrayList<>();
 
     String parentIdentifier = GENE_PREFIX + accId;
-    // todo optimise here by transcript type!
+
     List<SequenceFeature> sfs = geneSequence.getFeatures()
-            .getPositionalFeatures();
+            .getFeaturesByOntology(SequenceOntologyI.TRANSCRIPT);
+    sfs.addAll(geneSequence.getFeatures().getPositionalFeatures(
+            SequenceOntologyI.NMD_TRANSCRIPT_VARIANT));
 
     for (SequenceFeature sf : sfs)
     {
-      if (isTranscript(sf.getType()))
+      String parent = (String) sf.getValue(PARENT);
+      if (parentIdentifier.equalsIgnoreCase(parent))
       {
-        String parent = (String) sf.getValue(PARENT);
-        if (parentIdentifier.equals(parent))
-        {
-          transcriptFeatures.add(sf);
-        }
+        transcriptFeatures.add(sf);
       }
     }
 
@@ -486,8 +557,9 @@ public class EnsemblGene extends EnsemblSeqProxy
     if (SequenceOntologyFactory.getInstance().isA(sf.getType(),
             SequenceOntologyI.GENE))
     {
-      String id = (String) sf.getValue(ID);
-      if ((GENE_PREFIX + accId).equals(id))
+      // NB features as gff use 'ID'; rest services return as 'id'
+      String id = (String) sf.getValue("ID");
+      if ((GENE_PREFIX + accId).equalsIgnoreCase(id))
       {
         return true;
       }
@@ -514,7 +586,7 @@ public class EnsemblGene extends EnsemblSeqProxy
     if (isTranscript(type))
     {
       String parent = (String) sf.getValue(PARENT);
-      if (!(GENE_PREFIX + accessionId).equals(parent))
+      if (!(GENE_PREFIX + accessionId).equalsIgnoreCase(parent))
       {
         return false;
       }
index 458a233..bde3c0f 100644 (file)
@@ -103,7 +103,7 @@ public class EnsemblGenome extends EnsemblSeqProxy
   {
     if (isTranscript(sf.getType()))
     {
-      String id = (String) sf.getValue(ID);
+      String id = (String) sf.getValue("ID");
       if (("transcript:" + accId).equals(id))
       {
         return true;
index ef46a5b..9fc6a53 100644 (file)
@@ -20,6 +20,9 @@
  */
 package jalview.ext.ensembl;
 
+import jalview.bin.Cache;
+import jalview.datamodel.DBRefSource;
+
 /**
  * A class to behave much like EnsemblGene but referencing the ensemblgenomes
  * domain and data
@@ -35,31 +38,30 @@ public class EnsemblGenomes extends EnsemblGene
    */
   public EnsemblGenomes()
   {
-    super(ENSEMBL_GENOMES_REST);
-  }
-
-  @Override
-  public boolean isGeneIdentifier(String query)
-  {
-    return true;
+    super();
+    setDomain(Cache.getDefault(ENSEMBL_GENOMES_BASEURL,
+            DEFAULT_ENSEMBL_GENOMES_BASEURL));
   }
 
   @Override
   public String getDbName()
   {
-    return "EnsemblGenomes";
+    return DBRefSource.ENSEMBLGENOMES;
   }
 
   @Override
   public String getTestQuery()
   {
-    return "DDB_G0283883";
+    /*
+     * Salmonella gene, Uniprot Q8Z9G6, EMBLCDS CAD01290
+     */
+    return "CAD01290";
   }
 
   @Override
   public String getDbSource()
   {
-    return "EnsemblGenomes";
+    return DBRefSource.ENSEMBLGENOMES;
   }
 
 }
index 3108194..37dff51 100644 (file)
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
 package jalview.ext.ensembl;
 
-/**
- * A data class to model the data and rest version of one Ensembl domain,
- * currently for rest.ensembl.org and rest.ensemblgenomes.org
- * 
- * @author gmcarstairs
- */
-class EnsemblInfo
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.json.simple.JSONArray;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class EnsemblInfo extends EnsemblRestClient
 {
-  /*
-   * The http domain this object is holding data values for
-   */
-  String domain;
 
   /*
-   * The latest version Jalview has tested for, e.g. "4.5"; a minor version change should be
-   * ok, a major version change may break stuff 
+   * cached results of REST /info/divisions service, currently
+   * <pre>
+   * { 
+   *  { "ENSEMBLFUNGI", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLBACTERIA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPROTISTS", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLMETAZOA", "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBLPLANTS",  "http://rest.ensemblgenomes.org"},
+   *    "ENSEMBL", "http://rest.ensembl.org" }
+   *  }
+   * </pre>
+   * The values for EnsemblGenomes are retrieved by a REST call, that for
+   * Ensembl is added programmatically for convenience of lookup
    */
-  String expectedRestVersion;
+  private static Map<String, String> divisions;
 
-  /*
-   * Major / minor / point version e.g. "4.5.1"
-   * @see http://rest.ensembl.org/info/rest/?content-type=application/json
-   */
-  String restVersion;
+  @Override
+  public String getDbName()
+  {
+    return "ENSEMBL";
+  }
 
-  /*
-   * data version
-   * @see http://rest.ensembl.org/info/data/?content-type=application/json
-   */
-  String dataVersion;
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    return null;
+  }
 
-  /*
-   * true when http://rest.ensembl.org/info/ping/?content-type=application/json
-   * returns response code 200 and not {"error":"Database is unavailable"}
-   */
-  boolean restAvailable;
+  @Override
+  protected URL getUrl(List<String> ids) throws MalformedURLException
+  {
+    return null;
+  }
 
-  /*
-   * absolute time when availability was last checked
+  @Override
+  protected boolean useGetRequest()
+  {
+    return true;
+  }
+
+  @Override
+  protected String getRequestMimeType(boolean multipleIds)
+  {
+    return "application/json";
+  }
+
+  @Override
+  protected String getResponseMimeType()
+  {
+    return "application/json";
+  }
+
+  /**
+   * Answers the domain (http://rest.ensembl.org or
+   * http://rest.ensemblgenomes.org) for the given division, or null if not
+   * recognised by Ensembl.
+   * 
+   * @param division
+   * @return
    */
-  long lastAvailableCheckTime;
+  public String getDomain(String division)
+  {
+    if (divisions == null)
+    {
+      fetchDivisions();
+    }
+    return divisions.get(division.toUpperCase());
+  }
 
-  /*
-   * absolute time when version numbers were last checked
+  /**
+   * On first request only, populate the lookup map by fetching the list of
+   * divisions known to EnsemblGenomes.
    */
-  long lastVersionCheckTime;
+  void fetchDivisions()
+  {
+    divisions = new HashMap<>();
 
-  // flag set to true if REST major version is not the one expected
-  boolean restMajorVersionMismatch;
+    /*
+     * for convenience, pre-fill ensembl.org as the domain for "ENSEMBL"
+     */
+    divisions.put(DBRefSource.ENSEMBL.toUpperCase(), ensemblDomain);
 
-  /*
-   * absolute time to wait till if we overloaded the REST service
+    BufferedReader br = null;
+    try
+    {
+      URL url = getDivisionsUrl(ensemblGenomesDomain);
+      if (url != null)
+      {
+        br = getHttpResponse(url, null);
+      }
+      parseResponse(br, ensemblGenomesDomain);
+    } catch (IOException e)
+    {
+      // ignore
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
+   * Parses the JSON response to /info/divisions, and add each to the lookup map
+   * 
+   * @param br
+   * @param domain
    */
-  long retryAfter;
+  void parseResponse(BufferedReader br, String domain)
+  {
+    JSONParser jp = new JSONParser();
+
+    try
+    {
+      JSONArray parsed = (JSONArray) jp.parse(br);
+
+      Iterator rvals = parsed.iterator();
+      while (rvals.hasNext())
+      {
+        String division = rvals.next().toString();
+        divisions.put(division.toUpperCase(), domain);
+      }
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+  }
 
   /**
-   * Constructor given expected REST version number e.g 4.5 or 3.4.3
+   * Constructs the URL for the EnsemblGenomes /info/divisions REST service
+   * @param domain TODO
    * 
-   * @param restExpected
+   * @return
+   * @throws MalformedURLException
    */
-  EnsemblInfo(String theDomain, String restExpected)
+  URL getDivisionsUrl(String domain) throws MalformedURLException
   {
-    domain = theDomain;
-    expectedRestVersion = restExpected;
-    lastAvailableCheckTime = -1;
-    lastVersionCheckTime = -1;
+    return new URL(domain
+            + "/info/divisions?content-type=application/json");
   }
 
+  /**
+   * Returns the set of 'divisions' recognised by Ensembl or EnsemblGenomes
+   * 
+   * @return
+   */
+  public Set<String> getDivisions() {
+    if (divisions == null)
+    {
+      fetchDivisions();
+    }
+
+    return divisions.keySet();
+  }
 }
index eb8f90e..5f353f8 100644 (file)
  */
 package jalview.ext.ensembl;
 
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.GeneLociI;
+import jalview.util.MapList;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import org.json.simple.JSONObject;
@@ -34,14 +38,15 @@ import org.json.simple.parser.JSONParser;
 import org.json.simple.parser.ParseException;
 
 /**
- * A client for the Ensembl lookup REST endpoint; used to find the Parent gene
- * identifier given a transcript identifier.
+ * A client for the Ensembl /lookup REST endpoint, used to find the gene
+ * identifier given a gene, transcript or protein identifier, or to extract the
+ * species or chromosomal coordinates from the same service response
  * 
  * @author gmcarstairs
- *
  */
 public class EnsemblLookup extends EnsemblRestClient
 {
+  private static final String SPECIES = "species";
 
   /**
    * Default constructor (to use rest.ensembl.org)
@@ -77,17 +82,26 @@ public class EnsemblLookup extends EnsemblRestClient
   protected URL getUrl(List<String> ids) throws MalformedURLException
   {
     String identifier = ids.get(0);
-    return getUrl(identifier);
+    return getUrl(identifier, null);
   }
 
   /**
+   * Gets the url for lookup of the given identifier, optionally with objectType
+   * also specified in the request
+   * 
    * @param identifier
+   * @param objectType
    * @return
    */
-  protected URL getUrl(String identifier)
+  protected URL getUrl(String identifier, String objectType)
   {
     String url = getDomain() + "/lookup/id/" + identifier
-            + "?content-type=application/json";
+            + CONTENT_TYPE_JSON;
+    if (objectType != null)
+    {
+      url += "&" + OBJECT_TYPE + "=" + objectType;
+    }
+
     try
     {
       return new URL(url);
@@ -116,28 +130,112 @@ public class EnsemblLookup extends EnsemblRestClient
   }
 
   /**
-   * Calls the Ensembl lookup REST endpoint and retrieves the 'Parent' for the
+   * Returns the gene id related to the given identifier (which may be for a
+   * gene, transcript or protein)
+   * 
+   * @param identifier
+   * @return
+   */
+  public String getGeneId(String identifier)
+  {
+    return getGeneId(identifier, null);
+  }
+
+  /**
+   * Returns the gene id related to the given identifier (which may be for a
+   * gene, transcript or protein)
+   * 
+   * @param identifier
+   * @param objectType
+   * @return
+   */
+  public String getGeneId(String identifier, String objectType)
+  {
+    return parseGeneId(getResult(identifier, objectType));
+  }
+
+  /**
+   * Parses the JSON response and returns the gene identifier, or null if not
+   * found. If the returned object_type is Gene, returns the id, if Transcript
+   * returns the Parent. If it is Translation (peptide identifier), then the
+   * Parent is the transcript identifier, so we redo the search with this value.
+   * 
+   * @param br
+   * @return
+   */
+  protected String parseGeneId(JSONObject val)
+  {
+    String geneId = null;
+    String type = val.get(OBJECT_TYPE).toString();
+    if (OBJECT_TYPE_GENE.equalsIgnoreCase(type))
+    {
+      // got the gene - just returns its id
+      geneId = val.get(JSON_ID).toString();
+    }
+    else if (OBJECT_TYPE_TRANSCRIPT.equalsIgnoreCase(type))
+    {
+      // got the transcript - return its (Gene) Parent
+      geneId = val.get(PARENT).toString();
+    }
+    else if (OBJECT_TYPE_TRANSLATION.equalsIgnoreCase(type))
+    {
+      // got the protein - get its Parent, restricted to type Transcript
+      String transcriptId = val.get(PARENT).toString();
+      geneId = getGeneId(transcriptId, OBJECT_TYPE_TRANSCRIPT);
+    }
+
+    return geneId;
+  }
+
+  /**
+   * Calls the Ensembl lookup REST endpoint and retrieves the 'species' for the
    * given identifier, or null if not found
    * 
    * @param identifier
    * @return
    */
-  public String getParent(String identifier)
+  public String getSpecies(String identifier)
+  {
+    String species = null;
+    JSONObject json = getResult(identifier, null);
+    if (json != null)
+    {
+      Object o = json.get(SPECIES);
+      if (o != null)
+      {
+        species = o.toString();
+      }
+    }
+    return species;
+  }
+
+  /**
+   * Calls the /lookup/id rest service and returns the response as a JSONObject,
+   * or null if any error
+   * 
+   * @param identifier
+   * @param objectType
+   *          (optional)
+   * @return
+   */
+  protected JSONObject getResult(String identifier, String objectType)
   {
     List<String> ids = Arrays.asList(new String[] { identifier });
 
     BufferedReader br = null;
     try
     {
-      URL url = getUrl(identifier);
+      URL url = getUrl(identifier, objectType);
+
       if (url != null)
       {
         br = getHttpResponse(url, ids);
       }
-      return (parseResponse(br));
-    } catch (IOException e)
+      return br == null ? null : (JSONObject) (new JSONParser().parse(br));
+    } catch (IOException | ParseException e)
     {
-      // ignore
+      System.err.println("Error parsing " + identifier + " lookup response "
+              + e.getMessage());
       return null;
     } finally
     {
@@ -155,26 +253,82 @@ public class EnsemblLookup extends EnsemblRestClient
   }
 
   /**
-   * Parses "Parent" from the JSON response and returns the value, or null if
-   * not found
+   * Calls the /lookup/id rest service for the given id, and if successful,
+   * parses and returns the gene's chromosomal coordinates
    * 
-   * @param br
+   * @param geneId
+   * @return
+   */
+  public GeneLociI getGeneLoci(String geneId)
+  {
+    return parseGeneLoci(getResult(geneId, OBJECT_TYPE_GENE));
+  }
+
+  /**
+   * Parses the /lookup/id response for species, asssembly_name,
+   * seq_region_name, start, end and returns an object that wraps them, or null
+   * if unsuccessful
+   * 
+   * @param json
    * @return
-   * @throws IOException
    */
-  protected String parseResponse(BufferedReader br) throws IOException
+  GeneLociI parseGeneLoci(JSONObject json)
   {
-    String parent = null;
-    JSONParser jp = new JSONParser();
+    if (json == null)
+    {
+      return null;
+    }
+
     try
     {
-      JSONObject val = (JSONObject) jp.parse(br);
-      parent = val.get("Parent").toString();
-    } catch (ParseException e)
+      final String species = json.get("species").toString();
+      final String assembly = json.get("assembly_name").toString();
+      final String chromosome = json.get("seq_region_name").toString();
+      String strand = json.get("strand").toString();
+      int start = Integer.parseInt(json.get("start").toString());
+      int end = Integer.parseInt(json.get("end").toString());
+      int fromEnd = end - start + 1;
+      boolean reverseStrand = "-1".equals(strand);
+      int toStart = reverseStrand ? end : start;
+      int toEnd = reverseStrand ? start : end;
+      List<int[]> fromRange = Collections.singletonList(new int[] { 1,
+          fromEnd });
+      List<int[]> toRange = Collections.singletonList(new int[] { toStart,
+          toEnd });
+      final MapList map = new MapList(fromRange, toRange, 1, 1);
+      return new GeneLociI()
+      {
+
+        @Override
+        public String getSpeciesId()
+        {
+          return species == null ? "" : species;
+        }
+
+        @Override
+        public String getAssemblyId()
+        {
+          return assembly;
+        }
+
+        @Override
+        public String getChromosomeId()
+        {
+          return chromosome;
+        }
+
+        @Override
+        public MapList getMap()
+        {
+          return map;
+        }
+      };
+    } catch (NullPointerException | NumberFormatException e)
     {
-      // ignore
+      Cache.log.error("Error looking up gene loci: " + e.getMessage());
+      e.printStackTrace();
     }
-    return parent;
+    return null;
   }
 
 }
diff --git a/src/jalview/ext/ensembl/EnsemblMap.java b/src/jalview/ext/ensembl/EnsemblMap.java
new file mode 100644 (file)
index 0000000..56657e0
--- /dev/null
@@ -0,0 +1,422 @@
+package jalview.ext.ensembl;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
+import jalview.util.MapList;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public class EnsemblMap extends EnsemblRestClient
+{
+  private static final String MAPPED = "mapped";
+
+  private static final String MAPPINGS = "mappings";
+
+  private static final String CDS = "cds";
+
+  private static final String CDNA = "cdna";
+
+  /**
+   * Default constructor (to use rest.ensembl.org)
+   */
+  public EnsemblMap()
+  {
+    super();
+  }
+
+  /**
+   * Constructor given the target domain to fetch data from
+   * 
+   * @param
+   */
+  public EnsemblMap(String domain)
+  {
+    super(domain);
+  }
+
+  @Override
+  public String getDbName()
+  {
+    return DBRefSource.ENSEMBL;
+  }
+
+  @Override
+  public AlignmentI getSequenceRecords(String queries) throws Exception
+  {
+    return null; // not used
+  }
+
+  /**
+   * Constructs a URL of the format <code>
+   * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
+   * </code>
+   * 
+   * @param species
+   * @param chromosome
+   * @param fromRef
+   * @param toRef
+   * @param startPos
+   * @param endPos
+   * @return
+   * @throws MalformedURLException
+   */
+  protected URL getAssemblyMapUrl(String species, String chromosome, String fromRef,
+          String toRef, int startPos, int endPos)
+          throws MalformedURLException
+  {
+    /*
+     * start-end might be reverse strand - present forwards to the service
+     */
+    boolean forward = startPos <= endPos;
+    int start = forward ? startPos : endPos;
+    int end = forward ? endPos : startPos;
+    String strand = forward ? "1" : "-1";
+    String url = String.format(
+            "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
+            getDomain(), species, fromRef, chromosome, start, end, strand,
+            toRef);
+    return new URL(url);
+  }
+
+  @Override
+  protected boolean useGetRequest()
+  {
+    return true;
+  }
+
+  @Override
+  protected String getRequestMimeType(boolean multipleIds)
+  {
+    return "application/json";
+  }
+
+  @Override
+  protected String getResponseMimeType()
+  {
+    return "application/json";
+  }
+
+  @Override
+  protected URL getUrl(List<String> ids) throws MalformedURLException
+  {
+    return null; // not used
+  }
+
+  /**
+   * Calls the REST /map service to get the chromosomal coordinates (start/end)
+   * in 'toRef' that corresponding to the (start/end) queryRange in 'fromRef'
+   * 
+   * @param species
+   * @param chromosome
+   * @param fromRef
+   * @param toRef
+   * @param queryRange
+   * @return
+   * @see http://rest.ensemblgenomes.org/documentation/info/assembly_map
+   */
+  public int[] getAssemblyMapping(String species, String chromosome,
+          String fromRef, String toRef, int[] queryRange)
+  {
+    URL url = null;
+    BufferedReader br = null;
+
+    try
+    {
+      url = getAssemblyMapUrl(species, chromosome, fromRef, toRef, queryRange[0],
+              queryRange[1]);
+      br = getHttpResponse(url, null);
+      return (parseAssemblyMappingResponse(br));
+    } catch (Throwable t)
+    {
+      System.out.println("Error calling " + url + ": " + t.getMessage());
+      return null;
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
+   * Parses the JSON response from the /map/&lt;species&gt;/ REST service. The
+   * format is (with some fields omitted)
+   * 
+   * <pre>
+   *  {"mappings": 
+   *    [{
+   *       "original": {"end":45109016,"start":45051610},
+   *       "mapped"  : {"end":43186384,"start":43128978} 
+   *  }] }
+   * </pre>
+   * 
+   * @param br
+   * @return
+   */
+  protected int[] parseAssemblyMappingResponse(BufferedReader br)
+  {
+    int[] result = null;
+    JSONParser jp = new JSONParser();
+
+    try
+    {
+      JSONObject parsed = (JSONObject) jp.parse(br);
+      JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
+
+      Iterator rvals = mappings.iterator();
+      while (rvals.hasNext())
+      {
+        // todo check for "mapped"
+        JSONObject val = (JSONObject) rvals.next();
+        JSONObject mapped = (JSONObject) val.get(MAPPED);
+        int start = Integer.parseInt(mapped.get("start").toString());
+        int end = Integer.parseInt(mapped.get("end").toString());
+        String strand = mapped.get("strand").toString();
+        if ("1".equals(strand))
+        {
+          result = new int[] { start, end };
+        }
+        else
+        {
+          result = new int[] { end, start };
+        }
+      }
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+    return result;
+  }
+
+  /**
+   * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
+   * returned chromosomal coordinates, or returns null if the call fails
+   * 
+   * @param division
+   *          e.g. Ensembl, EnsemblMetazoa
+   * @param accession
+   *          e.g. ENST00000592782, Y55B1AR.1.1
+   * @param start
+   * @param end
+   * @return
+   */
+  public GeneLociI getCdsMapping(String division, String accession,
+          int start, int end)
+  {
+    return getIdMapping(division, accession, start, end, CDS);
+  }
+
+  /**
+   * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
+   * returned chromosomal coordinates, or returns null if the call fails
+   * 
+   * @param division
+   *          e.g. Ensembl, EnsemblMetazoa
+   * @param accession
+   *          e.g. ENST00000592782, Y55B1AR.1.1
+   * @param start
+   * @param end
+   * @return
+   */
+  public GeneLociI getCdnaMapping(String division, String accession,
+          int start, int end)
+  {
+    return getIdMapping(division, accession, start, end, CDNA);
+  }
+
+  GeneLociI getIdMapping(String division, String accession, int start,
+          int end, String cdsOrCdna)
+  {
+    URL url = null;
+    BufferedReader br = null;
+
+    try
+    {
+      String domain = new EnsemblInfo().getDomain(division);
+      if (domain != null)
+      {
+        url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
+        br = getHttpResponse(url, null);
+        return (parseIdMappingResponse(br, accession, domain));
+      }
+      return null;
+    } catch (Throwable t)
+    {
+      System.out.println("Error calling " + url + ": " + t.getMessage());
+      return null;
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+    }
+  }
+
+  /**
+   * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
+   * REST call is to either ensembl or ensemblgenomes, as determined from the
+   * division, e.g. Ensembl or EnsemblProtists.
+   * 
+   * @param domain
+   * @param accession
+   * @param start
+   * @param end
+   * @param cdsOrCdna
+   * @return
+   * @throws MalformedURLException
+   */
+  URL getIdMapUrl(String domain, String accession, int start, int end,
+          String cdsOrCdna) throws MalformedURLException
+  {
+    String url = String
+            .format("%s/map/%s/%s/%d..%d?include_original_region=1&content-type=application/json",
+                    domain, cdsOrCdna, accession, start, end);
+    return new URL(url);
+  }
+
+  /**
+   * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
+   * format is
+   * 
+   * <pre>
+   * {"mappings":
+   *   [
+   *    {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
+   *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
+   *    {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
+   *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
+   *   ]
+   * }
+   * </pre>
+   * 
+   * @param br
+   * @param accession
+   * @param domain
+   * @return
+   */
+  GeneLociI parseIdMappingResponse(BufferedReader br, String accession,
+          String domain)
+  {
+    JSONParser jp = new JSONParser();
+
+    try
+    {
+      JSONObject parsed = (JSONObject) jp.parse(br);
+      JSONArray mappings = (JSONArray) parsed.get(MAPPINGS);
+
+      Iterator rvals = mappings.iterator();
+      String assembly = null;
+      String chromosome = null;
+      int fromEnd = 0;
+      List<int[]> regions = new ArrayList<>();
+
+      while (rvals.hasNext())
+      {
+        JSONObject val = (JSONObject) rvals.next();
+        JSONObject original = (JSONObject) val.get("original");
+        fromEnd = Integer.parseInt(original.get("end").toString());
+
+        JSONObject mapped = (JSONObject) val.get(MAPPED);
+        int start = Integer.parseInt(mapped.get("start").toString());
+        int end = Integer.parseInt(mapped.get("end").toString());
+        String ass = mapped.get("assembly_name").toString();
+        if (assembly != null && !assembly.equals(ass))
+        {
+          System.err
+                  .println("EnsemblMap found multiple assemblies - can't resolve");
+          return null;
+        }
+        assembly = ass;
+        String chr = mapped.get("seq_region_name").toString();
+        if (chromosome != null && !chromosome.equals(chr))
+        {
+          System.err
+                  .println("EnsemblMap found multiple chromosomes - can't resolve");
+          return null;
+        }
+        chromosome = chr;
+        String strand = mapped.get("strand").toString();
+        if ("-1".equals(strand))
+        {
+          regions.add(new int[] { end, start });
+        }
+        else
+        {
+          regions.add(new int[] { start, end });
+        }
+      }
+
+      /*
+       * processed all mapped regions on chromosome, assemble the result,
+       * having first fetched the species id for the accession
+       */
+      final String species = new EnsemblLookup(domain)
+              .getSpecies(accession);
+      final String as = assembly;
+      final String chr = chromosome;
+      List<int[]> fromRange = Collections.singletonList(new int[] { 1,
+          fromEnd });
+      final MapList map = new MapList(fromRange, regions, 1, 1);
+      return new GeneLociI()
+      {
+
+        @Override
+        public String getSpeciesId()
+        {
+          return species == null ? "" : species;
+        }
+
+        @Override
+        public String getAssemblyId()
+        {
+          return as;
+        }
+
+        @Override
+        public String getChromosomeId()
+        {
+          return chr;
+        }
+
+        @Override
+        public MapList getMap()
+        {
+          return map;
+        }
+      };
+    } catch (IOException | ParseException | NumberFormatException e)
+    {
+      // ignore
+    }
+
+    return null;
+  }
+
+}
index 1554a0b..99006aa 100644 (file)
@@ -23,8 +23,6 @@ package jalview.ext.ensembl;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 
-import java.util.List;
-
 import com.stevesoft.pat.Regex;
 
 /**
index ad6c70c..9dea886 100644 (file)
@@ -31,6 +31,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
+import java.net.ProtocolException;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.List;
@@ -42,8 +43,6 @@ import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
 
-import com.stevesoft.pat.Regex;
-
 /**
  * Base class for Ensembl REST service clients
  * 
@@ -55,19 +54,25 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   private static final int CONNECT_TIMEOUT_MS = 10 * 1000; // 10 seconds
 
+  private static final int MAX_RETRIES = 3;
+
+  private static final int HTTP_OK = 200;
+
+  private static final int HTTP_OVERLOAD = 429;
+
   /*
    * update these constants when Jalview has been checked / updated for
    * changes to Ensembl REST API (ref JAL-2105)
    * @see https://github.com/Ensembl/ensembl-rest/wiki/Change-log
    * @see http://rest.ensembl.org/info/rest?content-type=application/json
    */
-  private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "5.0";
+  private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "6.0";
 
-  private static final String LATEST_ENSEMBL_REST_VERSION = "5.0";
+  private static final String LATEST_ENSEMBL_REST_VERSION = "6.1";
 
   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
 
-  private static Map<String, EnsemblInfo> domainData;
+  private static Map<String, EnsemblData> domainData;
 
   // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
   private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
@@ -76,22 +81,15 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
 
   private final static long VERSION_RETEST_INTERVAL = 1000L * 3600; // 1 hr
 
-  private static final Regex PROTEIN_REGEX = new Regex(
-          "(ENS)([A-Z]{3}|)P[0-9]{11}$");
-
-  private static final Regex TRANSCRIPT_REGEX = new Regex(
-          "(ENS)([A-Z]{3}|)T[0-9]{11}$");
-
-  private static final Regex GENE_REGEX = new Regex(
-          "(ENS)([A-Z]{3}|)G[0-9]{11}$");
+  protected static final String CONTENT_TYPE_JSON = "?content-type=application/json";
 
   static
   {
-    domainData = new HashMap<String, EnsemblInfo>();
-    domainData.put(ENSEMBL_REST,
-            new EnsemblInfo(ENSEMBL_REST, LATEST_ENSEMBL_REST_VERSION));
-    domainData.put(ENSEMBL_GENOMES_REST, new EnsemblInfo(
-            ENSEMBL_GENOMES_REST, LATEST_ENSEMBLGENOMES_REST_VERSION));
+    domainData = new HashMap<>();
+    domainData.put(DEFAULT_ENSEMBL_BASEURL,
+            new EnsemblData(DEFAULT_ENSEMBL_BASEURL, LATEST_ENSEMBL_REST_VERSION));
+    domainData.put(DEFAULT_ENSEMBL_GENOMES_BASEURL, new EnsemblData(
+            DEFAULT_ENSEMBL_GENOMES_BASEURL, LATEST_ENSEMBLGENOMES_REST_VERSION));
   }
 
   protected volatile boolean inProgress = false;
@@ -101,7 +99,21 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    */
   public EnsemblRestClient()
   {
-    this(ENSEMBL_REST);
+    super();
+
+    /*
+     * initialise domain info lazily
+     */
+    if (!domainData.containsKey(ensemblDomain))
+    {
+      domainData.put(ensemblDomain,
+              new EnsemblData(ensemblDomain, LATEST_ENSEMBL_REST_VERSION));
+    }
+    if (!domainData.containsKey(ensemblGenomesDomain))
+    {
+      domainData.put(ensemblGenomesDomain, new EnsemblData(
+              ensemblGenomesDomain, LATEST_ENSEMBLGENOMES_REST_VERSION));
+    }
   }
 
   /**
@@ -114,42 +126,6 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
     setDomain(d);
   }
 
-  /**
-   * Answers true if the query matches the regular expression pattern for an
-   * Ensembl transcript stable identifier
-   * 
-   * @param query
-   * @return
-   */
-  public boolean isTranscriptIdentifier(String query)
-  {
-    return query == null ? false : TRANSCRIPT_REGEX.search(query);
-  }
-
-  /**
-   * Answers true if the query matches the regular expression pattern for an
-   * Ensembl protein stable identifier
-   * 
-   * @param query
-   * @return
-   */
-  public boolean isProteinIdentifier(String query)
-  {
-    return query == null ? false : PROTEIN_REGEX.search(query);
-  }
-
-  /**
-   * Answers true if the query matches the regular expression pattern for an
-   * Ensembl gene stable identifier
-   * 
-   * @param query
-   * @return
-   */
-  public boolean isGeneIdentifier(String query)
-  {
-    return query == null ? false : GENE_REGEX.search(query);
-  }
-
   @Override
   public boolean queryInProgress()
   {
@@ -204,21 +180,26 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    * @see http://rest.ensembl.org/documentation/info/ping
    * @return
    */
-  private boolean checkEnsembl()
+  boolean checkEnsembl()
   {
     BufferedReader br = null;
+    String pingUrl = getDomain() + "/info/ping" + CONTENT_TYPE_JSON;
     try
     {
       // note this format works for both ensembl and ensemblgenomes
       // info/ping.json works for ensembl only (March 2016)
-      URL ping = new URL(
-              getDomain() + "/info/ping?content-type=application/json");
+      URL ping = new URL(pingUrl);
 
       /*
        * expect {"ping":1} if ok
        * if ping takes more than 2 seconds to respond, treat as if unavailable
        */
       br = getHttpResponse(ping, null, 2 * 1000);
+      if (br == null)
+      {
+        // error reponse status
+        return false;
+      }
       JSONParser jp = new JSONParser();
       JSONObject val = (JSONObject) jp.parse(br);
       String pingString = val.get("ping").toString();
@@ -226,7 +207,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
     } catch (Throwable t)
     {
       System.err.println(
-              "Error connecting to " + PING_URL + ": " + t.getMessage());
+              "Error connecting to " + pingUrl + ": " + t.getMessage());
     } finally
     {
       if (br != null)
@@ -281,7 +262,7 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   }
 
   /**
-   * Writes the HTTP request and gets the response as a reader.
+   * Sends the HTTP request and gets the response as a reader
    * 
    * @param url
    * @param ids
@@ -295,7 +276,56 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   protected BufferedReader getHttpResponse(URL url, List<String> ids,
           int readTimeout) throws IOException
   {
-    // long now = System.currentTimeMillis();
+    int retriesLeft = MAX_RETRIES;
+    HttpURLConnection connection = null;
+    int responseCode = 0;
+
+    while (retriesLeft > 0)
+    {
+      connection = tryConnection(url, ids, readTimeout);
+      responseCode = connection.getResponseCode();
+      if (responseCode == HTTP_OVERLOAD) // 429
+      {
+        retriesLeft--;
+        checkRetryAfter(connection);
+      }
+      else
+      {
+        retriesLeft = 0;
+      }
+    }
+    if (responseCode != HTTP_OK) // 200
+    {
+      /*
+       * note: a GET request for an invalid id returns an error code e.g. 415
+       * but POST request returns 200 and an empty Fasta response 
+       */
+      System.err.println("Response code " + responseCode + " for " + url);
+      return null;
+    }
+
+    InputStream response = connection.getInputStream();
+
+    // System.out.println(getClass().getName() + " took "
+    // + (System.currentTimeMillis() - now) + "ms to fetch");
+
+    BufferedReader reader = null;
+    reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
+    return reader;
+  }
+
+  /**
+   * @param url
+   * @param ids
+   * @param readTimeout
+   * @return
+   * @throws IOException
+   * @throws ProtocolException
+   */
+  protected HttpURLConnection tryConnection(URL url, List<String> ids,
+          int readTimeout) throws IOException, ProtocolException
+  {
+    // System.out.println(System.currentTimeMillis() + " " + url);
     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 
     /*
@@ -320,77 +350,40 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
     {
       writePostBody(connection, ids);
     }
-
-    int responseCode = connection.getResponseCode();
-
-    if (responseCode != 200)
-    {
-      /*
-       * note: a GET request for an invalid id returns an error code e.g. 415
-       * but POST request returns 200 and an empty Fasta response 
-       */
-      System.err.println("Response code " + responseCode + " for " + url);
-      return null;
-    }
-    // get content
-    InputStream response = connection.getInputStream();
-
-    // System.out.println(getClass().getName() + " took "
-    // + (System.currentTimeMillis() - now) + "ms to fetch");
-
-    checkRateLimits(connection);
-
-    BufferedReader reader = null;
-    reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
-    return reader;
+    return connection;
   }
 
   /**
-   * Inspect response headers for any sign of server overload and respect any
-   * 'retry-after' directive
+   * Inspects response headers for a 'retry-after' directive, and waits for the
+   * directed period (if less than 10 seconds)
    * 
    * @see https://github.com/Ensembl/ensembl-rest/wiki/Rate-Limits
    * @param connection
    */
-  void checkRateLimits(HttpURLConnection connection)
+  void checkRetryAfter(HttpURLConnection connection)
   {
-    // number of requests allowed per time interval:
-    String limit = connection.getHeaderField("X-RateLimit-Limit");
-    // length of quota time interval in seconds:
-    // String period = connection.getHeaderField("X-RateLimit-Period");
-    // seconds remaining until usage quota is reset:
-    String reset = connection.getHeaderField("X-RateLimit-Reset");
-    // number of requests remaining from quota for current period:
-    String remaining = connection.getHeaderField("X-RateLimit-Remaining");
-    // number of seconds to wait before retrying (if remaining == 0)
     String retryDelay = connection.getHeaderField("Retry-After");
 
     // to test:
     // retryDelay = "5";
 
-    EnsemblInfo info = domainData.get(getDomain());
     if (retryDelay != null)
     {
-      System.err.println("Ensembl REST service rate limit exceeded, wait "
-              + retryDelay + " seconds before retrying");
       try
       {
-        info.retryAfter = System.currentTimeMillis()
-                + (1000 * Integer.valueOf(retryDelay));
-      } catch (NumberFormatException e)
+        int retrySecs = Integer.valueOf(retryDelay);
+        if (retrySecs > 0 && retrySecs < 10)
+        {
+          System.err
+                  .println("Ensembl REST service rate limit exceeded, waiting "
+                          + retryDelay + " seconds before retrying");
+          Thread.sleep(1000 * retrySecs);
+        }
+      } catch (NumberFormatException | InterruptedException e)
       {
-        System.err
-                .println("Unexpected value for Retry-After: " + retryDelay);
+        System.err.println("Error handling Retry-After: " + e.getMessage());
       }
     }
-    else
-    {
-      info.retryAfter = 0;
-      // debug:
-      // System.out.println(String.format(
-      // "%s Ensembl requests remaining of %s (reset in %ss)",
-      // remaining, limit, reset));
-    }
   }
 
   /**
@@ -403,25 +396,11 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    */
   protected boolean isEnsemblAvailable()
   {
-    EnsemblInfo info = domainData.get(getDomain());
+    EnsemblData info = domainData.get(getDomain());
 
     long now = System.currentTimeMillis();
 
     /*
-     * check if we are waiting for 'Retry-After' to expire
-     */
-    if (info.retryAfter > now)
-    {
-      System.err.println("Still " + (1 + (info.retryAfter - now) / 1000)
-              + " secs to wait before retrying Ensembl");
-      return false;
-    }
-    else
-    {
-      info.retryAfter = 0;
-    }
-
-    /*
      * recheck if Ensembl is up if it was down, or the recheck period has elapsed
      */
     boolean retestAvailability = (now
@@ -491,15 +470,18 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
    */
   private void checkEnsemblRestVersion()
   {
-    EnsemblInfo info = domainData.get(getDomain());
+    EnsemblData info = domainData.get(getDomain());
 
     JSONParser jp = new JSONParser();
     URL url = null;
     try
     {
-      url = new URL(
-              getDomain() + "/info/rest?content-type=application/json");
+      url = new URL(getDomain() + "/info/rest" + CONTENT_TYPE_JSON);
       BufferedReader br = getHttpResponse(url, null);
+      if (br == null)
+      {
+        return;
+      }
       JSONObject val = (JSONObject) jp.parse(br);
       String version = val.get("release").toString();
       String majorVersion = version.substring(0, version.indexOf("."));
@@ -558,18 +540,35 @@ abstract class EnsemblRestClient extends EnsemblSequenceFetcher
   {
     JSONParser jp = new JSONParser();
     URL url = null;
+    BufferedReader br = null;
+
     try
     {
-      url = new URL(
-              getDomain() + "/info/data?content-type=application/json");
-      BufferedReader br = getHttpResponse(url, null);
-      JSONObject val = (JSONObject) jp.parse(br);
-      JSONArray versions = (JSONArray) val.get("releases");
-      domainData.get(getDomain()).dataVersion = versions.get(0).toString();
+      url = new URL(getDomain() + "/info/data" + CONTENT_TYPE_JSON);
+      br = getHttpResponse(url, null);
+      if (br != null)
+      {
+        JSONObject val = (JSONObject) jp.parse(br);
+        JSONArray versions = (JSONArray) val.get("releases");
+        domainData.get(getDomain()).dataVersion = versions.get(0)
+                .toString();
+      }
     } catch (Throwable t)
     {
       System.err.println(
               "Error checking Ensembl data version: " + t.getMessage());
+    } finally
+    {
+      if (br != null)
+      {
+        try
+        {
+          br.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
     }
   }
 
index 577111e..9229379 100644 (file)
@@ -34,6 +34,7 @@ import jalview.datamodel.features.SequenceFeatures;
 import jalview.exceptions.JalviewException;
 import jalview.io.FastaFile;
 import jalview.io.FileParse;
+import jalview.io.gff.Gff3Helper;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.util.Comparison;
@@ -48,8 +49,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Base class for Ensembl sequence fetchers
@@ -59,12 +58,6 @@ import java.util.Map.Entry;
  */
 public abstract class EnsemblSeqProxy extends EnsemblRestClient
 {
-  private static final String ALLELES = "alleles";
-
-  protected static final String PARENT = "Parent";
-
-  protected static final String ID = "ID";
-
   protected static final String NAME = "Name";
 
   protected static final String DESCRIPTION = "description";
@@ -209,7 +202,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     try
     {
       /*
-       * get 'dummy' genomic sequence with exon, cds and variation features
+       * get 'dummy' genomic sequence with gene, transcript, 
+       * exon, cds and variation features
        */
       SequenceI genomicSequence = null;
       EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain());
@@ -225,7 +219,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
         /*
          * transfer features to the query sequence
          */
-        SequenceI querySeq = alignment.findName(accId);
+        SequenceI querySeq = alignment.findName(accId, true);
         if (transferFeatures(accId, genomicSequence, querySeq))
         {
 
@@ -472,14 +466,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     urlstring.append("?type=").append(getSourceEnsemblType().getType());
     urlstring.append(("&Accept=text/x-fasta"));
 
-    Map<String, String> params = getAdditionalParameters();
-    if (params != null)
+    String objectType = getObjectType();
+    if (objectType != null)
     {
-      for (Entry<String, String> entry : params.entrySet())
-      {
-        urlstring.append("&").append(entry.getKey()).append("=")
-                .append(entry.getValue());
-      }
+      urlstring.append("&").append(OBJECT_TYPE).append("=")
+              .append(objectType);
     }
 
     URL url = new URL(urlstring.toString());
@@ -487,11 +478,11 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   }
 
   /**
-   * Override this method to add any additional x=y URL parameters needed
+   * Override this method to specify object_type request parameter
    * 
    * @return
    */
-  protected Map<String, String> getAdditionalParameters()
+  protected String getObjectType()
   {
     return null;
   }
@@ -560,7 +551,6 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
           String accId, int start)
   {
-    // SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
     List<SequenceFeature> sfs = sourceSequence.getFeatures()
             .getPositionalFeatures();
     if (sfs.isEmpty())
@@ -572,7 +562,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
      * generously initial size for number of cds regions
      * (worst case titin Q8WZ42 has c. 313 exons)
      */
-    List<int[]> regions = new ArrayList<int[]>(100);
+    List<int[]> regions = new ArrayList<>(100);
     int mappedLength = 0;
     int direction = 1; // forward
     boolean directionSet = false;
@@ -717,7 +707,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
    */
   static void reverseComplementAlleles(SequenceFeature sf)
   {
-    final String alleles = (String) sf.getValue(ALLELES);
+    final String alleles = (String) sf.getValue(Gff3Helper.ALLELES);
     if (alleles == null)
     {
       return;
@@ -728,7 +718,7 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
       reverseComplementAllele(complement, allele);
     }
     String comp = complement.toString();
-    sf.setValue(ALLELES, comp);
+    sf.setValue(Gff3Helper.ALLELES, comp);
     sf.setDescription(comp);
 
     /*
@@ -738,7 +728,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
     String atts = sf.getAttributes();
     if (atts != null)
     {
-      atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
+      atts = atts.replace(Gff3Helper.ALLELES + "=" + alleles,
+              Gff3Helper.ALLELES + "=" + comp);
       sf.setAttributes(atts);
     }
   }
@@ -871,7 +862,8 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   {
     String parent = (String) sf.getValue(PARENT);
     // using contains to allow for prefix "gene:", "transcript:" etc
-    if (parent != null && !parent.contains(identifier))
+    if (parent != null
+            && !parent.toUpperCase().contains(identifier.toUpperCase()))
     {
       // this genomic feature belongs to a different transcript
       return false;
@@ -899,14 +891,14 @@ public abstract class EnsemblSeqProxy extends EnsemblRestClient
   protected List<SequenceFeature> findFeatures(SequenceI sequence,
           String term, String parentId)
   {
-    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> result = new ArrayList<>();
 
     List<SequenceFeature> sfs = sequence.getFeatures()
             .getFeaturesByOntology(term);
     for (SequenceFeature sf : sfs)
     {
       String parent = (String) sf.getValue(PARENT);
-      if (parent != null && parent.equals(parentId))
+      if (parent != null && parent.equalsIgnoreCase(parentId))
       {
         result.add(sf);
       }
index 598dba1..9e3fef4 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.ext.ensembl;
 
+import jalview.bin.Cache;
 import jalview.datamodel.DBRefSource;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
@@ -32,6 +33,16 @@ import com.stevesoft.pat.Regex;
  */
 abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
 {
+  // domain properties lookup keys:
+  protected static final String ENSEMBL_BASEURL = "ENSEMBL_BASEURL";
+
+  protected static final String ENSEMBL_GENOMES_BASEURL = "ENSEMBL_GENOMES_BASEURL";
+
+  // domain properties default values:
+  protected static final String DEFAULT_ENSEMBL_BASEURL = "https://rest.ensembl.org";
+
+  protected static final String DEFAULT_ENSEMBL_GENOMES_BASEURL = "https://rest.ensemblgenomes.org";
+
   /*
    * accepts ENSG/T/E/P with 11 digits
    * or ENSMUSP or similar for other species
@@ -41,9 +52,21 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
           "(ENS([A-Z]{3}|)[GTEP]{1}[0-9]{11}$)" + "|"
                   + "(CCDS[0-9.]{3,}$)");
 
-  protected static final String ENSEMBL_GENOMES_REST = "http://rest.ensemblgenomes.org";
+  protected final String ensemblGenomesDomain;
+
+  protected final String ensemblDomain;
+
+  protected static final String OBJECT_TYPE_TRANSLATION = "Translation";
+
+  protected static final String OBJECT_TYPE_TRANSCRIPT = "Transcript";
+
+  protected static final String OBJECT_TYPE_GENE = "Gene";
 
-  protected static final String ENSEMBL_REST = "http://rest.ensembl.org";
+  protected static final String PARENT = "Parent";
+
+  protected static final String JSON_ID = "id";
+
+  protected static final String OBJECT_TYPE = "object_type";
 
   /*
    * possible values for the 'feature' parameter of the /overlap REST service
@@ -56,13 +79,29 @@ abstract class EnsemblSequenceFetcher extends DbSourceProxyImpl
     constrained, regulatory
   }
 
-  private String domain = ENSEMBL_REST;
+  private String domain;
+
+  /**
+   * Constructor
+   */
+  public EnsemblSequenceFetcher()
+  {
+    /*
+     * the default domain names may be overridden in .jalview_properties;
+     * this allows an easy change from http to https in future if needed
+     */
+    ensemblDomain = Cache.getDefault(ENSEMBL_BASEURL,
+            DEFAULT_ENSEMBL_BASEURL);
+    ensemblGenomesDomain = Cache.getDefault(ENSEMBL_GENOMES_BASEURL,
+            DEFAULT_ENSEMBL_GENOMES_BASEURL);
+    domain = ensemblDomain;
+  }
 
   @Override
   public String getDbSource()
   {
     // NB ensure Uniprot xrefs are canonicalised from "Ensembl" to "ENSEMBL"
-    if (ENSEMBL_GENOMES_REST.equals(getDomain()))
+    if (ensemblGenomesDomain.equals(getDomain()))
     {
       return DBRefSource.ENSEMBLGENOMES;
     }
index 9f86731..40d6cad 100644 (file)
@@ -42,6 +42,8 @@ import org.json.simple.parser.ParseException;
  */
 public class EnsemblSymbol extends EnsemblXref
 {
+  private static final String GENE = "gene";
+  private static final String TYPE = "type";
   /**
    * Constructor given the target domain to fetch data from
    * 
@@ -73,8 +75,9 @@ public class EnsemblSymbol extends EnsemblXref
       while (rvals.hasNext())
       {
         JSONObject val = (JSONObject) rvals.next();
-        String id = val.get("id").toString();
-        if (id != null && isGeneIdentifier(id))
+        String id = val.get(JSON_ID).toString();
+        String type = val.get(TYPE).toString();
+        if (id != null && GENE.equals(type))
         {
           result = id;
           break;
@@ -87,12 +90,31 @@ public class EnsemblSymbol extends EnsemblXref
     return result;
   }
 
-  protected URL getUrl(String id, Species species)
+  /**
+   * Constructs the URL for the REST symbol endpoint
+   * 
+   * @param id
+   *          the accession id (Ensembl or external)
+   * @param species
+   *          a species name recognisable by Ensembl
+   * @param type
+   *          an optional type to filter the response (gene, transcript,
+   *          translation)
+   * @return
+   */
+  protected URL getUrl(String id, Species species, String... type)
   {
-    String url = getDomain() + "/xrefs/symbol/" + species.toString() + "/"
-            + id + "?content-type=application/json";
+    StringBuilder sb = new StringBuilder();
+    sb.append(getDomain()).append("/xrefs/symbol/")
+            .append(species.toString()).append("/").append(id)
+            .append(CONTENT_TYPE_JSON);
+    for (String t : type)
+    {
+      sb.append("&object_type=").append(t);
+    }
     try
     {
+      String url = sb.toString();
       return new URL(url);
     } catch (MalformedURLException e)
     {
@@ -107,7 +129,7 @@ public class EnsemblSymbol extends EnsemblXref
    * @param identifier
    * @return
    */
-  public List<String> getIds(String identifier)
+  public List<String> getGeneIds(String identifier)
   {
     List<String> result = new ArrayList<String>();
     List<String> ids = new ArrayList<String>();
@@ -119,19 +141,19 @@ public class EnsemblSymbol extends EnsemblXref
     {
       for (String query : queries)
       {
-        for (Species taxon : Species.values())
+        for (Species taxon : Species.getModelOrganisms())
         {
-          if (taxon.isModelOrganism())
+          URL url = getUrl(query, taxon, GENE);
+          if (url != null)
           {
-            URL url = getUrl(query, taxon);
-            if (url != null)
-            {
-              br = getHttpResponse(url, ids);
-            }
-            String geneId = parseSymbolResponse(br);
-            if (geneId != null)
+            br = getHttpResponse(url, ids);
+            if (br != null)
             {
-              result.add(geneId);
+              String geneId = parseSymbolResponse(br);
+              if (geneId != null && !result.contains(geneId))
+              {
+                result.add(geneId);
+              }
             }
           }
         }
index c0b00b1..27c448e 100644 (file)
@@ -124,8 +124,11 @@ class EnsemblXref extends EnsemblRestClient
       if (url != null)
       {
         br = getHttpResponse(url, ids);
+        if (br != null)
+        {
+          result = parseResponse(br);
+        }
       }
-      return (parseResponse(br));
     } catch (IOException e)
     {
       // ignore
@@ -168,16 +171,13 @@ class EnsemblXref extends EnsemblRestClient
       while (rvals.hasNext())
       {
         JSONObject val = (JSONObject) rvals.next();
-        String dbName = val.get("dbname").toString();
-        if (dbName.equals(GO_GENE_ONTOLOGY))
-        {
-          continue;
-        }
+        String db = val.get("dbname").toString();
         String id = val.get("primary_id").toString();
-        if (dbName != null && id != null)
+        if (db != null && id != null
+                && !GO_GENE_ONTOLOGY.equals(db))
         {
-          dbName = DBRefUtils.getCanonicalName(dbName);
-          DBRefEntry dbref = new DBRefEntry(dbName, getXRefVersion(), id);
+          db = DBRefUtils.getCanonicalName(db);
+          DBRefEntry dbref = new DBRefEntry(db, getXRefVersion(), id);
           result.add(dbref);
         }
       }
@@ -211,7 +211,7 @@ class EnsemblXref extends EnsemblRestClient
   protected URL getUrl(String identifier)
   {
     String url = getDomain() + "/xrefs/id/" + identifier
-            + "?content-type=application/json&all_levels=1";
+            + CONTENT_TYPE_JSON + "&all_levels=1";
     try
     {
       return new URL(url);
index af01225..cc5465e 100644 (file)
@@ -20,6 +20,9 @@
  */
 package jalview.ext.ensembl;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Selected species identifiers used by Ensembl
  * 
@@ -38,6 +41,18 @@ enum Species
   chimpanzee(false), cat(false), zebrafish(true), chicken(true),
   dmelanogaster(true);
 
+  static Set<Species> modelOrganisms = new HashSet<>();
+
+  static
+  {
+    for (Species s : values())
+    {
+      if (s.isModelOrganism())
+      {
+        modelOrganisms.add(s);
+      }
+    }
+  }
   boolean modelOrganism;
 
   private Species(boolean model)
@@ -49,4 +64,9 @@ enum Species
   {
     return modelOrganism;
   }
+
+  public static Set<Species> getModelOrganisms()
+  {
+    return modelOrganisms;
+  }
 }
index 37ce625..73d1674 100644 (file)
  */
 package jalview.ext.htsjdk;
 
-import htsjdk.samtools.SAMSequenceDictionary;
-import htsjdk.samtools.SAMSequenceRecord;
-import htsjdk.samtools.reference.ReferenceSequence;
-import htsjdk.samtools.reference.ReferenceSequenceFile;
-import htsjdk.samtools.reference.ReferenceSequenceFileFactory;
-import htsjdk.samtools.util.StringUtil;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
 
 import java.io.File;
+import java.io.IOException;
 import java.math.BigInteger;
+import java.nio.file.Path;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
@@ -38,6 +34,15 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import htsjdk.samtools.SAMException;
+import htsjdk.samtools.SAMSequenceDictionary;
+import htsjdk.samtools.SAMSequenceRecord;
+import htsjdk.samtools.reference.FastaSequenceIndexCreator;
+import htsjdk.samtools.reference.ReferenceSequence;
+import htsjdk.samtools.reference.ReferenceSequenceFile;
+import htsjdk.samtools.reference.ReferenceSequenceFileFactory;
+import htsjdk.samtools.util.StringUtil;
+
 /**
  * a source of sequence data accessed via the HTSJDK
  * 
@@ -46,14 +51,25 @@ import java.util.Set;
  */
 public class HtsContigDb
 {
-
   private String name;
 
   private File dbLocation;
 
   private htsjdk.samtools.reference.ReferenceSequenceFile refFile = null;
 
-  public HtsContigDb(String name, File descriptor) throws Exception
+  public static void createFastaSequenceIndex(Path path, boolean overwrite)
+          throws IOException
+  {
+    try
+    {
+      FastaSequenceIndexCreator.create(path, overwrite);
+    } catch (SAMException e)
+    {
+      throw new IOException(e.getMessage());
+    }
+  }
+
+  public HtsContigDb(String name, File descriptor)
   {
     if (descriptor.isFile())
     {
@@ -63,7 +79,21 @@ public class HtsContigDb
     initSource();
   }
 
-  private void initSource() throws Exception
+  public void close()
+  {
+    if (refFile != null)
+    {
+      try
+      {
+        refFile.close();
+      } catch (IOException e)
+      {
+        // ignore
+      }
+    }
+  }
+
+  private void initSource()
   {
     if (refFile != null)
     {
@@ -142,8 +172,8 @@ public class HtsContigDb
     final ReferenceSequenceFile refSeqFile = ReferenceSequenceFileFactory
             .getReferenceSequenceFile(f, truncate);
     ReferenceSequence refSeq;
-    List<SAMSequenceRecord> ret = new ArrayList<SAMSequenceRecord>();
-    Set<String> sequenceNames = new HashSet<String>();
+    List<SAMSequenceRecord> ret = new ArrayList<>();
+    Set<String> sequenceNames = new HashSet<>();
     for (int numSequences = 0; (refSeq = refSeqFile
             .nextSequence()) != null; ++numSequences)
     {
@@ -220,14 +250,29 @@ public class HtsContigDb
 
   // ///// end of hts bits.
 
-  SequenceI getSequenceProxy(String id)
+  /**
+   * Reads the contig with the given id and returns as a Jalview SequenceI object.
+   * Note the database must be indexed for this operation to succeed.
+   * 
+   * @param id
+   * @return
+   */
+  public SequenceI getSequenceProxy(String id)
   {
-    if (!isValid())
+    if (!isValid() || !refFile.isIndexed())
     {
+      System.err.println(
+              "Cannot read contig as file is invalid or not indexed");
       return null;
     }
 
     ReferenceSequence sseq = refFile.getSequence(id);
     return new Sequence(sseq.getName(), new String(sseq.getBases()));
   }
+
+  public boolean isIndexed()
+  {
+    return refFile != null && refFile.isIndexed();
+  }
+
 }
diff --git a/src/jalview/ext/htsjdk/VCFReader.java b/src/jalview/ext/htsjdk/VCFReader.java
new file mode 100644 (file)
index 0000000..14c057f
--- /dev/null
@@ -0,0 +1,214 @@
+package jalview.ext.htsjdk;
+
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFFileReader;
+import htsjdk.variant.vcf.VCFHeader;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A thin wrapper for htsjdk classes to read either plain, or compressed, or
+ * compressed and indexed VCF files
+ */
+public class VCFReader implements Closeable, Iterable<VariantContext>
+{
+  private static final String GZ = "gz";
+
+  private static final String TBI_EXTENSION = ".tbi";
+
+  private boolean indexed;
+
+  private VCFFileReader reader;
+
+  /**
+   * Constructor given a raw or compressed VCF file or a (tabix) index file
+   * <p>
+   * For now, file type is inferred from its suffix: .gz or .bgz for compressed
+   * data, .tbi for an index file, anything else is assumed to be plain text
+   * VCF.
+   * 
+   * @param f
+   * @throws IOException
+   */
+  public VCFReader(String filePath) throws IOException
+  {
+    if (filePath.endsWith(GZ))
+    {
+      if (new File(filePath + TBI_EXTENSION).exists())
+      {
+        indexed = true;
+      }
+    }
+    else if (filePath.endsWith(TBI_EXTENSION))
+    {
+      indexed = true;
+      filePath = filePath.substring(0, filePath.length() - 4);
+    }
+
+    reader = new VCFFileReader(new File(filePath), indexed);
+  }
+
+  @Override
+  public void close() throws IOException
+  {
+    if (reader != null)
+    {
+      reader.close();
+    }
+  }
+
+  /**
+   * Returns an iterator over VCF variants in the file. The client should call
+   * close() on the iterator when finished with it.
+   */
+  @Override
+  public CloseableIterator<VariantContext> iterator()
+  {
+    return reader == null ? null : reader.iterator();
+  }
+
+  /**
+   * Queries for records overlapping the region specified. Note that this method
+   * is performant if the VCF file is indexed, and may be very slow if it is
+   * not.
+   * <p>
+   * Client code should call close() on the iterator when finished with it.
+   * 
+   * @param chrom
+   *          the chromosome to query
+   * @param start
+   *          query interval start
+   * @param end
+   *          query interval end
+   * @return
+   */
+  public CloseableIterator<VariantContext> query(final String chrom,
+          final int start, final int end)
+  {
+   if (reader == null) {
+     return null;
+   }
+    if (indexed)
+    {
+      return reader.query(chrom, start, end);
+    }
+    else
+    {
+      return queryUnindexed(chrom, start, end);
+    }
+  }
+
+  /**
+   * Returns an iterator over variant records read from a flat file which
+   * overlap the specified chromosomal positions. Call close() on the iterator
+   * when finished with it!
+   * 
+   * @param chrom
+   * @param start
+   * @param end
+   * @return
+   */
+  protected CloseableIterator<VariantContext> queryUnindexed(
+          final String chrom, final int start, final int end)
+  {
+    final CloseableIterator<VariantContext> it = reader.iterator();
+    
+    return new CloseableIterator<VariantContext>()
+    {
+      boolean atEnd = false;
+
+      // prime look-ahead buffer with next matching record
+      private VariantContext next = findNext();
+
+      private VariantContext findNext()
+      {
+        if (atEnd)
+        {
+          return null;
+        }
+        VariantContext variant = null;
+        while (it.hasNext())
+        {
+          variant = it.next();
+          int vstart = variant.getStart();
+
+          if (vstart > end)
+          {
+            atEnd = true;
+            close();
+            return null;
+          }
+
+          int vend = variant.getEnd();
+          // todo what is the undeprecated way to get
+          // the chromosome for the variant?
+          if (chrom.equals(variant.getChr()) && (vstart <= end)
+                  && (vend >= start))
+          {
+            return variant;
+          }
+        }
+        return null;
+      }
+
+      @Override
+      public boolean hasNext()
+      {
+        boolean hasNext = !atEnd && (next != null);
+        if (!hasNext)
+        {
+          close();
+        }
+        return hasNext;
+      }
+
+      @Override
+      public VariantContext next()
+      {
+        /*
+         * return the next match, and then re-prime
+         * it with the following one (if any)
+         */
+        VariantContext temp = next;
+        next = findNext();
+        return temp;
+      }
+
+      @Override
+      public void remove()
+      {
+        // not implemented
+      }
+
+      @Override
+      public void close()
+      {
+        it.close();
+      }
+    };
+  }
+
+  /**
+   * Returns an object that models the VCF file headers
+   * 
+   * @return
+   */
+  public VCFHeader getFileHeader()
+  {
+    return reader == null ? null : reader.getFileHeader();
+  }
+
+  /**
+   * Answers true if we are processing a tab-indexed VCF file, false if it is a
+   * plain text (uncompressed) file.
+   * 
+   * @return
+   */
+  public boolean isIndex()
+  {
+    return indexed;
+  }
+}
index 96dfcfe..8832278 100644 (file)
@@ -27,6 +27,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SequenceI;
+import jalview.gui.IProgressIndicator;
 import jalview.io.DataSourceType;
 import jalview.io.StructureFile;
 import jalview.schemes.ColourSchemeI;
@@ -72,7 +73,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
    */
   private boolean associateNewStructs = false;
 
-  Vector<String> atomsPicked = new Vector<String>();
+  Vector<String> atomsPicked = new Vector<>();
 
   private List<String> chainNames;
 
@@ -477,6 +478,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     lastCommand = command;
   }
 
+  Thread colourby = null;
   /**
    * Sends a set of colour commands to the structure viewer
    * 
@@ -484,15 +486,28 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
    */
   @Override
   protected void colourBySequence(
-          StructureMappingcommandSet[] colourBySequenceCommands)
+          final StructureMappingcommandSet[] colourBySequenceCommands)
   {
-    for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
+    if (colourby != null)
     {
-      for (String cbyseq : cpdbbyseq.commands)
+      colourby.interrupt();
+      colourby = null;
+    }
+    colourby = new Thread(new Runnable()
+    {
+      @Override
+      public void run()
       {
-        executeWhenReady(cbyseq);
+        for (StructureMappingcommandSet cpdbbyseq : colourBySequenceCommands)
+        {
+          for (String cbyseq : cpdbbyseq.commands)
+          {
+            executeWhenReady(cbyseq);
+          }
+        }
       }
-    }
+    });
+    colourby.start();
   }
 
   /**
@@ -610,7 +625,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     }
     if (modelFileNames == null)
     {
-      List<String> mset = new ArrayList<String>();
+      List<String> mset = new ArrayList<>();
       _modelFileNameMap = new int[viewer.ms.mc];
       String m = viewer.ms.getModelFileName(0);
       if (m != null)
@@ -670,7 +685,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
   @Override
   public synchronized String[] getStructureFiles()
   {
-    List<String> mset = new ArrayList<String>();
+    List<String> mset = new ArrayList<>();
     if (viewer == null)
     {
       return new String[0];
@@ -861,19 +876,30 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
       try
       {
         // recover PDB filename for the model hovered over.
-        int _mp = _modelFileNameMap.length - 1,
-                mnumber = new Integer(mdlId).intValue() - 1;
-        while (mnumber < _modelFileNameMap[_mp])
+        int mnumber = new Integer(mdlId).intValue() - 1;
+        if (_modelFileNameMap != null)
         {
-          _mp--;
+          int _mp = _modelFileNameMap.length - 1;
+
+          while (mnumber < _modelFileNameMap[_mp])
+          {
+            _mp--;
+          }
+          pdbfilename = modelFileNames[_mp];
         }
-        pdbfilename = modelFileNames[_mp];
-        if (pdbfilename == null)
+        else
         {
-          pdbfilename = new File(viewer.ms.getModelFileName(mnumber))
-                  .getAbsolutePath();
-        }
+          if (mnumber >= 0 && mnumber < modelFileNames.length)
+          {
+            pdbfilename = modelFileNames[mnumber];
+          }
 
+          if (pdbfilename == null)
+          {
+            pdbfilename = new File(viewer.ms.getModelFileName(mnumber))
+                    .getAbsolutePath();
+          }
+        }
       } catch (Exception e)
       {
       }
@@ -975,6 +1001,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
         notifyAtomPicked(((Integer) data[2]).intValue(), (String) data[1],
                 (String) data[0]);
         // also highlight in alignment
+        // deliberate fall through
       case HOVER:
         notifyAtomHovered(((Integer) data[2]).intValue(), (String) data[1],
                 (String) data[0]);
@@ -1059,8 +1086,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     fileLoadingError = null;
     String[] oldmodels = modelFileNames;
     modelFileNames = null;
-    chainNames = new ArrayList<String>();
-    chainFile = new Hashtable<String, String>();
+    chainNames = new ArrayList<>();
+    chainFile = new Hashtable<>();
     boolean notifyLoaded = false;
     String[] modelfilenames = getStructureFiles();
     // first check if we've lost any structures
@@ -1126,7 +1153,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
           // see JAL-623 - need method of matching pasted data up
           {
             pdb = getSsm().setMapping(getSequence()[pe], getChains()[pe],
-                    pdbfile, DataSourceType.PASTE);
+                    pdbfile, DataSourceType.PASTE,
+                    getIProgressIndicator());
             getPdbEntry(modelnum).setFile("INLINE" + pdb.getId());
             matches = true;
             foundEntry = true;
@@ -1158,7 +1186,7 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
             }
             // Explicitly map to the filename used by Jmol ;
             pdb = getSsm().setMapping(getSequence()[pe], getChains()[pe],
-                    fileName, protocol);
+                    fileName, protocol, getIProgressIndicator());
             // pdbentry[pe].getFile(), protocol);
 
           }
@@ -1226,6 +1254,8 @@ public abstract class JalviewJmolBinding extends AAStructureBindingModel
     return chainNames;
   }
 
+  protected abstract IProgressIndicator getIProgressIndicator();
+
   public void notifyNewPickingModeMeasurement(int iatom, String strMeasure)
   {
     notifyAtomPicked(iatom, strMeasure, null);
index 6bf7010..8fb0de6 100644 (file)
@@ -77,7 +77,6 @@ public class JmolCommands
         continue;
       }
 
-      int lastPos = -1;
       for (int s = 0; s < sequence[pdbfnum].length; s++)
       {
         for (int sp, m = 0; m < mapping.length; m++)
@@ -85,6 +84,7 @@ public class JmolCommands
           if (mapping[m].getSequence() == sequence[pdbfnum][s]
                   && (sp = al.findIndex(sequence[pdbfnum][s])) > -1)
           {
+            int lastPos = StructureMapping.UNASSIGNED_VALUE;
             SequenceI asp = al.getSequenceAt(sp);
             for (int r = 0; r < asp.getLength(); r++)
             {
@@ -95,10 +95,22 @@ public class JmolCommands
               }
               int pos = mapping[m].getPDBResNum(asp.findPosition(r));
 
-              if (pos < 1 || pos == lastPos)
+              if (pos == lastPos)
               {
                 continue;
               }
+              if (pos == StructureMapping.UNASSIGNED_VALUE)
+              {
+                // terminate current colour op
+                if (command.length() > 0
+                        && command.charAt(command.length() - 1) != ';')
+                {
+                  command.append(";");
+                }
+                // reset lastPos
+                lastPos = StructureMapping.UNASSIGNED_VALUE;
+                continue;
+              }
 
               lastPos = pos;
 
@@ -128,7 +140,12 @@ public class JmolCommands
               // TODO: deal with case when buffer is too large for Jmol to parse
               // - execute command and flush
 
-              command.append(";");
+              if (command.length() > 0
+                      && command.charAt(command.length() - 1) != ';')
+              {
+                command.append(";");
+              }
+
               if (command.length() > 51200)
               {
                 // add another chunk
index dc3d0ee..2a510a2 100644 (file)
@@ -28,7 +28,6 @@ import jalview.io.DataSourceType;
 import jalview.io.FileParse;
 import jalview.io.StructureFile;
 import jalview.schemes.ResidueProperties;
-import jalview.structure.StructureImportSettings;
 import jalview.util.Format;
 import jalview.util.MessageManager;
 
@@ -60,6 +59,12 @@ public class JmolParser extends StructureFile implements JmolStatusListener
 {
   Viewer viewer = null;
 
+  public JmolParser(boolean immediate, String inFile,
+          DataSourceType sourceType) throws IOException
+  {
+    super(immediate, inFile, sourceType);
+  }
+
   public JmolParser(String inFile, DataSourceType sourceType)
           throws IOException
   {
@@ -183,7 +188,11 @@ public class JmolParser extends StructureFile implements JmolStatusListener
         }
         lastID = tmpatom.resNumIns.trim();
       }
-      xferSettings();
+      if (isParseImmediately())
+      {
+        // configure parsing settings from the static singleton
+        xferSettings();
+      }
 
       makeResidueList();
       makeCaBondList();
@@ -200,7 +209,8 @@ public class JmolParser extends StructureFile implements JmolStatusListener
           prot.add(chainseq);
         }
 
-        if (StructureImportSettings.isProcessSecondaryStructure())
+        // look at local setting for adding secondary tructure
+        if (predictSecondaryStructure)
         {
           createAnnotation(chainseq, chain, ms.at);
         }
index f923f7f..39d6704 100644 (file)
@@ -120,7 +120,7 @@ public class AtomSpecModel
 
       for (String chain : modelData.keySet())
       {
-        chain = chain.trim();
+        chain = " ".equals(chain) ? chain : chain.trim();
 
         List<int[]> rangeList = modelData.get(chain);
 
@@ -192,9 +192,10 @@ public class AtomSpecModel
     {
       sb.append(start).append("-").append(end);
     }
-    if (chain.length() > 0)
-    {
-      sb.append(".").append(chain);
+
+    sb.append(".");
+    if (!" ".equals(chain)) {
+      sb.append(chain);
     }
   }
 }
index 99c0c51..974cc88 100644 (file)
@@ -145,4 +145,11 @@ public interface GFTSPanelI
    * @return
    */
   public String getCacheKey();
+
+  /**
+   * 
+   * @return user preference name for configuring this FTS search's autosearch
+   *         checkbox
+   */
+  public String getAutosearchPreference();
 }
index 076e212..f94d455 100644 (file)
@@ -284,7 +284,8 @@ public abstract class FTSRestClient implements FTSRestClientI
               public boolean equals(Object otherObject)
               {
                 FTSDataColumnI that = (FTSDataColumnI) otherObject;
-                return this.getCode().equals(that.getCode())
+                return otherObject == null ? false
+                        : this.getCode().equals(that.getCode())
                         && this.getName().equals(that.getName())
                         && this.getGroup().equals(that.getGroup());
               }
index c0d005f..86710e1 100644 (file)
@@ -56,6 +56,7 @@ import java.util.List;
 
 import javax.swing.ImageIcon;
 import javax.swing.JButton;
+import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JFrame;
 import javax.swing.JInternalFrame;
@@ -88,9 +89,10 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
   protected JInternalFrame mainFrame = new JInternalFrame(
           getFTSFrameTitle());
 
+  protected JTabbedPane tabs = new JTabbedPane();
   protected IProgressIndicator progressIndicator;
 
-  protected JComboBox<FTSDataColumnI> cmb_searchTarget = new JComboBox<FTSDataColumnI>();
+  protected JComboBox<FTSDataColumnI> cmb_searchTarget = new JComboBox<>();
 
   protected JButton btn_ok = new JButton();
 
@@ -98,6 +100,8 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   protected JButton btn_cancel = new JButton();
 
+  protected JCheckBox btn_autosearch = new JCheckBox();
+
   protected JvCacheableInputBox<String> txt_search;
 
   protected SequenceFetcher seqFetcher;
@@ -147,7 +151,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   protected int pageLimit;
 
-  protected HashSet<String> paginatorCart = new HashSet<String>();
+  protected HashSet<String> paginatorCart = new HashSet<>();
 
   private static final int MIN_WIDTH = 670;
 
@@ -239,16 +243,38 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   public GFTSPanel()
   {
+    this(null);
+  }
+
+  public GFTSPanel(SequenceFetcher fetcher)
+  {
     try
     {
+      if (fetcher == null)
+      {
+        tabs = null;
+      }
       jbInit();
+      if (fetcher != null)
+      {
+        tabs.addTab(MessageManager.getString("label.retrieve_ids"),
+                fetcher);
+        fetcher.setDatabaseChooserVisible(false);
+        fetcher.embedWithFTSPanel(this);
+      }
       mainFrame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
+      final JPanel ftsPanel = this;
       mainFrame.addFocusListener(new FocusAdapter()
       {
         @Override
         public void focusGained(FocusEvent e)
         {
-          txt_search.requestFocusInWindow();
+          // TODO: make selected tab gain focus in correct widget
+          if (tabs != null
+                  && tabs.getSelectedComponent() == ftsPanel)
+          {
+            txt_search.requestFocusInWindow();
+          }
         }
       });
       mainFrame.invalidate();
@@ -267,7 +293,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
   private void jbInit() throws Exception
   {
 
-    txt_search = new JvCacheableInputBox<String>(getCacheKey());
+    txt_search = new JvCacheableInputBox<>(getCacheKey());
     populateCmbSearchTargetOptions();
     Integer width = getTempUserPrefs().get("FTSPanel.width") == null ? 800
             : getTempUserPrefs().get("FTSPanel.width");
@@ -331,6 +357,20 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
       }
     });
 
+    btn_autosearch.setText(MessageManager.getString("option.autosearch"));
+    btn_autosearch.setToolTipText(
+            MessageManager.getString("option.enable_disable_autosearch"));
+    btn_autosearch.setSelected(
+            jalview.bin.Cache.getDefault(getAutosearchPreference(), true));
+    btn_autosearch.addActionListener(new java.awt.event.ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        jalview.bin.Cache.setProperty(getAutosearchPreference(),
+                Boolean.toString(btn_autosearch.isSelected()));
+      }
+    });
     btn_back.setFont(new java.awt.Font("Verdana", 0, 12));
     btn_back.setText(MessageManager.getString("action.back"));
     btn_back.addActionListener(new java.awt.event.ActionListener()
@@ -511,8 +551,14 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
                   if (primaryKeyName.equalsIgnoreCase(getCmbSearchTarget()
                           .getSelectedItem().toString()))
                   {
+                    // TODO: nicer to show the list in the result set before
+                    // viewing in Jalview perhaps ?
                     transferToSequenceFetcher(getTypedText());
                   }
+                  else
+                  {
+                    performSearchAction();
+                  }
                 }
               }
             });
@@ -522,12 +568,10 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
               @Override
               public void actionPerformed(ActionEvent e)
               {
-                String typed = getTypedText();
-                if (!typed.equalsIgnoreCase(lastSearchTerm))
+                if (btn_autosearch.isSelected()
+                        || txt_search.wasEnterPressed())
                 {
-                  searchAction(true);
-                  paginatorCart.clear();
-                  lastSearchTerm = typed;
+                  performSearchAction();
                 }
               }
             }, false);
@@ -549,6 +593,15 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
       }
     });
 
+    txt_search.addActionListener(new ActionListener()
+    {
+
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        performSearchAction();
+      }
+    });
     final String searchTabTitle = MessageManager
             .getString("label.search_result");
     final String configureCols = MessageManager
@@ -613,6 +666,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     pnl_results.add(tabbedPane);
     pnl_inputs.add(cmb_searchTarget);
     pnl_inputs.add(txt_search);
+    pnl_inputs.add(btn_autosearch);
     pnl_inputs.add(lbl_loading);
     pnl_inputs.add(lbl_warning);
     pnl_inputs.add(lbl_blank);
@@ -624,7 +678,18 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     this.add(pnl_results, java.awt.BorderLayout.CENTER);
     this.add(pnl_actions, java.awt.BorderLayout.SOUTH);
     mainFrame.setVisible(true);
-    mainFrame.setContentPane(this);
+    if (tabs != null)
+    {
+      tabs.setOpaque(true);
+      tabs.insertTab(MessageManager.getString("label.free_text_search"),
+              null, this, "", 0);
+      mainFrame.setContentPane(tabs);
+      tabs.setVisible(true);
+    }
+    else
+    {
+      mainFrame.setContentPane(this);
+    }
     mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
     mainFrame.addInternalFrameListener(
             new javax.swing.event.InternalFrameAdapter()
@@ -635,8 +700,6 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
                 closeAction();
               }
             });
-    mainFrame.setVisible(true);
-    mainFrame.setContentPane(this);
     mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
     Integer x = getTempUserPrefs().get("FTSPanel.x");
     Integer y = getTempUserPrefs().get("FTSPanel.y");
@@ -698,6 +761,18 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
 
   }
 
+  void performSearchAction()
+  {
+    String typed = getTypedText();
+    if (typed != null && typed.length() > 0
+            && !typed.equalsIgnoreCase(lastSearchTerm))
+    {
+      searchAction(true);
+      paginatorCart.clear();
+      lastSearchTerm = typed;
+    }
+  }
+
   public boolean wantedFieldsUpdated()
   {
     if (previousWantedFields == null)
@@ -774,7 +849,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     }
   }
 
-  protected void btn_back_ActionPerformed()
+  public void btn_back_ActionPerformed()
   {
     closeAction();
     new SequenceFetcher(progressIndicator);
@@ -787,7 +862,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
     btn_cancel.setEnabled(false);
   }
 
-  protected void btn_cancel_ActionPerformed()
+  public void btn_cancel_ActionPerformed()
   {
     closeAction();
   }
@@ -797,7 +872,7 @@ public abstract class GFTSPanel extends JPanel implements GFTSPanelI
    */
   public void populateCmbSearchTargetOptions()
   {
-    List<FTSDataColumnI> searchableTargets = new ArrayList<FTSDataColumnI>();
+    List<FTSDataColumnI> searchableTargets = new ArrayList<>();
     try
     {
       Collection<FTSDataColumnI> foundFTSTargets = getFTSRestClient()
index 2a53ab9..053d91b 100644 (file)
@@ -43,13 +43,15 @@ public class PDBFTSPanel extends GFTSPanel
 
   private static final String PDB_FTS_CACHE_KEY = "CACHE.PDB_FTS";
 
-  public PDBFTSPanel(SequenceFetcher seqFetcher)
+  private static final String PDB_AUTOSEARCH = "FTS.PDB.AUTOSEARCH";
+
+  public PDBFTSPanel(SequenceFetcher fetcher)
   {
-    super();
+    super(fetcher);
     pageLimit = PDBFTSRestClient.getInstance().getDefaultResponsePageSize();
-    this.seqFetcher = seqFetcher;
-    this.progressIndicator = (seqFetcher == null) ? null
-            : seqFetcher.getProgressIndicator();
+    this.seqFetcher = fetcher;
+    this.progressIndicator = (fetcher == null) ? null
+            : fetcher.getProgressIndicator();
   }
 
   @Override
@@ -86,15 +88,16 @@ public class PDBFTSPanel extends GFTSPanel
           request.setSearchTerm(searchTerm + ")");
           request.setOffSet(offSet);
           request.setWantedFields(wantedFields);
-          FTSRestClientI pdbRestCleint = PDBFTSRestClient.getInstance();
+          FTSRestClientI pdbRestClient = PDBFTSRestClient.getInstance();
           FTSRestResponse resultList;
           try
           {
-            resultList = pdbRestCleint.executeRequest(request);
+            resultList = pdbRestClient.executeRequest(request);
           } catch (Exception e)
           {
             setErrorMessage(e.getMessage());
             checkForErrors();
+            setSearchInProgress(false);
             return;
           }
 
@@ -280,4 +283,9 @@ public class PDBFTSPanel extends GFTSPanel
     return PDB_FTS_CACHE_KEY;
   }
 
-}
+  @Override
+  public String getAutosearchPreference()
+  {
+    return PDB_AUTOSEARCH;
+  }
+}
\ No newline at end of file
index a483f44..cbeaff1 100644 (file)
@@ -59,7 +59,7 @@ public class PDBFTSRestClient extends FTSRestClient
 
   private static FTSRestClientI instance = null;
 
-  public static final String PDB_SEARCH_ENDPOINT = "http://www.ebi.ac.uk/pdbe/search/pdb/select?";
+  public static final String PDB_SEARCH_ENDPOINT = "https://www.ebi.ac.uk/pdbe/search/pdb/select?";
 
   protected PDBFTSRestClient()
   {
index 250fba0..262ed86 100644 (file)
@@ -21,6 +21,7 @@
 
 package jalview.fts.service.uniprot;
 
+import jalview.bin.Cache;
 import jalview.fts.api.FTSData;
 import jalview.fts.api.FTSDataColumnI;
 import jalview.fts.api.FTSRestClientI;
@@ -44,9 +45,18 @@ import com.sun.jersey.api.client.config.DefaultClientConfig;
 
 public class UniProtFTSRestClient extends FTSRestClient
 {
+  private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
+
   private static FTSRestClientI instance = null;
 
-  public static final String UNIPROT_SEARCH_ENDPOINT = "http://www.uniprot.org/uniprot/?";
+  public final String uniprotSearchEndpoint;
+
+  public UniProtFTSRestClient()
+  {
+    super();
+    uniprotSearchEndpoint = Cache.getDefault("UNIPROT_DOMAIN",
+            DEFAULT_UNIPROT_DOMAIN) + "/uniprot/?";
+  }
 
   @Override
   public FTSRestResponse executeRequest(FTSRestRequest uniportRestRequest)
@@ -81,7 +91,7 @@ public class UniProtFTSRestClient extends FTSRestClient
       }
 
       WebResource webResource = null;
-      webResource = client.resource(UNIPROT_SEARCH_ENDPOINT)
+      webResource = client.resource(uniprotSearchEndpoint)
               .queryParam("format", "tab")
               .queryParam("columns", wantedFields)
               .queryParam("limit", String.valueOf(responseSize))
@@ -158,7 +168,7 @@ public class UniProtFTSRestClient extends FTSRestClient
     String[] foundDataRow = uniProtTabDelimittedResponseString.split("\n");
     if (foundDataRow != null && foundDataRow.length > 0)
     {
-      result = new ArrayList<FTSData>();
+      result = new ArrayList<>();
       boolean firstRow = true;
       for (String dataRow : foundDataRow)
       {
index 2dad2f7..df54dea 100644 (file)
@@ -40,18 +40,20 @@ public class UniprotFTSPanel extends GFTSPanel
   private static String defaultFTSFrameTitle = MessageManager
           .getString("label.uniprot_sequence_fetcher");
 
-  private static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
+  private static Map<String, Integer> tempUserPrefs = new HashMap<>();
 
   private static final String UNIPROT_FTS_CACHE_KEY = "CACHE.UNIPROT_FTS";
 
-  public UniprotFTSPanel(SequenceFetcher seqFetcher)
+  private static final String UNIPROT_AUTOSEARCH = "FTS.UNIPROT.AUTOSEARCH";
+
+  public UniprotFTSPanel(SequenceFetcher fetcher)
   {
-    super();
+    super(fetcher);
     pageLimit = UniProtFTSRestClient.getInstance()
             .getDefaultResponsePageSize();
-    this.seqFetcher = seqFetcher;
-    this.progressIndicator = (seqFetcher == null) ? null
-            : seqFetcher.getProgressIndicator();
+    this.seqFetcher = fetcher;
+    this.progressIndicator = (fetcher == null) ? null
+            : fetcher.getProgressIndicator();
   }
 
   @Override
@@ -85,17 +87,17 @@ public class UniprotFTSPanel extends GFTSPanel
           request.setSearchTerm(searchTerm);
           request.setOffSet(offSet);
           request.setWantedFields(wantedFields);
-          FTSRestClientI uniProtRestCleint = UniProtFTSRestClient
+          FTSRestClientI uniProtRestClient = UniProtFTSRestClient
                   .getInstance();
           FTSRestResponse resultList;
           try
           {
-            resultList = uniProtRestCleint.executeRequest(request);
+            resultList = uniProtRestClient.executeRequest(request);
           } catch (Exception e)
           {
-            e.printStackTrace();
             setErrorMessage(e.getMessage());
             checkForErrors();
+            setSearchInProgress(false);
             return;
           }
 
@@ -183,7 +185,7 @@ public class UniprotFTSPanel extends GFTSPanel
   {
     disableActionButtons();
     StringBuilder selectedIds = new StringBuilder();
-    HashSet<String> selectedIdsSet = new HashSet<String>();
+    HashSet<String> selectedIdsSet = new HashSet<>();
     int primaryKeyColIndex = 0;
     try
     {
@@ -237,4 +239,10 @@ public class UniprotFTSPanel extends GFTSPanel
   {
     return UNIPROT_FTS_CACHE_KEY;
   }
+
+  @Override
+  public String getAutosearchPreference()
+  {
+    return UNIPROT_AUTOSEARCH;
+  }
 }
index 13b715e..9de9e3b 100644 (file)
@@ -81,6 +81,7 @@ import jalview.io.JnetAnnotationMaker;
 import jalview.io.NewickFile;
 import jalview.io.ScoreMatrixFile;
 import jalview.io.TCoffeeScoreFile;
+import jalview.io.vcf.VCFLoader;
 import jalview.jbgui.GAlignFrame;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
@@ -163,8 +164,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
   AlignViewport viewport;
 
-  ViewportRanges vpRanges;
-
   public AlignViewControllerI avc;
 
   List<AlignmentPanel> alignPanels = new ArrayList<>();
@@ -336,7 +335,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       progressBar = new ProgressBar(this.statusPanel, this.statusBar);
     }
 
-    vpRanges = viewport.getRanges();
     avc = new jalview.controller.AlignViewController(this, viewport,
             alignPanel);
     if (viewport.getAlignmentConservationAnnotation() == null)
@@ -654,9 +652,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                   { (viewport.cursorMode ? "on" : "off") }));
           if (viewport.cursorMode)
           {
-            alignPanel.getSeqPanel().seqCanvas.cursorX = vpRanges
+            ViewportRanges ranges = viewport.getRanges();
+            alignPanel.getSeqPanel().seqCanvas.cursorX = ranges
                     .getStartRes();
-            alignPanel.getSeqPanel().seqCanvas.cursorY = vpRanges
+            alignPanel.getSeqPanel().seqCanvas.cursorY = ranges
                     .getStartSeq();
           }
           alignPanel.getSeqPanel().seqCanvas.repaint();
@@ -689,10 +688,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           break;
         }
         case KeyEvent.VK_PAGE_UP:
-          vpRanges.pageUp();
+          viewport.getRanges().pageUp();
           break;
         case KeyEvent.VK_PAGE_DOWN:
-          vpRanges.pageDown();
+          viewport.getRanges().pageDown();
           break;
         }
       }
@@ -841,6 +840,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     AlignmentI al = getViewport().getAlignment();
     boolean nucleotide = al.isNucleotide();
 
+    loadVcf.setVisible(nucleotide);
     showTranslation.setVisible(nucleotide);
     showReverse.setVisible(nucleotide);
     showReverseComplement.setVisible(nucleotide);
@@ -1392,13 +1392,13 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   @Override
   public void exportFeatures_actionPerformed(ActionEvent e)
   {
-    new AnnotationExporter().exportFeatures(alignPanel);
+    new AnnotationExporter(alignPanel).exportFeatures();
   }
 
   @Override
   public void exportAnnotations_actionPerformed(ActionEvent e)
   {
-    new AnnotationExporter().exportAnnotations(alignPanel);
+    new AnnotationExporter(alignPanel).exportAnnotations();
   }
 
   @Override
@@ -1711,7 +1711,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     }
     viewport.getAlignment().moveSelectedSequencesByOne(sg,
             viewport.getHiddenRepSequences(), up);
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   synchronized void slideSequences(boolean right, int size)
@@ -1828,7 +1828,6 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   @Override
   protected void copy_actionPerformed(ActionEvent e)
   {
-    System.gc();
     if (viewport.getSelectionGroup() == null)
     {
       return;
@@ -1864,23 +1863,17 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       return;
     }
 
-    ArrayList<int[]> hiddenColumns = null;
+    HiddenColumns hiddenColumns = null;
     if (viewport.hasHiddenColumns())
     {
-      hiddenColumns = new ArrayList<>();
       int hiddenOffset = viewport.getSelectionGroup().getStartRes();
       int hiddenCutoff = viewport.getSelectionGroup().getEndRes();
-      ArrayList<int[]> hiddenRegions = viewport.getAlignment()
-              .getHiddenColumns().getHiddenColumnsCopy();
-      for (int[] region : hiddenRegions)
-      {
-        if (region[0] >= hiddenOffset && region[1] <= hiddenCutoff)
-        {
-          hiddenColumns
-                  .add(new int[]
-                  { region[0] - hiddenOffset, region[1] - hiddenOffset });
-        }
-      }
+
+      // create new HiddenColumns object with copy of hidden regions
+      // between startRes and endRes, offset by startRes
+      hiddenColumns = new HiddenColumns(
+              viewport.getAlignment().getHiddenColumns(), hiddenOffset,
+              hiddenCutoff, hiddenOffset);
     }
 
     Desktop.jalviewClipboard = new Object[] { seqs,
@@ -2147,7 +2140,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       {
 
         // propagate alignment changed.
-        vpRanges.setEndSeq(alignment.getHeight());
+        viewport.getRanges().setEndSeq(alignment.getHeight());
         if (annotationAdded)
         {
           // Duplicate sequence annotation in all views.
@@ -2209,11 +2202,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         if (Desktop.jalviewClipboard != null
                 && Desktop.jalviewClipboard[2] != null)
         {
-          List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-          for (int[] region : hc)
-          {
-            af.viewport.hideColumns(region[0], region[1]);
-          }
+          HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+          af.viewport.setHiddenColumns(hc);
         }
 
         // >>>This is a fix for the moment, until a better solution is
@@ -2268,11 +2258,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       if (Desktop.jalviewClipboard != null
               && Desktop.jalviewClipboard[2] != null)
       {
-        List<int[]> hc = (List<int[]>) Desktop.jalviewClipboard[2];
-        for (int region[] : hc)
-        {
-          af.viewport.hideColumns(region[0], region[1]);
-        }
+        HiddenColumns hc = (HiddenColumns) Desktop.jalviewClipboard[2];
+        af.viewport.setHiddenColumns(hc);
       }
 
       // >>>This is a fix for the moment, until a better solution is
@@ -2397,7 +2384,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -2423,7 +2410,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
     // updating.
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
   }
 
@@ -2448,7 +2435,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // JAL-2034 - should delegate to
     // alignPanel to decide if overview needs
     // updating.
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
     viewport.sendSelection();
   }
@@ -2479,7 +2466,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // alignPanel to decide if overview needs
     // updating.
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
     PaintRefresher.Refresh(alignPanel, viewport.getSequenceSetId());
     viewport.sendSelection();
   }
@@ -2488,7 +2475,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void invertColSel_actionPerformed(ActionEvent e)
   {
     viewport.invertColumnSelection();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
     viewport.sendSelection();
   }
 
@@ -2548,7 +2535,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       {
         trimRegion = new TrimRegionCommand("Remove Left", true, seqs,
                 column, viewport.getAlignment());
-        vpRanges.setStartRes(0);
+        viewport.getRanges().setStartRes(0);
       }
       else
       {
@@ -2613,13 +2600,14 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // This is to maintain viewport position on first residue
     // of first sequence
     SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-    int startRes = seq.findPosition(vpRanges.getStartRes());
+    ViewportRanges ranges = viewport.getRanges();
+    int startRes = seq.findPosition(ranges.getStartRes());
     // ShiftList shifts;
     // viewport.getAlignment().removeGaps(shifts=new ShiftList());
     // edit.alColumnChanges=shifts.getInverse();
     // if (viewport.hasHiddenColumns)
     // viewport.getColumnSelection().compensateForEdits(shifts);
-    vpRanges.setStartRes(seq.findIndex(startRes) - 1);
+    ranges.setStartRes(seq.findIndex(startRes) - 1);
     viewport.firePropertyChange("alignment", null,
             viewport.getAlignment().getSequences());
 
@@ -2652,12 +2640,12 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     // This is to maintain viewport position on first residue
     // of first sequence
     SequenceI seq = viewport.getAlignment().getSequenceAt(0);
-    int startRes = seq.findPosition(vpRanges.getStartRes());
+    int startRes = seq.findPosition(viewport.getRanges().getStartRes());
 
     addHistoryItem(new RemoveGapsCommand("Remove Gaps", seqs, start, end,
             viewport.getAlignment()));
 
-    vpRanges.setStartRes(seq.findIndex(startRes) - 1);
+    viewport.getRanges().setStartRes(seq.findIndex(startRes) - 1);
 
     viewport.firePropertyChange("alignment", null,
             viewport.getAlignment().getSequences());
@@ -2713,8 +2701,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     /*
      * Create a new AlignmentPanel (with its own, new Viewport)
      */
-    AlignmentPanel newap = new Jalview2XML().copyAlignPanel(alignPanel,
-            true);
+    AlignmentPanel newap = new Jalview2XML().copyAlignPanel(alignPanel);
     if (!copyAnnotation)
     {
       /*
@@ -2868,21 +2855,21 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     alignPanel.getIdPanel().getIdCanvas()
             .setPreferredSize(alignPanel.calculateIdWidth());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   @Override
   public void idRightAlign_actionPerformed(ActionEvent e)
   {
     viewport.setRightAlignIds(idRightAlign.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   @Override
   public void centreColumnLabels_actionPerformed(ActionEvent e)
   {
     viewport.setCentreColumnLabels(centreColumnLabelsMenuItem.getState());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /*
@@ -2915,7 +2902,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void colourTextMenuItem_actionPerformed(ActionEvent e)
   {
     viewport.setColourText(colourTextMenuItem.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -2944,7 +2931,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void showAllColumns_actionPerformed(ActionEvent e)
   {
     viewport.showAllHiddenColumns();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
     viewport.sendSelection();
   }
 
@@ -3048,7 +3035,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     viewport.expandColSelection(sg, false);
     viewport.hideAllSelectedSeqs();
     viewport.hideSelectedColumns();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
     viewport.sendSelection();
   }
 
@@ -3064,7 +3051,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   {
     viewport.showAllHiddenColumns();
     viewport.showAllHiddenSeqs();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
     viewport.sendSelection();
   }
 
@@ -3072,7 +3059,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void hideSelColumns_actionPerformed(ActionEvent e)
   {
     viewport.hideSelectedColumns();
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
     viewport.sendSelection();
   }
 
@@ -3093,7 +3080,8 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void scaleAbove_actionPerformed(ActionEvent e)
   {
     viewport.setScaleAboveWrapped(scaleAbove.isSelected());
-    alignPanel.paintAlignment(true);
+    // TODO: do we actually need to update overview for scale above change ?
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3106,7 +3094,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void scaleLeft_actionPerformed(ActionEvent e)
   {
     viewport.setScaleLeftWrapped(scaleLeft.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3119,7 +3107,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void scaleRight_actionPerformed(ActionEvent e)
   {
     viewport.setScaleRightWrapped(scaleRight.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3132,7 +3120,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void viewBoxesMenuItem_actionPerformed(ActionEvent e)
   {
     viewport.setShowBoxes(viewBoxesMenuItem.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -3145,7 +3133,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void viewTextMenuItem_actionPerformed(ActionEvent e)
   {
     viewport.setShowText(viewTextMenuItem.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -3158,7 +3146,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void renderGapsMenuItem_actionPerformed(ActionEvent e)
   {
     viewport.setRenderGaps(renderGapsMenuItem.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   public FeatureSettings featureSettings;
@@ -3196,7 +3184,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   public void showSeqFeatures_actionPerformed(ActionEvent evt)
   {
     viewport.setShowSequenceFeatures(showSeqFeatures.isSelected());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   /**
@@ -3272,6 +3260,10 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                 alignPanel.setOverviewPanel(null);
               };
             });
+    if (getKeyListeners().length > 0)
+    {
+      frame.addKeyListener(getKeyListeners()[0]);
+    }
 
     alignPanel.setOverviewPanel(overview);
   }
@@ -3353,7 +3345,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
 
     viewport.setGlobalColourScheme(cs);
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
   }
 
   /**
@@ -3438,7 +3430,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             viewport.getAlignment().getSequenceAt(0));
     addHistoryItem(new OrderCommand("Pairwise Sort", oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3454,7 +3446,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     AlignmentSorter.sortByID(viewport.getAlignment());
     addHistoryItem(
             new OrderCommand("ID Sort", oldOrder, viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3470,7 +3462,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     AlignmentSorter.sortByLength(viewport.getAlignment());
     addHistoryItem(new OrderCommand("Length Sort", oldOrder,
             viewport.getAlignment()));
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3487,7 +3479,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     addHistoryItem(new OrderCommand("Group Sort", oldOrder,
             viewport.getAlignment()));
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
   }
 
   /**
@@ -3644,7 +3636,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         addHistoryItem(new OrderCommand(order.getName(), oldOrder,
                 viewport.getAlignment()));
 
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, false);
       }
     });
   }
@@ -3673,7 +3665,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                 viewport.getAlignment());// ,viewport.getSelectionGroup());
         addHistoryItem(new OrderCommand("Sort by " + scoreLabel, oldOrder,
                 viewport.getAlignment()));
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, false);
       }
     });
   }
@@ -3788,7 +3780,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       addHistoryItem(new OrderCommand(undoname, oldOrder,
               viewport.getAlignment()));
     }
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, false);
     return true;
   }
 
@@ -4259,7 +4251,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
           final String source)
   {
-    new Thread(CrossRefAction.showProductsFor(sel, _odna, source, this))
+    new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this))
             .start();
   }
 
@@ -4472,17 +4464,21 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             int assocfiles = 0;
             if (filesmatched.size() > 0)
             {
-              if (Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false)
-                      || JvOptionPane.showConfirmDialog(thisaf,
-                              MessageManager.formatMessage(
-                                      "label.automatically_associate_structure_files_with_sequences_same_name",
-                                      new Object[]
-                                      { Integer.valueOf(filesmatched.size())
-                                              .toString() }),
-                              MessageManager.getString(
-                                      "label.automatically_associate_structure_files_by_name"),
-                              JvOptionPane.YES_NO_OPTION) == JvOptionPane.YES_OPTION)
-
+              boolean autoAssociate = Cache.getDefault("AUTOASSOCIATE_PDBANDSEQS", false);
+              if (!autoAssociate)
+              {
+                String msg = MessageManager.formatMessage(
+                        "label.automatically_associate_structure_files_with_sequences_same_name",
+                        new Object[]
+                        { Integer.valueOf(filesmatched.size())
+                                .toString() });
+                String ttl = MessageManager.getString(
+                        "label.automatically_associate_structure_files_by_name");
+                int choice = JvOptionPane.showConfirmDialog(thisaf, msg,
+                        ttl, JvOptionPane.YES_NO_OPTION);
+                autoAssociate = choice == JvOptionPane.YES_OPTION;
+              }
+              if (autoAssociate)
               {
                 for (Object[] fm : filesmatched)
                 {
@@ -4503,7 +4499,19 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
                       assocfiles++;
                     }
                   }
-                  alignPanel.paintAlignment(true);
+                  // TODO: do we need to update overview ? only if features are
+                  // shown I guess
+                  alignPanel.paintAlignment(true, false);
+                }
+              }
+              else
+              {
+                /*
+                 * add declined structures as sequences
+                 */
+                for (Object[] o : filesmatched)
+                {
+                  filesnotmatched.add((String) o[0]);
                 }
               }
             }
@@ -4638,11 +4646,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             new JnetAnnotationMaker();
             JnetAnnotationMaker.add_annotation(predictions,
                     viewport.getAlignment(), 0, false);
-            SequenceI repseq = viewport.getAlignment().getSequenceAt(0);
-            viewport.getAlignment().setSeqrep(repseq);
-            HiddenColumns cs = new HiddenColumns();
-            cs.hideInsertionsFor(repseq);
-            viewport.getAlignment().setHiddenColumns(cs);
+            viewport.getAlignment().setupJPredAlignment();
             isAnnotation = true;
           }
           // else if (IdentifyFile.FeaturesFile.equals(format))
@@ -4650,7 +4654,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
           {
             if (parseFeaturesFile(file, sourceType))
             {
-              alignPanel.paintAlignment(true);
+              alignPanel.paintAlignment(true, true);
             }
           }
           else
@@ -4665,7 +4669,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
         alignPanel.adjustAnnotationHeight();
         viewport.updateSequenceIdColours();
         buildSortByAnnotationScoresMenu();
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, true);
       }
     } catch (Exception ex)
     {
@@ -4864,14 +4868,15 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
             MessageManager.getString("option.trim_retrieved_seqs"));
     trimrs.setToolTipText(
             MessageManager.getString("label.trim_retrieved_sequences"));
-    trimrs.setSelected(Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true));
+    trimrs.setSelected(
+            Cache.getDefault(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES, true));
     trimrs.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
         trimrs.setSelected(trimrs.isSelected());
-        Cache.setProperty("TRIM_FETCHED_DATASET_SEQS",
+        Cache.setProperty(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES,
                 Boolean.valueOf(trimrs.isSelected()).toString());
       };
     });
@@ -5190,7 +5195,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   protected void showUnconservedMenuItem_actionPerformed(ActionEvent e)
   {
     viewport.setShowUnconserved(showNonconservedMenuItem.getState());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /*
@@ -5279,7 +5284,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     {
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -5291,7 +5296,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       viewport.getAlignment().setSeqrep(null);
       PaintRefresher.Refresh(this, viewport.getSequenceSetId());
       alignPanel.updateAnnotation();
-      alignPanel.paintAlignment(true);
+      alignPanel.paintAlignment(true, true);
     }
   }
 
@@ -5381,7 +5386,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     this.alignPanel.av.setSortAnnotationsBy(getAnnotationSortOrder());
     this.alignPanel.av
             .setShowAutocalculatedAbove(isShowAutoCalculatedAbove());
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -5584,6 +5589,27 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
       new CalculationChooser(AlignFrame.this);
     }
   }
+
+  @Override
+  protected void loadVcf_actionPerformed()
+  {
+    JalviewFileChooser chooser = new JalviewFileChooser(
+            Cache.getProperty("LAST_DIRECTORY"));
+    chooser.setFileView(new JalviewFileView());
+    chooser.setDialogTitle(MessageManager.getString("label.load_vcf_file"));
+    chooser.setToolTipText(MessageManager.getString("label.load_vcf_file"));
+
+    int value = chooser.showOpenDialog(null);
+
+    if (value == JalviewFileChooser.APPROVE_OPTION)
+    {
+      String choice = chooser.getSelectedFile().getPath();
+      Cache.setProperty("LAST_DIRECTORY", choice);
+      SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
+      new VCFLoader(choice).loadVCF(seqs, this);
+    }
+
+  }
 }
 
 class PrintThread extends Thread
index ba83bfb..15c0971 100644 (file)
@@ -22,7 +22,6 @@ package jalview.gui;
 
 import jalview.analysis.AlignmentUtils;
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
-import jalview.analysis.TreeModel;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
 import jalview.api.FeatureColourI;
@@ -36,7 +35,6 @@ import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
-import jalview.datamodel.PDBEntry;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
@@ -58,10 +56,9 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Rectangle;
-import java.util.ArrayList;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Vector;
 
 import javax.swing.JInternalFrame;
 
@@ -76,8 +73,6 @@ public class AlignViewport extends AlignmentViewport
 {
   Font font;
 
-  TreeModel currentTree = null;
-
   boolean cursorMode = false;
 
   boolean antiAlias = false;
@@ -450,27 +445,6 @@ public class AlignViewport extends AlignmentViewport
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param tree
-   *          DOCUMENT ME!
-   */
-  public void setCurrentTree(TreeModel tree)
-  {
-    currentTree = tree;
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @return DOCUMENT ME!
-   */
-  public TreeModel getCurrentTree()
-  {
-    return currentTree;
-  }
-
-  /**
    * returns the visible column regions of the alignment
    * 
    * @param selectedRegionOnly
@@ -478,10 +452,10 @@ public class AlignViewport extends AlignmentViewport
    *          area
    * @return
    */
-  public int[] getViewAsVisibleContigs(boolean selectedRegionOnly)
+  public Iterator<int[]> getViewAsVisibleContigs(boolean selectedRegionOnly)
   {
-    int[] viscontigs = null;
-    int start = 0, end = 0;
+    int start = 0;
+    int end = 0;
     if (selectedRegionOnly && selectionGroup != null)
     {
       start = selectionGroup.getStartRes();
@@ -491,8 +465,8 @@ public class AlignViewport extends AlignmentViewport
     {
       end = alignment.getWidth();
     }
-    viscontigs = alignment.getHiddenColumns().getVisibleContigs(start, end);
-    return viscontigs;
+    return (alignment.getHiddenColumns().getVisContigsIterator(start, end,
+            false));
   }
 
   /**
@@ -608,58 +582,6 @@ public class AlignViewport extends AlignmentViewport
             .getStructureSelectionManager(Desktop.instance);
   }
 
-  /**
-   * 
-   * @param pdbEntries
-   * @return an array of SequenceI arrays, one for each PDBEntry, listing which
-   *         sequences in the alignment hold a reference to it
-   */
-  public SequenceI[][] collateForPDB(PDBEntry[] pdbEntries)
-  {
-    List<SequenceI[]> seqvectors = new ArrayList<SequenceI[]>();
-    for (PDBEntry pdb : pdbEntries)
-    {
-      List<SequenceI> choosenSeqs = new ArrayList<SequenceI>();
-      for (SequenceI sq : alignment.getSequences())
-      {
-        Vector<PDBEntry> pdbRefEntries = sq.getDatasetSequence()
-                .getAllPDBEntries();
-        if (pdbRefEntries == null)
-        {
-          continue;
-        }
-        for (PDBEntry pdbRefEntry : pdbRefEntries)
-        {
-          if (pdbRefEntry.getId().equals(pdb.getId()))
-          {
-            if (pdbRefEntry.getChainCode() != null
-                    && pdb.getChainCode() != null)
-            {
-              if (pdbRefEntry.getChainCode().equalsIgnoreCase(
-                      pdb.getChainCode()) && !choosenSeqs.contains(sq))
-              {
-                choosenSeqs.add(sq);
-                continue;
-              }
-            }
-            else
-            {
-              if (!choosenSeqs.contains(sq))
-              {
-                choosenSeqs.add(sq);
-                continue;
-              }
-            }
-
-          }
-        }
-      }
-      seqvectors
-              .add(choosenSeqs.toArray(new SequenceI[choosenSeqs.size()]));
-    }
-    return seqvectors.toArray(new SequenceI[seqvectors.size()][]);
-  }
-
   @Override
   public boolean isNormaliseSequenceLogo()
   {
@@ -681,7 +603,7 @@ public class AlignViewport extends AlignmentViewport
     return validCharWidth;
   }
 
-  private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<String, AutoCalcSetting>();
+  private Hashtable<String, AutoCalcSetting> calcIdParams = new Hashtable<>();
 
   public AutoCalcSetting getCalcIdSettingsFor(String calcId)
   {
@@ -1112,5 +1034,4 @@ public class AlignViewport extends AlignmentViewport
     }
     fr.setTransparency(featureSettings.getTransparency());
   }
-
 }
index 76368ed..2c5684a 100644 (file)
@@ -48,7 +48,6 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
-import java.awt.Insets;
 import java.awt.event.AdjustmentEvent;
 import java.awt.event.AdjustmentListener;
 import java.awt.event.ComponentAdapter;
@@ -76,8 +75,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
 {
   public AlignViewport av;
 
-  ViewportRanges vpRanges;
-
   OverviewPanel overviewPanel;
 
   private SeqPanel seqPanel;
@@ -97,9 +94,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
   private AnnotationLabels alabels;
 
-  // this value is set false when selection area being dragged
-  boolean fastPaint = true;
-
   private int hextent = 0;
 
   private int vextent = 0;
@@ -124,7 +118,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
   {
     alignFrame = af;
     this.av = av;
-    vpRanges = av.getRanges();
     setSeqPanel(new SeqPanel(av, this));
     setIdPanel(new IdPanel(av, this));
 
@@ -156,11 +149,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
         // reset the viewport ranges when the alignment panel is resized
         // in particular, this initialises the end residue value when Jalview
         // is initialised
+        ViewportRanges ranges = av.getRanges();
         if (av.getWrapAlignment())
         {
           int widthInRes = getSeqPanel().seqCanvas.getWrappedCanvasWidth(
                   getSeqPanel().seqCanvas.getWidth());
-          vpRanges.setViewportWidth(widthInRes);
+          ranges.setViewportWidth(widthInRes);
         }
         else
         {
@@ -169,8 +163,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
           int heightInSeq = getSeqPanel().seqCanvas.getHeight()
                   / av.getCharHeight();
 
-          vpRanges.setViewportWidth(widthInRes);
-          vpRanges.setViewportHeight(heightInSeq);
+          ranges.setViewportWidth(widthInRes);
+          ranges.setViewportHeight(heightInSeq);
         }
       }
 
@@ -214,7 +208,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     alignFrame.updateEditMenuBar();
 
-    paintAlignment(true);
+    // no idea if we need to update structure
+    paintAlignment(true, true);
 
   }
 
@@ -243,11 +238,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
     getIdPanel().getIdCanvas().setPreferredSize(d);
     hscrollFillerPanel.setPreferredSize(d);
 
-    if (this.alignFrame.getSplitViewContainer() != null)
-    {
-      ((SplitFrame) this.alignFrame.getSplitViewContainer()).adjustLayout();
-    }
-
     repaint();
   }
 
@@ -384,6 +374,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
           int verticalOffset, boolean redrawOverview, boolean centre)
   {
     int startv, endv, starts, ends;
+    ViewportRanges ranges = av.getRanges();
 
     if (results == null || results.isEmpty() || av == null
             || av.getAlignment() == null)
@@ -411,7 +402,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
      */
     if (centre)
     {
-      int offset = (vpRanges.getEndRes() - vpRanges.getStartRes() + 1) / 2 - 1;
+      int offset = (ranges.getEndRes() - ranges.getStartRes() + 1) / 2 - 1;
       start = Math.max(start - offset, 0);
       end = end + offset - 1;
     }
@@ -427,8 +418,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (av.hasHiddenColumns())
     {
       HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-      start = hidden.findColumnPosition(start);
-      end = hidden.findColumnPosition(end);
+      start = hidden.absoluteToVisibleColumn(start);
+      end = hidden.absoluteToVisibleColumn(end);
       if (start == end)
       {
         if (!hidden.isVisible(r[0]))
@@ -447,33 +438,33 @@ public class AlignmentPanel extends GAlignmentPanel implements
 
     if (!av.getWrapAlignment())
     {
-      if ((startv = vpRanges.getStartRes()) >= start)
+      if ((startv = ranges.getStartRes()) >= start)
       {
         /*
          * Scroll left to make start of search results visible
          */
         setScrollValues(start, seqIndex);
       }
-      else if ((endv = vpRanges.getEndRes()) <= end)
+      else if ((endv = ranges.getEndRes()) <= end)
       {
         /*
          * Scroll right to make end of search results visible
          */
         setScrollValues(startv + end - endv, seqIndex);
       }
-      else if ((starts = vpRanges.getStartSeq()) > seqIndex)
+      else if ((starts = ranges.getStartSeq()) > seqIndex)
       {
         /*
          * Scroll up to make start of search results visible
          */
-        setScrollValues(vpRanges.getStartRes(), seqIndex);
+        setScrollValues(ranges.getStartRes(), seqIndex);
       }
-      else if ((ends = vpRanges.getEndSeq()) <= seqIndex)
+      else if ((ends = ranges.getEndSeq()) <= seqIndex)
       {
         /*
          * Scroll down to make end of search results visible
          */
-        setScrollValues(vpRanges.getStartRes(), starts + seqIndex - ends
+        setScrollValues(ranges.getStartRes(), starts + seqIndex - ends
                 + 1);
       }
       /*
@@ -483,10 +474,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
     }
     else
     {
-      scrollNeeded = vpRanges.scrollToWrappedVisible(start);
+      scrollNeeded = ranges.scrollToWrappedVisible(start);
     }
 
-    paintAlignment(redrawOverview);
+    paintAlignment(redrawOverview, false);
 
     return scrollNeeded;
   }
@@ -543,7 +534,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
     }
     validateAnnotationDimensions(true);
     addNotify();
-    paintAlignment(true);
+    // TODO: many places call this method and also paintAlignment with various
+    // different settings. this means multiple redraws are triggered...
+    paintAlignment(true, false);
   }
 
   /**
@@ -562,13 +555,10 @@ public class AlignmentPanel extends GAlignmentPanel implements
       /*
        * Estimate available height in the AlignFrame for alignment +
        * annotations. Deduct an estimate for title bar, menu bar, scale panel,
-       * hscroll, status bar (as these are not laid out we can't inspect their
-       * actual heights). Insets gives frame borders.
+       * hscroll, status bar, insets. 
        */
-      int stuff = Platform.isAMac() ? 80 : 100;
-      Insets insets = alignFrame.getInsets();
-      int availableHeight = alignFrame.getHeight() - stuff - insets.top
-              - insets.bottom;
+      int stuff = Platform.isAMac() ? 120 : 140;
+      int availableHeight = alignFrame.getHeight() - stuff;
 
       /*
        * If not enough vertical space, maximize annotation height while keeping
@@ -610,7 +600,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     fontChanged();
     setAnnotationVisible(av.isShowAnnotation());
     boolean wrap = av.getWrapAlignment();
-    vpRanges.setStartSeq(0);
+    ViewportRanges ranges = av.getRanges();
+    ranges.setStartSeq(0);
     scalePanelHolder.setVisible(!wrap);
     hscroll.setVisible(!wrap);
     idwidthAdjuster.setVisible(!wrap);
@@ -633,16 +624,16 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         int widthInRes = getSeqPanel().seqCanvas
                 .getWrappedCanvasWidth(canvasWidth);
-        vpRanges.setViewportWidth(widthInRes);
+        ranges.setViewportWidth(widthInRes);
       }
       else
       {
-        int widthInRes = (canvasWidth / av.getCharWidth()) - 1;
+        int widthInRes = (canvasWidth / av.getCharWidth());
         int heightInSeq = (getSeqPanel().seqCanvas.getHeight()
-                / av.getCharHeight()) - 1;
+                / av.getCharHeight());
 
-        vpRanges.setViewportWidth(widthInRes);
-        vpRanges.setViewportHeight(heightInSeq);
+        ranges.setViewportWidth(widthInRes);
+        ranges.setViewportHeight(heightInSeq);
       }
     }
 
@@ -683,7 +674,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         // reset the width to exclude hidden columns
         width = av.getAlignment().getHiddenColumns()
-                .findColumnPosition(width);
+                .absoluteToVisibleColumn(width);
       }
 
       hextent = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
@@ -741,10 +732,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
       return;
     }
 
+    ViewportRanges ranges = av.getRanges();
+
     if (evt.getSource() == hscroll)
     {
-      int oldX = vpRanges.getStartRes();
-      int oldwidth = vpRanges.getViewportWidth();
+      int oldX = ranges.getStartRes();
+      int oldwidth = ranges.getViewportWidth();
       int x = hscroll.getValue();
       int width = getSeqPanel().seqCanvas.getWidth() / av.getCharWidth();
 
@@ -755,12 +748,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         return;
       }
-      vpRanges.setViewportStartAndWidth(x, width);
+      ranges.setViewportStartAndWidth(x, width);
     }
     else if (evt.getSource() == vscroll)
     {
-      int oldY = vpRanges.getStartSeq();
-      int oldheight = vpRanges.getViewportHeight();
+      int oldY = ranges.getStartSeq();
+      int oldheight = ranges.getViewportHeight();
       int y = vscroll.getValue();
       int height = getSeqPanel().seqCanvas.getHeight() / av.getCharHeight();
 
@@ -771,12 +764,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
       {
         return;
       }
-      vpRanges.setViewportStartAndHeight(y, height);
-    }
-    if (!fastPaint)
-    {
-      repaint();
+      ranges.setViewportStartAndHeight(y, height);
     }
+    repaint();
   }
 
   /**
@@ -791,6 +781,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
     {
       return; // no horizontal scroll when wrapped
     }
+    final ViewportRanges ranges = av.getRanges();
+
     if (evt.getSource() == vscroll)
     {
       int newY = vscroll.getValue();
@@ -800,8 +792,8 @@ public class AlignmentPanel extends GAlignmentPanel implements
        * this prevents infinite recursion of events when the scroll/viewport
        * ranges values are the same
        */
-      int oldX = vpRanges.getStartRes();
-      int oldY = vpRanges.getWrappedScrollPosition(oldX);
+      int oldX = ranges.getStartRes();
+      int oldY = ranges.getWrappedScrollPosition(oldX);
       if (oldY == newY)
       {
         return;
@@ -811,9 +803,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
         /*
          * limit page up/down to one width's worth of positions
          */
-        int rowSize = vpRanges.getViewportWidth();
+        int rowSize = ranges.getViewportWidth();
         int newX = newY > oldY ? oldX + rowSize : oldX - rowSize;
-        vpRanges.setViewportStartAndWidth(Math.max(0, newX), rowSize);
+        ranges.setViewportStartAndWidth(Math.max(0, newX), rowSize);
       }
     }
     else
@@ -834,20 +826,20 @@ public class AlignmentPanel extends GAlignmentPanel implements
                   "Unexpected path through code: Wrapped jar file opened with wrap alignment set in preferences");
 
           // scroll to start of panel
-          vpRanges.setStartRes(0);
-          vpRanges.setStartSeq(0);
+          ranges.setStartRes(0);
+          ranges.setStartSeq(0);
         }
       });
     }
     repaint();
   }
 
-  /**
-   * Repaint the alignment including the annotations and overview panels (if
-   * shown).
+  /* (non-Javadoc)
+   * @see jalview.api.AlignmentViewPanel#paintAlignment(boolean)
    */
   @Override
-  public void paintAlignment(boolean updateOverview)
+  public void paintAlignment(boolean updateOverview,
+          boolean updateStructures)
   {
     final AnnotationSorter sorter = new AnnotationSorter(getAlignment(),
             av.isShowAutocalculatedAbove());
@@ -855,10 +847,12 @@ public class AlignmentPanel extends GAlignmentPanel implements
             av.getSortAnnotationsBy());
     repaint();
 
-    if (updateOverview)
+    if (updateStructures)
     {
-      // TODO: determine if this paintAlignment changed structure colours
       av.getStructureSelectionManager().sequenceColoursChanged(this);
+    }
+    if (updateOverview)
+    {
 
       if (overviewPanel != null)
       {
@@ -876,17 +870,20 @@ public class AlignmentPanel extends GAlignmentPanel implements
   @Override
   public void paintComponent(Graphics g)
   {
-    invalidate();
+    invalidate(); // needed so that the id width adjuster works correctly
 
     Dimension d = getIdPanel().getIdCanvas().getPreferredSize();
     idPanelHolder.setPreferredSize(d);
     hscrollFillerPanel.setPreferredSize(new Dimension(d.width, 12));
-    validate();
+
+    validate(); // needed so that the id width adjuster works correctly
 
     /*
-     * set scroll bar positions
+     * set scroll bar positions - tried to remove but necessary for split panel to resize correctly
+     * though I still think this call should be elsewhere.
      */
-    setScrollValues(vpRanges.getStartRes(), vpRanges.getStartSeq());
+    ViewportRanges ranges = av.getRanges();
+    setScrollValues(ranges.getStartRes(), ranges.getStartSeq());
   }
 
   /**
@@ -898,8 +895,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   private void setScrollingForWrappedPanel(int topLeftColumn)
   {
-    int scrollPosition = vpRanges.getWrappedScrollPosition(topLeftColumn);
-    int maxScroll = vpRanges.getWrappedMaxScroll(topLeftColumn);
+    ViewportRanges ranges = av.getRanges();
+    int scrollPosition = ranges.getWrappedScrollPosition(topLeftColumn);
+    int maxScroll = ranges.getWrappedMaxScroll(topLeftColumn);
 
     /*
      * a scrollbar's value can be set to at most (maximum-extent)
@@ -1174,7 +1172,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     int resWidth = getSeqPanel().seqCanvas
@@ -1366,7 +1364,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth);
+              .absoluteToVisibleColumn(maxwidth);
     }
 
     int height = ((av.getAlignment().getHeight() + 1) * av.getCharHeight())
@@ -1584,7 +1582,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     int height = ((maxwidth / chunkWidth) + 1) * cHeight;
@@ -1610,13 +1608,14 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (annotationPanel != null)
     {
       annotationPanel.dispose();
+      annotationPanel = null;
     }
 
     if (av != null)
     {
       av.removePropertyChangeListener(propertyChangeListener);
-      jalview.structure.StructureSelectionManager ssm = av
-              .getStructureSelectionManager();
+      propertyChangeListener = null;
+      StructureSelectionManager ssm = av.getStructureSelectionManager();
       ssm.removeStructureViewerListener(getSeqPanel(), null);
       ssm.removeSelectionListener(getSeqPanel());
       ssm.removeCommandListener(av);
@@ -1639,9 +1638,15 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   protected void closeChildFrames()
   {
+    if (overviewPanel != null)
+    {
+      overviewPanel.dispose();
+      overviewPanel = null;
+    }
     if (calculationDialog != null)
     {
       calculationDialog.closeFrame();
+      calculationDialog = null;
     }
   }
 
@@ -1806,35 +1811,6 @@ public class AlignmentPanel extends GAlignmentPanel implements
    */
   protected void scrollToCentre(SearchResultsI sr, int verticalOffset)
   {
-    /*
-     * To avoid jumpy vertical scrolling (if some sequences are gapped or not
-     * mapped), we can make the scroll-to location a sequence above the one
-     * actually mapped.
-     */
-    SequenceI mappedTo = sr.getResults().get(0).getSequence();
-    List<SequenceI> seqs = av.getAlignment().getSequences();
-
-    /*
-     * This is like AlignmentI.findIndex(seq) but here we are matching the
-     * dataset sequence not the aligned sequence
-     */
-    boolean matched = false;
-    for (SequenceI seq : seqs)
-    {
-      if (mappedTo == seq.getDatasetSequence())
-      {
-        matched = true;
-        break;
-      }
-    }
-    if (!matched)
-    {
-      return; // failsafe, shouldn't happen
-    }
-
-    /*
-     * Scroll to position but centring the target residue.
-     */
     scrollToPosition(sr, verticalOffset, true, true);
   }
 
@@ -1871,7 +1847,7 @@ public class AlignmentPanel extends GAlignmentPanel implements
     if (adjustHeight)
     {
       // sort, repaint, update overview
-      paintAlignment(true);
+      paintAlignment(true, false);
     }
     else
     {
@@ -1888,8 +1864,9 @@ public class AlignmentPanel extends GAlignmentPanel implements
   public void propertyChange(PropertyChangeEvent evt)
   {
     // update this panel's scroll values based on the new viewport ranges values
-    int x = vpRanges.getStartRes();
-    int y = vpRanges.getStartSeq();
+    ViewportRanges ranges = av.getRanges();
+    int x = ranges.getStartRes();
+    int y = ranges.getStartSeq();
     setScrollValues(x, y);
 
     // now update any complementary alignment (its viewport ranges object
index 26796de..84883d7 100644 (file)
@@ -82,7 +82,7 @@ public class AnnotationChooser extends JPanel
   private boolean applyToUnselectedSequences;
 
   // currently selected 'annotation type' checkboxes
-  private Map<String, String> selectedTypes = new HashMap<String, String>();
+  private Map<String, String> selectedTypes = new HashMap<>();
 
   /**
    * Constructor.
@@ -202,7 +202,7 @@ public class AnnotationChooser extends JPanel
     // this.ap.alabels.setSize(this.ap.alabels.getSize().width,
     // this.ap.annotationPanel.getSize().height);
     // this.ap.validate();
-    this.ap.paintAlignment(true);
+    this.ap.paintAlignment(true, false);
   }
 
   /**
@@ -233,7 +233,7 @@ public class AnnotationChooser extends JPanel
     // this.ap.alabels.setSize(this.ap.alabels.getSize().width,
     // this.ap.annotationPanel.getSize().height);
     // this.ap.validate();
-    this.ap.paintAlignment(true);
+    this.ap.paintAlignment(true, false);
   }
 
   /**
@@ -251,7 +251,7 @@ public class AnnotationChooser extends JPanel
 
     this.ap.updateAnnotation();
     // this.ap.annotationPanel.adjustPanelHeight();
-    this.ap.paintAlignment(true);
+    this.ap.paintAlignment(true, false);
   }
 
   /**
@@ -356,7 +356,7 @@ public class AnnotationChooser extends JPanel
   public static List<String> getAnnotationTypes(AlignmentI alignment,
           boolean sequenceSpecificOnly)
   {
-    List<String> result = new ArrayList<String>();
+    List<String> result = new ArrayList<>();
     for (AlignmentAnnotation aa : alignment.getAlignmentAnnotation())
     {
       if (!sequenceSpecificOnly || aa.sequenceRef != null)
index 8d123bb..384635b 100644 (file)
@@ -79,7 +79,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     oldcs = av.getGlobalColourScheme();
     if (av.getAlignment().getGroups() != null)
     {
-      oldgroupColours = new Hashtable<SequenceGroup, ColourSchemeI>();
+      oldgroupColours = new Hashtable<>();
       for (SequenceGroup sg : ap.av.getAlignment().getGroups())
       {
         if (sg.getColourScheme() != null)
@@ -122,7 +122,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     }
     Vector<String> annotItems = getAnnotationItems(
             seqAssociated.isSelected());
-    annotations = new JComboBox<String>(annotItems);
+    annotations = new JComboBox<>(annotItems);
 
     populateThresholdComboBox(threshold);
 
@@ -341,7 +341,7 @@ public class AnnotationColourChooser extends AnnotationRowFilter
       getCurrentAnnotation().threshold.value = slider.getValue() / 1000f;
       propagateSeqAssociatedThreshold(updateAllAnnotation,
               getCurrentAnnotation());
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -415,12 +415,9 @@ public class AnnotationColourChooser extends AnnotationRowFilter
     colorAlignmentContaining(getCurrentAnnotation(), selectedThresholdItem);
 
     ap.alignmentChanged();
-    // ensure all associated views (overviews, structures, etc) are notified of
-    // updated colours.
-    ap.paintAlignment(true);
   }
 
-  protected boolean colorAlignmentContaining(AlignmentAnnotation currentAnn,
+  protected void colorAlignmentContaining(AlignmentAnnotation currentAnn,
           int selectedThresholdOption)
   {
 
@@ -460,7 +457,13 @@ public class AnnotationColourChooser extends AnnotationRowFilter
                 acg.getInstance(sg, ap.av.getHiddenRepSequences()));
       }
     }
-    return false;
+  }
+
+  @Override
+  protected void sliderDragReleased()
+  {
+    super.sliderDragReleased();
+    ap.paintAlignment(true, true);
   }
 
 }
index 84b2c6f..6924b63 100644 (file)
@@ -36,7 +36,6 @@ import java.awt.event.ActionListener;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 import java.awt.event.KeyEvent;
-import java.util.ArrayList;
 
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBox;
@@ -241,20 +240,10 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
       {
         HiddenColumns oldHidden = av.getAnnotationColumnSelectionState()
                 .getOldHiddenColumns();
-        if (oldHidden != null)
-        {
-          ArrayList<int[]> regions = oldHidden.getHiddenColumnsCopy();
-          for (int[] positions : regions)
-          {
-            av.hideColumns(positions[0], positions[1]);
-          }
-        }
-        // TODO not clear why we need to hide all the columns (above) if we are
-        // going to copy the hidden columns over wholesale anyway
         av.getAlignment().setHiddenColumns(oldHidden);
       }
       av.sendSelection();
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
   }
 
@@ -267,7 +256,7 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
       updateView();
       propagateSeqAssociatedThreshold(updateAllAnnotation,
               getCurrentAnnotation());
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -391,7 +380,8 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
     av.getColumnSelection().filterAnnotations(
             getCurrentAnnotation().annotations, filterParams);
 
-    if (getActionOption() == ACTION_OPTION_HIDE)
+    boolean hideCols = getActionOption() == ACTION_OPTION_HIDE;
+    if (hideCols)
     {
       av.hideSelectedColumns();
     }
@@ -399,7 +389,8 @@ public class AnnotationColumnChooser extends AnnotationRowFilter
 
     filterParams = null;
     av.setAnnotationColumnSelectionState(this);
-    ap.paintAlignment(true);
+    // only update overview and structures if columns were hidden
+    ap.paintAlignment(hideCols, hideCols);
   }
 
   public HiddenColumns getOldHiddenColumns()
index a619997..6fefbd0 100644 (file)
 package jalview.gui;
 
 import jalview.api.FeatureColourI;
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.io.AnnotationFile;
 import jalview.io.FeaturesFile;
 import jalview.io.JalviewFileChooser;
@@ -34,6 +36,8 @@ import java.awt.Color;
 import java.awt.FlowLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.io.FileWriter;
+import java.io.PrintWriter;
 import java.util.List;
 import java.util.Map;
 
@@ -57,18 +61,22 @@ import javax.swing.SwingConstants;
  */
 public class AnnotationExporter extends JPanel
 {
-  JInternalFrame frame;
+  private JInternalFrame frame;
 
-  AlignmentPanel ap;
+  private AlignmentPanel ap;
 
-  boolean features = true;
+  /*
+   * true if exporting features, false if exporting annotations
+   */
+  private boolean exportFeatures = true;
 
   private AlignmentAnnotation[] annotations;
 
   private boolean wholeView;
 
-  public AnnotationExporter()
+  public AnnotationExporter(AlignmentPanel panel)
   {
+    this.ap = panel;
     try
     {
       jbInit();
@@ -84,47 +92,54 @@ public class AnnotationExporter extends JPanel
             frame.getPreferredSize().height);
   }
 
-  public void exportFeatures(AlignmentPanel ap)
+  /**
+   * Configures the diglog for options to export visible features
+   */
+  public void exportFeatures()
   {
-    this.ap = ap;
-    features = true;
+    exportFeatures = true;
     CSVFormat.setVisible(false);
     frame.setTitle(MessageManager.getString("label.export_features"));
   }
 
-  public void exportAnnotations(AlignmentPanel ap)
+  /**
+   * Configures the dialog for options to export all visible annotations
+   */
+  public void exportAnnotations()
   {
-    this.ap = ap;
-    annotations = ap.av.isShowAnnotation() ? null
-            : ap.av.getAlignment().getAlignmentAnnotation();
-    wholeView = true;
-    startExportAnnotation();
+    boolean showAnnotation = ap.av.isShowAnnotation();
+    exportAnnotation(showAnnotation ? null
+            : ap.av.getAlignment().getAlignmentAnnotation(), true);
   }
 
-  public void exportAnnotations(AlignmentPanel alp,
-          AlignmentAnnotation[] toExport)
+  /**
+   * Configures the dialog for options to export the given annotation row
+   * 
+   * @param toExport
+   */
+  public void exportAnnotation(AlignmentAnnotation toExport)
   {
-    ap = alp;
-    annotations = toExport;
-    wholeView = false;
-    startExportAnnotation();
+    exportAnnotation(new AlignmentAnnotation[] { toExport }, false);
   }
 
-  private void startExportAnnotation()
+  private void exportAnnotation(AlignmentAnnotation[] toExport,
+          boolean forWholeView)
   {
-    features = false;
+    wholeView = forWholeView;
+    annotations = toExport;
+    exportFeatures = false;
     GFFFormat.setVisible(false);
     CSVFormat.setVisible(true);
     frame.setTitle(MessageManager.getString("label.export_annotations"));
   }
 
-  public void toFile_actionPerformed(ActionEvent e)
+  private void toFile_actionPerformed()
   {
     JalviewFileChooser chooser = new JalviewFileChooser(
-            jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
+            Cache.getProperty("LAST_DIRECTORY"));
 
     chooser.setFileView(new JalviewFileView());
-    chooser.setDialogTitle(features
+    chooser.setDialogTitle(exportFeatures
             ? MessageManager.getString("label.save_features_to_file")
             : MessageManager.getString("label.save_annotation_to_file"));
     chooser.setToolTipText(MessageManager.getString("action.save"));
@@ -133,13 +148,12 @@ public class AnnotationExporter extends JPanel
 
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
-      String text = getFileContents();
+      String text = getText();
 
       try
       {
-        java.io.PrintWriter out = new java.io.PrintWriter(
-                new java.io.FileWriter(chooser.getSelectedFile()));
-
+        PrintWriter out = new PrintWriter(
+                new FileWriter(chooser.getSelectedFile()));
         out.print(text);
         out.close();
       } catch (Exception ex)
@@ -148,64 +162,89 @@ public class AnnotationExporter extends JPanel
       }
     }
 
-    close_actionPerformed(null);
+    close_actionPerformed();
+  }
+
+  /**
+   * Answers the text to output for either Features (in GFF or Jalview format) or
+   * Annotations (in CSV or Jalview format)
+   * 
+   * @return
+   */
+  private String getText()
+  {
+    return exportFeatures ? getFeaturesText() : getAnnotationsText();
   }
 
-  private String getFileContents()
+  /**
+   * Returns the text contents for output of annotations in either CSV or Jalview
+   * format
+   * 
+   * @return
+   */
+  private String getAnnotationsText()
   {
-    String text = MessageManager
-            .getString("label.no_features_on_alignment");
-    if (features)
+    String text;
+    if (CSVFormat.isSelected())
     {
-      FeaturesFile formatter = new FeaturesFile();
-      SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
-      Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
-              .getDisplayedFeatureCols();
-      List<String> featureGroups = ap.getFeatureRenderer()
-              .getDisplayedFeatureGroups();
-      boolean includeNonPositional = ap.av.isShowNPFeats();
-      if (GFFFormat.isSelected())
-      {
-        text = formatter.printGffFormat(sequences, featureColours,
-                featureGroups, includeNonPositional);
-      }
-      else
-      {
-        text = formatter.printJalviewFormat(sequences, featureColours,
-                featureGroups, includeNonPositional);
-      }
+      text = new AnnotationFile().printCSVAnnotations(annotations);
     }
     else
     {
-      if (CSVFormat.isSelected())
+      if (wholeView)
       {
-        text = new AnnotationFile().printCSVAnnotations(annotations);
+        text = new AnnotationFile().printAnnotationsForView(ap.av);
       }
       else
       {
-        if (wholeView)
-        {
-          text = new AnnotationFile().printAnnotationsForView(ap.av);
-        }
-        else
-        {
-          text = new AnnotationFile().printAnnotations(annotations, null,
-                  null);
-        }
+        text = new AnnotationFile().printAnnotations(annotations, null,
+                null);
       }
     }
     return text;
   }
 
-  public void toTextbox_actionPerformed(ActionEvent e)
+  /**
+   * Returns the text contents for output of features in either GFF or Jalview
+   * format
+   * 
+   * @return
+   */
+  private String getFeaturesText()
+  {
+    String text;
+    SequenceI[] sequences = ap.av.getAlignment().getSequencesArray();
+    Map<String, FeatureColourI> featureColours = ap.getFeatureRenderer()
+            .getDisplayedFeatureCols();
+    Map<String, FeatureMatcherSetI> featureFilters = ap.getFeatureRenderer()
+            .getFeatureFilters();
+    List<String> featureGroups = ap.getFeatureRenderer()
+            .getDisplayedFeatureGroups();
+    boolean includeNonPositional = ap.av.isShowNPFeats();
+
+    FeaturesFile formatter = new FeaturesFile();
+    if (GFFFormat.isSelected())
+    {
+      text = formatter.printGffFormat(sequences, featureColours,
+              featureGroups, includeNonPositional);
+    }
+    else
+    {
+      text = formatter.printJalviewFormat(sequences, featureColours,
+              featureFilters, featureGroups, includeNonPositional);
+    }
+    return text;
+  }
+
+  private void toTextbox_actionPerformed()
   {
     CutAndPasteTransfer cap = new CutAndPasteTransfer();
 
     try
     {
-      String text = getFileContents();
+      String text = getText();
       cap.setText(text);
-      Desktop.addInternalFrame(cap, (features ? MessageManager
+      Desktop.addInternalFrame(cap, (exportFeatures ? MessageManager
               .formatMessage("label.features_for_params", new String[]
               { ap.alignFrame.getTitle() })
               : MessageManager.formatMessage("label.annotations_for_params",
@@ -214,7 +253,7 @@ public class AnnotationExporter extends JPanel
               600, 500);
     } catch (OutOfMemoryError oom)
     {
-      new OOMWarning((features ? MessageManager.formatMessage(
+      new OOMWarning((exportFeatures ? MessageManager.formatMessage(
               "label.generating_features_for_params", new String[]
               { ap.alignFrame.getTitle() })
               : MessageManager.formatMessage(
@@ -225,10 +264,10 @@ public class AnnotationExporter extends JPanel
       cap.dispose();
     }
 
-    close_actionPerformed(null);
+    close_actionPerformed();
   }
 
-  public void close_actionPerformed(ActionEvent e)
+  private void close_actionPerformed()
   {
     try
     {
@@ -248,7 +287,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        toFile_actionPerformed(e);
+        toFile_actionPerformed();
       }
     });
     toTextbox.setText(MessageManager.getString("label.to_textbox"));
@@ -257,7 +296,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        toTextbox_actionPerformed(e);
+        toTextbox_actionPerformed();
       }
     });
     close.setText(MessageManager.getString("action.close"));
@@ -266,7 +305,7 @@ public class AnnotationExporter extends JPanel
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        close_actionPerformed(e);
+        close_actionPerformed();
       }
     });
     jalviewFormat.setOpaque(false);
index d07cae2..6f8b225 100755 (executable)
  */
 package jalview.gui;
 
+import jalview.analysis.AlignSeq;
 import jalview.analysis.AlignmentUtils;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
+import jalview.util.Comparison;
 import jalview.util.MessageManager;
+import jalview.util.Platform;
 
 import java.awt.Color;
+import java.awt.Cursor;
 import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
-import java.awt.Image;
-import java.awt.MediaTracker;
 import java.awt.RenderingHints;
 import java.awt.Toolkit;
 import java.awt.datatransfer.StringSelection;
@@ -52,6 +55,7 @@ import java.awt.image.BufferedImage;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.regex.Pattern;
 
 import javax.swing.JCheckBoxMenuItem;
@@ -62,63 +66,74 @@ import javax.swing.SwingUtilities;
 import javax.swing.ToolTipManager;
 
 /**
- * DOCUMENT ME!
- * 
- * @author $author$
- * @version $Revision$
+ * The panel that holds the labels for alignment annotations, providing
+ * tooltips, context menus, drag to reorder rows, and drag to adjust panel
+ * height
  */
 public class AnnotationLabels extends JPanel
         implements MouseListener, MouseMotionListener, ActionListener
 {
+  /**
+   * width in pixels within which height adjuster arrows are shown and active
+   */
+  private static final int HEIGHT_ADJUSTER_WIDTH = 50;
+
+  /**
+   * height in pixels for allowing height adjuster to be active
+   */
+  private static int HEIGHT_ADJUSTER_HEIGHT = 10;
+
   private static final Pattern LEFT_ANGLE_BRACKET_PATTERN = Pattern
           .compile("<");
 
-  String TOGGLE_LABELSCALE = MessageManager
+  private static final Font font = new Font("Arial", Font.PLAIN, 11);
+
+  private static final String TOGGLE_LABELSCALE = MessageManager
           .getString("label.scale_label_to_column");
 
-  String ADDNEW = MessageManager.getString("label.add_new_row");
+  private static final String ADDNEW = MessageManager
+          .getString("label.add_new_row");
 
-  String EDITNAME = MessageManager
+  private static final String EDITNAME = MessageManager
           .getString("label.edit_label_description");
 
-  String HIDE = MessageManager.getString("label.hide_row");
+  private static final String HIDE = MessageManager
+          .getString("label.hide_row");
 
-  String DELETE = MessageManager.getString("label.delete_row");
+  private static final String DELETE = MessageManager
+          .getString("label.delete_row");
 
-  String SHOWALL = MessageManager.getString("label.show_all_hidden_rows");
+  private static final String SHOWALL = MessageManager
+          .getString("label.show_all_hidden_rows");
 
-  String OUTPUT_TEXT = MessageManager.getString("label.export_annotation");
+  private static final String OUTPUT_TEXT = MessageManager
+          .getString("label.export_annotation");
 
-  String COPYCONS_SEQ = MessageManager
+  private static final String COPYCONS_SEQ = MessageManager
           .getString("label.copy_consensus_sequence");
 
-  boolean resizePanel = false;
-
-  Image image;
+  private final boolean debugRedraw = false;
 
-  AlignmentPanel ap;
+  private AlignmentPanel ap;
 
   AlignViewport av;
 
-  boolean resizing = false;
-
-  MouseEvent dragEvent;
+  private MouseEvent dragEvent;
 
-  int oldY;
+  private int oldY;
 
-  int selectedRow;
+  private int selectedRow;
 
   private int scrollOffset = 0;
 
-  Font font = new Font("Arial", Font.PLAIN, 11);
-
   private boolean hasHiddenRows;
 
+  private boolean resizePanel = false;
+
   /**
-   * Creates a new AnnotationLabels object.
+   * Creates a new AnnotationLabels object
    * 
    * @param ap
-   *          DOCUMENT ME!
    */
   public AnnotationLabels(AlignmentPanel ap)
   {
@@ -126,30 +141,6 @@ public class AnnotationLabels extends JPanel
     av = ap.av;
     ToolTipManager.sharedInstance().registerComponent(this);
 
-    java.net.URL url = getClass().getResource("/images/idwidth.gif");
-    Image temp = null;
-
-    if (url != null)
-    {
-      temp = java.awt.Toolkit.getDefaultToolkit().createImage(url);
-    }
-
-    try
-    {
-      MediaTracker mt = new MediaTracker(this);
-      mt.addImage(temp, 0);
-      mt.waitForID(0);
-    } catch (Exception ex)
-    {
-    }
-
-    BufferedImage bi = new BufferedImage(temp.getHeight(this),
-            temp.getWidth(this), BufferedImage.TYPE_INT_RGB);
-    Graphics2D g = (Graphics2D) bi.getGraphics();
-    g.rotate(Math.toRadians(90));
-    g.drawImage(temp, 0, -bi.getWidth(this), this);
-    image = bi;
-
     addMouseListener(this);
     addMouseMotionListener(this);
     addMouseWheelListener(ap.getAnnotationPanel());
@@ -266,9 +257,7 @@ public class AnnotationLabels extends JPanel
     }
     else if (evt.getActionCommand().equals(OUTPUT_TEXT))
     {
-      new AnnotationExporter().exportAnnotations(ap,
-              new AlignmentAnnotation[]
-              { aa[selectedRow] });
+      new AnnotationExporter(ap).exportAnnotation(aa[selectedRow]);
     }
     else if (evt.getActionCommand().equals(COPYCONS_SEQ))
     {
@@ -607,10 +596,9 @@ public class AnnotationLabels extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Reorders annotation rows after a drag of a label
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseReleased(MouseEvent evt)
@@ -625,6 +613,9 @@ public class AnnotationLabels extends JPanel
     getSelectedRow(evt.getY() - getScrollOffset());
     int end = selectedRow;
 
+    /*
+     * if dragging to resize instead, start == end
+     */
     if (start != end)
     {
       // Swap these annotations
@@ -648,31 +639,13 @@ public class AnnotationLabels extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
-   * 
-   * @param evt
-   *          DOCUMENT ME!
-   */
-  @Override
-  public void mouseEntered(MouseEvent evt)
-  {
-    if (evt.getY() < 10)
-    {
-      resizePanel = true;
-      repaint();
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param evt
-   *          DOCUMENT ME!
+   * Removes the height adjuster image on leaving the panel, unless currently
+   * dragging it
    */
   @Override
   public void mouseExited(MouseEvent evt)
   {
-    if (dragEvent == null)
+    if (resizePanel && dragEvent == null)
     {
       resizePanel = false;
       repaint();
@@ -680,10 +653,11 @@ public class AnnotationLabels extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * A mouse drag may be either an adjustment of the panel height (if flag
+   * resizePanel is set on), or a reordering of the annotation rows. The former
+   * is dealt with by this method, the latter in mouseReleased.
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseDragged(MouseEvent evt)
@@ -705,7 +679,7 @@ public class AnnotationLabels extends JPanel
         d = ap.annotationSpaceFillerHolder.getPreferredSize();
         ap.annotationSpaceFillerHolder
                 .setPreferredSize(new Dimension(d.width, d.height - dif));
-        ap.paintAlignment(true);
+        ap.paintAlignment(true, false);
       }
 
       ap.addNotify();
@@ -717,15 +691,14 @@ public class AnnotationLabels extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Updates the tooltip as the mouse moves over the labels
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseMoved(MouseEvent evt)
   {
-    resizePanel = evt.getY() < 10;
+    showOrHideAdjuster(evt);
 
     getSelectedRow(evt.getY() - getScrollOffset());
 
@@ -801,6 +774,26 @@ public class AnnotationLabels extends JPanel
     }
   }
 
+  /**
+   * Shows the height adjuster image if the mouse moves into the top left
+   * region, or hides it if the mouse leaves the regio
+   * 
+   * @param evt
+   */
+  protected void showOrHideAdjuster(MouseEvent evt)
+  {
+    boolean was = resizePanel;
+    resizePanel = evt.getY() < HEIGHT_ADJUSTER_HEIGHT && evt.getX() < HEIGHT_ADJUSTER_WIDTH;
+
+    if (resizePanel != was)
+    {
+      setCursor(Cursor.getPredefinedCursor(
+              resizePanel ? Cursor.S_RESIZE_CURSOR
+                      : Cursor.DEFAULT_CURSOR));
+      repaint();
+    }
+  }
+
   @Override
   public void mouseClicked(MouseEvent evt)
   {
@@ -820,11 +813,9 @@ public class AnnotationLabels extends JPanel
             // process modifiers
             SequenceGroup sg = ap.av.getSelectionGroup();
             if (sg == null || sg == aa[selectedRow].groupRef
-                    || !(jalview.util.Platform.isControlDown(evt)
-                            || evt.isShiftDown()))
+                    || !(Platform.isControlDown(evt) || evt.isShiftDown()))
             {
-              if (jalview.util.Platform.isControlDown(evt)
-                      || evt.isShiftDown())
+              if (Platform.isControlDown(evt) || evt.isShiftDown())
               {
                 // clone a new selection group from the associated group
                 ap.av.setSelectionGroup(
@@ -855,7 +846,7 @@ public class AnnotationLabels extends JPanel
               }
             }
 
-            ap.paintAlignment(false);
+            ap.paintAlignment(false, false);
             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
             ap.av.sendSelection();
           }
@@ -883,8 +874,7 @@ public class AnnotationLabels extends JPanel
               // we make a copy rather than edit the current selection if no
               // modifiers pressed
               // see Enhancement JAL-1557
-              if (!(jalview.util.Platform.isControlDown(evt)
-                      || evt.isShiftDown()))
+              if (!(Platform.isControlDown(evt) || evt.isShiftDown()))
               {
                 sg = new SequenceGroup(sg);
                 sg.clear();
@@ -892,7 +882,7 @@ public class AnnotationLabels extends JPanel
               }
               else
               {
-                if (jalview.util.Platform.isControlDown(evt))
+                if (Platform.isControlDown(evt))
                 {
                   sg.addOrRemove(aa[selectedRow].sequenceRef, true);
                 }
@@ -912,7 +902,7 @@ public class AnnotationLabels extends JPanel
               sg.addSequence(aa[selectedRow].sequenceRef, false);
             }
             ap.av.setSelectionGroup(sg);
-            ap.paintAlignment(false);
+            ap.paintAlignment(false, false);
             PaintRefresher.Refresh(ap, ap.av.getSequenceSetId());
             ap.av.sendSelection();
           }
@@ -937,16 +927,17 @@ public class AnnotationLabels extends JPanel
     if (dseqs[0] == null)
     {
       dseqs[0] = new Sequence(sq);
-      dseqs[0].setSequence(jalview.analysis.AlignSeq.extractGaps(
-              jalview.util.Comparison.GapChars, sq.getSequenceAsString()));
+      dseqs[0].setSequence(AlignSeq.extractGaps(Comparison.GapChars,
+              sq.getSequenceAsString()));
 
       sq.setDatasetSequence(dseqs[0]);
     }
     Alignment ds = new Alignment(dseqs);
     if (av.hasHiddenColumns())
     {
-      omitHidden = av.getAlignment().getHiddenColumns()
-              .getVisibleSequenceStrings(0, sq.getLength(), seqs);
+      Iterator<int[]> it = av.getAlignment().getHiddenColumns()
+              .getVisContigsIterator(0, sq.getLength(), false);
+      omitHidden = new String[] { sq.getSequenceStringFromIterator(it) };
     }
 
     int[] alignmentStartEnd = new int[] { 0, ds.getWidth() - 1 };
@@ -962,12 +953,12 @@ public class AnnotationLabels extends JPanel
     Toolkit.getDefaultToolkit().getSystemClipboard()
             .setContents(new StringSelection(output), Desktop.instance);
 
-    ArrayList<int[]> hiddenColumns = null;
+    HiddenColumns hiddenColumns = null;
 
     if (av.hasHiddenColumns())
     {
-      hiddenColumns = av.getAlignment().getHiddenColumns()
-              .getHiddenColumnsCopy();
+      hiddenColumns = new HiddenColumns(
+              av.getAlignment().getHiddenColumns());
     }
 
     Desktop.jalviewClipboard = new Object[] { seqs, ds, // what is the dataset
@@ -1020,8 +1011,6 @@ public class AnnotationLabels extends JPanel
     drawComponent(g, false, width);
   }
 
-  private final boolean debugRedraw = false;
-
   /**
    * Draw the full set of annotation Labels for the alignment at the given
    * cursor
@@ -1204,11 +1193,7 @@ public class AnnotationLabels extends JPanel
       }
     }
 
-    if (resizePanel)
-    {
-      g.drawImage(image, 2, 0 - getScrollOffset(), this);
-    }
-    else if (dragEvent != null && aa != null)
+    if (!resizePanel && dragEvent != null && aa != null)
     {
       g.setColor(Color.lightGray);
       g.drawString(aa[selectedRow].label, dragEvent.getX(),
@@ -1227,4 +1212,9 @@ public class AnnotationLabels extends JPanel
   {
     return scrollOffset;
   }
+
+  @Override
+  public void mouseEntered(MouseEvent e)
+  {
+  }
 }
index be8f5f6..dee56b0 100755 (executable)
@@ -175,11 +175,12 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     if (e.isShiftDown())
     {
       e.consume();
-      if (e.getWheelRotation() > 0)
+      double wheelRotation = e.getPreciseWheelRotation();
+      if (wheelRotation > 0)
       {
         av.getRanges().scrollRight(true);
       }
-      else
+      else if (wheelRotation < 0)
       {
         av.getRanges().scrollRight(false);
       }
@@ -670,7 +671,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       }
       graphStretchY = evt.getY();
       adjustPanelHeight();
-      ap.paintAlignment(true);
+      ap.paintAlignment(false, false);
     }
     else
     {
@@ -724,7 +725,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     if (av.hasHiddenColumns())
     {
       column = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(column);
+              .visibleToAbsoluteColumn(column);
     }
 
     AlignmentAnnotation ann = aa[row];
@@ -782,6 +783,10 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       {
         this.setToolTipText(JvSwingUtils.wrapTooltip(true, description));
       }
+      else
+      {
+        this.setToolTipText(null); // no tooltip if null or empty description
+      }
     }
     else
     {
@@ -904,6 +909,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
   @Override
   public void paintComponent(Graphics g)
   {
+    super.paintComponent(g);
+
     g.setColor(Color.white);
     g.fillRect(0, 0, getWidth(), getHeight());
 
@@ -959,7 +966,7 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
       gg.fillRect(0, 0, imgWidth, image.getHeight());
       imageFresh = true;
     }
-
+    
     drawComponent(gg, av.getRanges().getStartRes(),
             av.getRanges().getEndRes() + 1);
     imageFresh = false;
@@ -992,10 +999,8 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     int er = av.getRanges().getEndRes() + 1;
     int transX = 0;
 
-    long stime = System.currentTimeMillis();
     gg.copyArea(0, 0, imgWidth, getHeight(),
             -horizontal * av.getCharWidth(), 0);
-    long mtime = System.currentTimeMillis();
 
     if (horizontal > 0) // scrollbar pulled right, image to the left
     {
@@ -1012,17 +1017,13 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     drawComponent(gg, sr, er);
 
     gg.translate(-transX, 0);
-    long dtime = System.currentTimeMillis();
+
     fastPaint = true;
-    repaint();
-    long rtime = System.currentTimeMillis();
-    if (debugRedraw)
-    {
-      System.err.println("Scroll:\t" + horizontal + "\tCopyArea:\t"
-              + (mtime - stime) + "\tDraw component:\t" + (dtime - mtime)
-              + "\tRepaint call:\t" + (rtime - dtime));
-    }
 
+    // Call repaint on alignment panel so that repaints from other alignment
+    // panel components can be aggregated. Otherwise performance of the overview
+    // window and others may be adversely affected.
+    av.getAlignPanel().repaint();
   }
 
   private volatile boolean lastImageGood = false;
@@ -1185,5 +1186,14 @@ public class AnnotationPanel extends JPanel implements AwtRenderPanelI,
     {
       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
     }
+    else if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      fastPaint(((int[]) evt.getNewValue())[0]
+              - ((int[]) evt.getOldValue())[0]);
+    }
+    else if (evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      repaint();
+    }
   }
 }
index 8b2486f..f13cb10 100644 (file)
@@ -80,7 +80,7 @@ public abstract class AnnotationRowFilter extends JPanel
    */
   protected boolean sliderDragging = false;
 
-  protected JComboBox<String> threshold = new JComboBox<String>();
+  protected JComboBox<String> threshold = new JComboBox<>();
 
   protected JComboBox<String> annotations;
 
@@ -172,12 +172,7 @@ public abstract class AnnotationRowFilter extends JPanel
       @Override
       public void mouseReleased(MouseEvent evt)
       {
-        if (sliderDragging)
-        {
-          sliderDragging = false;
-          valueChanged(true);
-        }
-        ap.paintAlignment(true);
+        sliderDragReleased();
       }
     });
   }
@@ -192,9 +187,9 @@ public abstract class AnnotationRowFilter extends JPanel
    */
   public Vector<String> getAnnotationItems(boolean isSeqAssociated)
   {
-    annotationLabels = new HashMap<AlignmentAnnotation, String>();
+    annotationLabels = new HashMap<>();
 
-    Vector<String> list = new Vector<String>();
+    Vector<String> list = new Vector<>();
     int index = 1;
     int[] anmap = new int[av.getAlignment()
             .getAlignmentAnnotation().length];
@@ -271,7 +266,7 @@ public abstract class AnnotationRowFilter extends JPanel
   public void cancel_actionPerformed()
   {
     reset();
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
     try
     {
       frame.setClosed(true);
@@ -413,6 +408,11 @@ public abstract class AnnotationRowFilter extends JPanel
     this.currentAnnotation = annotation;
   }
 
+  /**
+   * update associated view model and trigger any necessary repaints.
+   * 
+   * @param updateAllAnnotation
+   */
   protected abstract void valueChanged(boolean updateAllAnnotation);
 
   protected abstract void updateView();
@@ -519,4 +519,13 @@ public abstract class AnnotationRowFilter extends JPanel
   {
     this.annotations = anns;
   }
+
+  protected void sliderDragReleased()
+  {
+    if (sliderDragging)
+    {
+      sliderDragging = false;
+      valueChanged(true);
+    }
+  }
 }
index a4597d3..6c934c8 100644 (file)
@@ -44,7 +44,6 @@ import java.util.List;
 import java.util.Vector;
 
 import javax.swing.JCheckBoxMenuItem;
-import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
 import javax.swing.JSplitPane;
 import javax.swing.SwingUtilities;
@@ -58,7 +57,7 @@ public class AppJmol extends StructureViewerBase
 
   private static final String SPACE = " ";
 
-  private static final String BACKSLASH = "\"";
+  private static final String QUOTE = "\"";
 
   AppJmolBinding jmb;
 
@@ -157,8 +156,14 @@ public class AppJmol extends StructureViewerBase
 
   IProgressIndicator progressBar = null;
 
+  @Override
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    return progressBar;
+  }
+  
   /**
-   * add a single PDB structure to a new or existing Jmol view
+   * display a single PDB structure in a new Jmol view
    * 
    * @param pdbentry
    * @param seq
@@ -169,33 +174,14 @@ public class AppJmol extends StructureViewerBase
           final AlignmentPanel ap)
   {
     progressBar = ap.alignFrame;
-    String pdbId = pdbentry.getId();
-
-    /*
-     * If the PDB file is already loaded, the user may just choose to add to an
-     * existing viewer (or cancel)
-     */
-    if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
-    {
-      return;
-    }
-
-    /*
-     * Check if there are other Jmol views involving this alignment and prompt
-     * user about adding this molecule to one of them
-     */
-    if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
-    {
-      return;
-    }
 
-    /*
-     * If the options above are declined or do not apply, open a new viewer
-     */
-    openNewJmol(ap, new PDBEntry[] { pdbentry }, new SequenceI[][] { seq });
+    openNewJmol(ap, alignAddedStructures, new PDBEntry[] { pdbentry },
+            new SequenceI[][]
+            { seq });
   }
 
-  private void openNewJmol(AlignmentPanel ap, PDBEntry[] pdbentrys,
+  private void openNewJmol(AlignmentPanel ap, boolean alignAdded,
+          PDBEntry[] pdbentrys,
           SequenceI[][] seqs)
   {
     progressBar = ap.alignFrame;
@@ -204,11 +190,9 @@ public class AppJmol extends StructureViewerBase
     addAlignmentPanel(ap);
     useAlignmentPanelForColourbyseq(ap);
 
-    if (pdbentrys.length > 1)
-    {
-      alignAddedStructures = true;
-      useAlignmentPanelForSuperposition(ap);
-    }
+    alignAddedStructures = alignAdded;
+    useAlignmentPanelForSuperposition(ap);
+
     jmb.setColourBySequence(true);
     setSize(400, 400); // probably should be a configurable/dynamic default here
     initMenus();
@@ -229,41 +213,21 @@ public class AppJmol extends StructureViewerBase
   }
 
   /**
-   * create a new Jmol containing several structures superimposed using the
-   * given alignPanel.
+   * create a new Jmol containing several structures optionally superimposed
+   * using the given alignPanel.
    * 
    * @param ap
+   * @param alignAdded
+   *          - true to superimpose
    * @param pe
    * @param seqs
    */
-  public AppJmol(AlignmentPanel ap, PDBEntry[] pe, SequenceI[][] seqs)
+  public AppJmol(AlignmentPanel ap, boolean alignAdded, PDBEntry[] pe,
+          SequenceI[][] seqs)
   {
-    openNewJmol(ap, pe, seqs);
+    openNewJmol(ap, alignAdded, pe, seqs);
   }
 
-  /**
-   * Returns a list of any Jmol viewers. The list is restricted to those linked
-   * to the given alignment panel if it is not null.
-   */
-  @Override
-  protected List<StructureViewerBase> getViewersFor(AlignmentPanel apanel)
-  {
-    List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
-    JInternalFrame[] frames = Desktop.instance.getAllFrames();
-
-    for (JInternalFrame frame : frames)
-    {
-      if (frame instanceof AppJmol)
-      {
-        if (apanel == null
-                || ((StructureViewerBase) frame).isLinkedWith(apanel))
-        {
-          result.add((StructureViewerBase) frame);
-        }
-      }
-    }
-    return result;
-  }
 
   void initJmol(String command)
   {
@@ -295,12 +259,10 @@ public class AppJmol extends StructureViewerBase
     jmb.setFinishedInit(true);
   }
 
-  boolean allChainsSelected = false;
-
   @Override
   void showSelectedChains()
   {
-    Vector<String> toshow = new Vector<String>();
+    Vector<String> toshow = new Vector<>();
     for (int i = 0; i < chainMenu.getItemCount(); i++)
     {
       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
@@ -363,8 +325,8 @@ public class AppJmol extends StructureViewerBase
     StringBuilder fileList = new StringBuilder();
     for (String s : files)
     {
-      fileList.append(SPACE).append(BACKSLASH)
-              .append(Platform.escapeString(s)).append(BACKSLASH);
+      fileList.append(SPACE).append(QUOTE)
+              .append(Platform.escapeString(s)).append(QUOTE);
     }
     String filesString = fileList.toString();
 
@@ -439,7 +401,7 @@ public class AppJmol extends StructureViewerBase
       jmb.updateColours(ap);
     }
     // do superposition if asked to
-    if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
+    if (alignAddedStructures)
     {
       alignAddedStructures();
     }
@@ -473,7 +435,7 @@ public class AppJmol extends StructureViewerBase
         }
       }
     });
-    alignAddedStructures = false;
+
   }
 
   /**
@@ -489,7 +451,7 @@ public class AppJmol extends StructureViewerBase
     // todo - record which pdbids were successfully imported.
     StringBuilder errormsgs = new StringBuilder();
 
-    List<String> files = new ArrayList<String>();
+    List<String> files = new ArrayList<>();
     String pdbid = "";
     try
     {
@@ -502,6 +464,7 @@ public class AppJmol extends StructureViewerBase
         String file = jmb.getPdbEntry(pi).getFile();
         if (file == null)
         {
+          // todo: extract block as method and pull up (also ChimeraViewFrame)
           // retrieve the pdb and store it locally
           AlignmentI pdbseq = null;
           pdbid = jmb.getPdbEntry(pi).getId();
index 9325172..724cec1 100644 (file)
@@ -49,6 +49,12 @@ public class AppJmolBinding extends JalviewJmolBinding
   }
 
   @Override
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    return appJmolWindow.progressBar;
+  }
+
+  @Override
   public SequenceRenderer getSequenceRenderer(AlignmentViewPanel alignment)
   {
     return new SequenceRenderer(((AlignmentPanel) alignment).av);
diff --git a/src/jalview/gui/AquaInternalFrameManager.java b/src/jalview/gui/AquaInternalFrameManager.java
new file mode 100644 (file)
index 0000000..537ec17
--- /dev/null
@@ -0,0 +1,257 @@
+/*
+ * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jalview.gui;
+
+import java.awt.Container;
+import java.beans.PropertyVetoException;
+import java.util.Vector;
+
+import javax.swing.DefaultDesktopManager;
+import javax.swing.DesktopManager;
+import javax.swing.JInternalFrame;
+
+/**
+ * Based on AquaInternalFrameManager
+ *
+ * DesktopManager implementation for Aqua
+ *
+ * Mac is more like Windows than it's like Motif/Basic
+ *
+ * From WindowsDesktopManager:
+ *
+ * This class implements a DesktopManager which more closely follows the MDI
+ * model than the DefaultDesktopManager. Unlike the DefaultDesktopManager
+ * policy, MDI requires that the selected and activated child frames are the
+ * same, and that that frame always be the top-most window.
+ * <p>
+ * The maximized state is managed by the DesktopManager with MDI, instead of
+ * just being a property of the individual child frame. This means that if the
+ * currently selected window is maximized and another window is selected, that
+ * new window will be maximized.
+ *
+ * Downloaded from
+ * https://raw.githubusercontent.com/frohoff/jdk8u-jdk/master/src/macosx/classes/com/apple/laf/AquaInternalFrameManager.java
+ * 
+ * Patch from Jim Procter - when the most recently opened frame is closed,
+ * correct behaviour is to go to the next most recent frame, rather than wrap
+ * around to the bottom of the window stack (as the original implementation
+ * does)
+ * 
+ * see com.sun.java.swing.plaf.windows.WindowsDesktopManager
+ */
+public class AquaInternalFrameManager extends DefaultDesktopManager
+{
+  // Variables
+
+  /* The frame which is currently selected/activated.
+   * We store this value to enforce Mac's single-selection model.
+   */
+  JInternalFrame fCurrentFrame;
+
+  JInternalFrame fInitialFrame;
+
+  /* The list of frames, sorted by order of creation.
+   * This list is necessary because by default the order of
+   * child frames in the JDesktopPane changes during frame
+   * activation (the activated frame is moved to index 0).
+   * We preserve the creation order so that "next" and "previous"
+   * frame actions make sense.
+   */
+  Vector<JInternalFrame> fChildFrames = new Vector<>(1);
+
+  /**
+   * keep a reference to the original LAF manager so we can iconise/de-iconise
+   * correctly
+   */
+  private DesktopManager ourManager;
+
+  public AquaInternalFrameManager(DesktopManager desktopManager)
+  {
+    ourManager = desktopManager;
+  }
+
+  @Override
+  public void closeFrame(final JInternalFrame f)
+  {
+    if (f == fCurrentFrame)
+    {
+      boolean mostRecentFrame = fChildFrames
+              .indexOf(f) == fChildFrames.size() - 1;
+      if (!mostRecentFrame)
+      {
+        activateNextFrame();
+      }
+      else
+      {
+        activatePreviousFrame();
+      }
+    }
+    fChildFrames.removeElement(f);
+    super.closeFrame(f);
+  }
+
+  @Override
+  public void deiconifyFrame(final JInternalFrame f)
+  {
+    JInternalFrame.JDesktopIcon desktopIcon;
+
+    desktopIcon = f.getDesktopIcon();
+    // If the icon moved, move the frame to that spot before expanding it
+    // reshape does delta checks for us
+    f.reshape(desktopIcon.getX(), desktopIcon.getY(), f.getWidth(),
+            f.getHeight());
+    ourManager.deiconifyFrame(f);
+  }
+
+  void addIcon(final Container c,
+          final JInternalFrame.JDesktopIcon desktopIcon)
+  {
+    c.add(desktopIcon);
+  }
+
+  /**
+   * Removes the frame from its parent and adds its desktopIcon to the parent.
+   */
+  @Override
+  public void iconifyFrame(final JInternalFrame f)
+  {
+    ourManager.iconifyFrame(f);
+  }
+
+  // WindowsDesktopManager code
+  @Override
+  public void activateFrame(final JInternalFrame f)
+  {
+    try
+    {
+      if (f != null)
+      {
+        super.activateFrame(f);
+      }
+
+      // If this is the first activation, add to child list.
+      if (fChildFrames.indexOf(f) == -1)
+      {
+        fChildFrames.addElement(f);
+      }
+
+      if (fCurrentFrame != null && f != fCurrentFrame)
+      {
+        if (fCurrentFrame.isSelected())
+        {
+          fCurrentFrame.setSelected(false);
+        }
+      }
+
+      if (f != null && !f.isSelected())
+      {
+        f.setSelected(true);
+      }
+
+      fCurrentFrame = f;
+    } catch (final PropertyVetoException e)
+    {
+    }
+  }
+
+  private void switchFrame(final boolean next)
+  {
+    if (fCurrentFrame == null)
+    {
+      // initialize first frame we find
+      if (fInitialFrame != null)
+      {
+        activateFrame(fInitialFrame);
+      }
+      return;
+    }
+
+    final int count = fChildFrames.size();
+    if (count <= 1)
+    {
+      // No other child frames.
+      return;
+    }
+
+    final int currentIndex = fChildFrames.indexOf(fCurrentFrame);
+    if (currentIndex == -1)
+    {
+      // the "current frame" is no longer in the list
+      fCurrentFrame = null;
+      return;
+    }
+
+    int nextIndex;
+    if (next)
+    {
+      nextIndex = currentIndex + 1;
+      if (nextIndex == count)
+      {
+        nextIndex = 0;
+      }
+    }
+    else
+    {
+      nextIndex = currentIndex - 1;
+      if (nextIndex == -1)
+      {
+        nextIndex = count - 1;
+      }
+    }
+    final JInternalFrame f = fChildFrames.elementAt(nextIndex);
+    activateFrame(f);
+    fCurrentFrame = f;
+  }
+
+  /**
+   * Activate the next child JInternalFrame, as determined by the frames'
+   * Z-order. If there is only one child frame, it remains activated. If there
+   * are no child frames, nothing happens.
+   */
+  public void activateNextFrame()
+  {
+    switchFrame(true);
+  }
+
+  /**
+   * same as above but will activate a frame if none have been selected
+   */
+  public void activateNextFrame(final JInternalFrame f)
+  {
+    fInitialFrame = f;
+    switchFrame(true);
+  }
+
+  /**
+   * Activate the previous child JInternalFrame, as determined by the frames'
+   * Z-order. If there is only one child frame, it remains activated. If there
+   * are no child frames, nothing happens.
+   */
+  public void activatePreviousFrame()
+  {
+    switchFrame(false);
+  }
+}
index a9f3966..f674c7e 100644 (file)
@@ -105,6 +105,11 @@ public class CalculationChooser extends JPanel
 
   List<String> tips = new ArrayList<String>();
 
+  /*
+   * the most recently opened PCA results panel
+   */
+  private PCAPanel pcaPanel;
+
   /**
    * Constructor
    * 
@@ -164,8 +169,8 @@ public class CalculationChooser extends JPanel
     JPanel treePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
     treePanel.setOpaque(false);
 
-    treePanel.setBorder(BorderFactory
-            .createTitledBorder(MessageManager.getString("label.tree")));
+    JvSwingUtils.createTitledBorder(treePanel,
+            MessageManager.getString("label.tree"), true);
 
     // then copy the inset dimensions for the border-less PCA panel
     JPanel pcaBorderless = new JPanel(new FlowLayout(FlowLayout.LEFT));
@@ -534,7 +539,7 @@ public class CalculationChooser extends JPanel
               JvOptionPane.WARNING_MESSAGE);
       return;
     }
-    new PCAPanel(af.alignPanel, modelName, params);
+    pcaPanel = new PCAPanel(af.alignPanel, modelName, params);
   }
 
   /**
@@ -592,4 +597,9 @@ public class CalculationChooser extends JPanel
     {
     }
   }
+
+  public PCAPanel getPcaPanel()
+  {
+    return pcaPanel;
+  }
 }
index ba360af..d07a7c2 100644 (file)
@@ -100,41 +100,34 @@ public class ChimeraViewFrame extends StructureViewerBase
     savemenu.setVisible(false); // not yet implemented
     viewMenu.add(fitToWindow);
 
-    /*
-     * exchange of Jalview features and Chimera attributes is for now
-     * an optionally enabled experimental feature
-     */
-    if (Desktop.instance.showExperimental())
+    JMenuItem writeFeatures = new JMenuItem(
+            MessageManager.getString("label.create_chimera_attributes"));
+    writeFeatures.setToolTipText(MessageManager
+            .getString("label.create_chimera_attributes_tip"));
+    writeFeatures.addActionListener(new ActionListener()
     {
-      JMenuItem writeFeatures = new JMenuItem(
-              MessageManager.getString("label.create_chimera_attributes"));
-      writeFeatures.setToolTipText(MessageManager
-              .getString("label.create_chimera_attributes_tip"));
-      writeFeatures.addActionListener(new ActionListener()
-      {
-        @Override
-        public void actionPerformed(ActionEvent e)
-        {
-          sendFeaturesToChimera();
-        }
-      });
-      viewerActionMenu.add(writeFeatures);
-
-      final JMenu fetchAttributes = new JMenu(
-              MessageManager.getString("label.fetch_chimera_attributes"));
-      fetchAttributes.setToolTipText(MessageManager
-              .getString("label.fetch_chimera_attributes_tip"));
-      fetchAttributes.addMouseListener(new MouseAdapter()
+      @Override
+      public void actionPerformed(ActionEvent e)
       {
+        sendFeaturesToChimera();
+      }
+    });
+    viewerActionMenu.add(writeFeatures);
 
-        @Override
-        public void mouseEntered(MouseEvent e)
-        {
-          buildAttributesMenu(fetchAttributes);
-        }
-      });
-      viewerActionMenu.add(fetchAttributes);
-    }
+    final JMenu fetchAttributes = new JMenu(
+            MessageManager.getString("label.fetch_chimera_attributes"));
+    fetchAttributes.setToolTipText(
+            MessageManager.getString("label.fetch_chimera_attributes_tip"));
+    fetchAttributes.addMouseListener(new MouseAdapter()
+    {
+
+      @Override
+      public void mouseEntered(MouseEvent e)
+      {
+        buildAttributesMenu(fetchAttributes);
+      }
+    });
+    viewerActionMenu.add(fetchAttributes);
   }
 
   /**
@@ -202,7 +195,7 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * add a single PDB structure to a new or existing Chimera view
+   * open a single PDB structure in a new Chimera view
    * 
    * @param pdbentry
    * @param seq
@@ -213,30 +206,7 @@ public class ChimeraViewFrame extends StructureViewerBase
           String[] chains, final AlignmentPanel ap)
   {
     this();
-    String pdbId = pdbentry.getId();
-
-    /*
-     * If the PDB file is already loaded, the user may just choose to add to an
-     * existing viewer (or cancel)
-     */
-    if (addAlreadyLoadedFile(seq, chains, ap, pdbId))
-    {
-      return;
-    }
-
-    /*
-     * Check if there are other Chimera views involving this alignment and give
-     * user the option to add and align this molecule to one of them (or cancel)
-     */
-    if (addToExistingViewer(pdbentry, seq, chains, ap, pdbId))
-    {
-      return;
-    }
 
-    /*
-     * If the options above are declined or do not apply, show the structure in
-     * a new viewer
-     */
     openNewChimera(ap, new PDBEntry[] { pdbentry },
             new SequenceI[][]
             { seq });
@@ -264,7 +234,6 @@ public class ChimeraViewFrame extends StructureViewerBase
 
     if (pdbentrys.length > 1)
     {
-      alignAddedStructures = true;
       useAlignmentPanelForSuperposition(ap);
     }
     jmb.setColourBySequence(true);
@@ -323,17 +292,19 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * create a new viewer containing several structures superimposed using the
-   * given alignPanel.
+   * create a new viewer containing several structures, optionally superimposed
+   * using the given alignPanel.
    * 
    * @param pe
    * @param seqs
    * @param ap
    */
-  public ChimeraViewFrame(PDBEntry[] pe, SequenceI[][] seqs,
+  public ChimeraViewFrame(PDBEntry[] pe, boolean alignAdded,
+          SequenceI[][] seqs,
           AlignmentPanel ap)
   {
     this();
+    setAlignAddedStructures(alignAdded);
     openNewChimera(ap, pe, seqs);
   }
 
@@ -352,29 +323,6 @@ public class ChimeraViewFrame extends StructureViewerBase
   }
 
   /**
-   * Returns a list of any Chimera viewers in the desktop. The list is
-   * restricted to those linked to the given alignment panel if it is not null.
-   */
-  @Override
-  protected List<StructureViewerBase> getViewersFor(AlignmentPanel ap)
-  {
-    List<StructureViewerBase> result = new ArrayList<StructureViewerBase>();
-    JInternalFrame[] frames = Desktop.instance.getAllFrames();
-
-    for (JInternalFrame frame : frames)
-    {
-      if (frame instanceof ChimeraViewFrame)
-      {
-        if (ap == null || ((StructureViewerBase) frame).isLinkedWith(ap))
-        {
-          result.add((StructureViewerBase) frame);
-        }
-      }
-    }
-    return result;
-  }
-
-  /**
    * Launch Chimera. If we have a chimera session file name, send Chimera the
    * command to open its saved session file.
    */
@@ -414,7 +362,7 @@ public class ChimeraViewFrame extends StructureViewerBase
   @Override
   void showSelectedChains()
   {
-    List<String> toshow = new ArrayList<String>();
+    List<String> toshow = new ArrayList<>();
     for (int i = 0; i < chainMenu.getItemCount(); i++)
     {
       if (chainMenu.getItem(i) instanceof JCheckBoxMenuItem)
@@ -484,8 +432,8 @@ public class ChimeraViewFrame extends StructureViewerBase
     // todo - record which pdbids were successfully imported.
     StringBuilder errormsgs = new StringBuilder(128);
     StringBuilder files = new StringBuilder(128);
-    List<PDBEntry> filePDB = new ArrayList<PDBEntry>();
-    List<Integer> filePDBpos = new ArrayList<Integer>();
+    List<PDBEntry> filePDB = new ArrayList<>();
+    List<Integer> filePDBpos = new ArrayList<>();
     PDBEntry thePdbEntry = null;
     StructureFile pdb = null;
     try
@@ -598,9 +546,12 @@ public class ChimeraViewFrame extends StructureViewerBase
               stopProgressBar("", startTime);
             }
             // Explicitly map to the filename used by Chimera ;
+
             pdb = jmb.getSsm().setMapping(jmb.getSequence()[pos],
-                    jmb.getChains()[pos], pe.getFile(), protocol);
+                    jmb.getChains()[pos], pe.getFile(), protocol,
+                    progressBar);
             stashFoundChains(pdb, pe.getFile());
+
           } catch (OutOfMemoryError oomerror)
           {
             new OOMWarning(
@@ -638,7 +589,7 @@ public class ChimeraViewFrame extends StructureViewerBase
         jmb.updateColours(ap);
       }
       // do superposition if asked to
-      if (Cache.getDefault("AUTOSUPERIMPOSE", true) && alignAddedStructures)
+      if (alignAddedStructures)
       {
         new Thread(new Runnable()
         {
@@ -648,7 +599,6 @@ public class ChimeraViewFrame extends StructureViewerBase
             alignStructs_withAllAlignPanels();
           }
         }).start();
-        alignAddedStructures = false;
       }
       addingStructures = false;
     }
@@ -658,7 +608,7 @@ public class ChimeraViewFrame extends StructureViewerBase
 
   /**
    * Fetch PDB data and save to a local file. Returns the full path to the file,
-   * or null if fetch fails.
+   * or null if fetch fails. TODO: refactor to common with Jmol ? duplication
    * 
    * @param processingEntry
    * @return
@@ -678,7 +628,6 @@ public class ChimeraViewFrame extends StructureViewerBase
 
   private String fetchPdbFile(PDBEntry processingEntry) throws Exception
   {
-    // FIXME: this is duplicated code with Jmol frame ?
     String filePath = null;
     Pdb pdbclient = new Pdb();
     AlignmentI pdbseq = null;
@@ -891,4 +840,10 @@ public class ChimeraViewFrame extends StructureViewerBase
     }
     return reply;
   }
+
+  @Override
+  protected IProgressIndicator getIProgressIndicator()
+  {
+    return progressBar;
+  }
 }
index 01ee1ff..85f2498 100644 (file)
@@ -27,17 +27,25 @@ import jalview.api.FeatureSettingsModelI;
 import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.SequenceI;
+import jalview.ext.ensembl.EnsemblInfo;
+import jalview.ext.ensembl.EnsemblMap;
 import jalview.io.gff.SequenceOntologyI;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.DBRefUtils;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
 import jalview.util.MessageManager;
 import jalview.ws.SequenceFetcher;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
-
-import javax.swing.JOptionPane;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Factory constructor and runnable for discovering and displaying
@@ -52,13 +60,13 @@ public class CrossRefAction implements Runnable
 
   private SequenceI[] sel;
 
-  private boolean _odna;
+  private final boolean _odna;
 
   private String source;
 
-  List<AlignmentViewPanel> xrefViews = new ArrayList<AlignmentViewPanel>();
+  List<AlignmentViewPanel> xrefViews = new ArrayList<>();
 
-  public List<jalview.api.AlignmentViewPanel> getXrefViews()
+  List<AlignmentViewPanel> getXrefViews()
   {
     return xrefViews;
   }
@@ -90,6 +98,13 @@ public class CrossRefAction implements Runnable
       {
         return;
       }
+
+      /*
+       * try to look up chromosomal coordinates for nucleotide
+       * sequences (if not already retrieved)
+       */
+      findGeneLoci(xrefs.getSequences());
+
       /*
        * get display scheme (if any) to apply to features
        */
@@ -113,75 +128,14 @@ public class CrossRefAction implements Runnable
 
       if (Cache.getDefault(Preferences.ENABLE_SPLIT_FRAME, true))
       {
-        boolean copyAlignmentIsAligned = false;
-        if (dna)
-        {
-          copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
-                  xrefsAlignment.getSequencesArray());
-          if (copyAlignment.getHeight() == 0)
-          {
-            JvOptionPane.showMessageDialog(alignFrame,
-                    MessageManager.getString("label.cant_map_cds"),
-                    MessageManager.getString("label.operation_failed"),
-                    JvOptionPane.OK_OPTION);
-            System.err.println("Failed to make CDS alignment");
-          }
-
-          /*
-           * pending getting Embl transcripts to 'align', 
-           * we are only doing this for Ensembl
-           */
-          // TODO proper criteria for 'can align as cdna'
-          if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
-                  || AlignmentUtils.looksLikeEnsembl(alignment))
-          {
-            copyAlignment.alignAs(alignment);
-            copyAlignmentIsAligned = true;
-          }
-        }
-        else
+        copyAlignment = copyAlignmentForSplitFrame(alignment, dataset, dna,
+                xrefs, xrefsAlignment);
+        if (copyAlignment == null)
         {
-          copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
-                  xrefs.getSequencesArray(), dataset);
-        }
-        copyAlignment
-                .setGapCharacter(alignFrame.viewport.getGapCharacter());
-
-        StructureSelectionManager ssm = StructureSelectionManager
-                .getStructureSelectionManager(Desktop.instance);
-
-        /*
-         * register any new mappings for sequence mouseover etc
-         * (will not duplicate any previously registered mappings)
-         */
-        ssm.registerMappings(dataset.getCodonFrames());
-
-        if (copyAlignment.getHeight() <= 0)
-        {
-          System.err.println(
-                  "No Sequences generated for xRef type " + source);
-          return;
-        }
-        /*
-         * align protein to dna
-         */
-        if (dna && copyAlignmentIsAligned)
-        {
-          xrefsAlignment.alignAs(copyAlignment);
-        }
-        else
-        {
-          /*
-           * align cdna to protein - currently only if 
-           * fetching and aligning Ensembl transcripts!
-           */
-          // TODO: generalise for other sources of locus/transcript/cds data
-          if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
-          {
-            copyAlignment.alignAs(xrefsAlignment);
-          }
+          return; // failed
         }
       }
+
       /*
        * build AlignFrame(s) according to available alignment data
        */
@@ -207,6 +161,7 @@ public class CrossRefAction implements Runnable
         xrefViews.add(newFrame.alignPanel);
         return; // via finally clause
       }
+
       AlignFrame copyThis = new AlignFrame(copyAlignment,
               AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
       copyThis.setTitle(alignFrame.getTitle());
@@ -221,10 +176,14 @@ public class CrossRefAction implements Runnable
       /*
        * copy feature rendering settings to split frame
        */
-      newFrame.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(myFeatureStyling);
-      copyThis.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(myFeatureStyling);
+      FeatureRenderer fr1 = newFrame.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
+      fr1.transferSettings(myFeatureStyling);
+      fr1.findAllFeatures(true);
+      FeatureRenderer fr2 = copyThis.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
+      fr2.transferSettings(myFeatureStyling);
+      fr2.findAllFeatures(true);
 
       /*
        * apply 'database source' feature configuration
@@ -242,7 +201,7 @@ public class CrossRefAction implements Runnable
       String linkedTitle = MessageManager
               .getString("label.linked_view_title");
       Desktop.addInternalFrame(sf, linkedTitle, -1, -1);
-      sf.adjustDivider();
+      sf.adjustInitialLayout();
 
       // finally add the top, then bottom frame to the view list
       xrefViews.add(dna ? copyThis.alignPanel : newFrame.alignPanel);
@@ -263,6 +222,260 @@ public class CrossRefAction implements Runnable
   }
 
   /**
+   * Tries to add chromosomal coordinates to any nucleotide sequence which does
+   * not already have them. Coordinates are retrieved from Ensembl given an
+   * Ensembl identifier, either on the sequence itself or on a peptide sequence
+   * it has a reference to.
+   * 
+   * <pre>
+   * Example (human):
+   * - fetch EMBLCDS cross-references for Uniprot entry P30419
+   * - the EMBL sequences do not have xrefs to Ensembl
+   * - the Uniprot entry has xrefs to 
+   *    ENSP00000258960, ENSP00000468424, ENST00000258960, ENST00000592782
+   * - either of the transcript ids can be used to retrieve gene loci e.g.
+   *    http://rest.ensembl.org/map/cds/ENST00000592782/1..100000
+   * Example (invertebrate):
+   * - fetch EMBLCDS cross-references for Uniprot entry Q43517 (FER1_SOLLC)
+   * - the Uniprot entry has an xref to ENSEMBLPLANTS Solyc10g044520.1.1
+   * - can retrieve gene loci with
+   *    http://rest.ensemblgenomes.org/map/cds/Solyc10g044520.1.1/1..100000
+   * </pre>
+   * 
+   * @param sequences
+   */
+  public static void findGeneLoci(List<SequenceI> sequences)
+  {
+    Map<DBRefEntry, GeneLociI> retrievedLoci = new HashMap<>();
+    for (SequenceI seq : sequences)
+    {
+      findGeneLoci(seq, retrievedLoci);
+    }
+  }
+
+  /**
+   * Tres to find chromosomal coordinates for the sequence, by searching its
+   * direct and indirect cross-references for Ensembl. If the loci have already
+   * been retrieved, just reads them out of the map of retrievedLoci; this is
+   * the case of an alternative transcript for the same protein. Otherwise calls
+   * a REST service to retrieve the loci, and if successful, adds them to the
+   * sequence and to the retrievedLoci.
+   * 
+   * @param seq
+   * @param retrievedLoci
+   */
+  static void findGeneLoci(SequenceI seq,
+          Map<DBRefEntry, GeneLociI> retrievedLoci)
+  {
+    /*
+     * don't replace any existing chromosomal coordinates
+     */
+    if (seq == null || seq.isProtein() || seq.getGeneLoci() != null
+            || seq.getDBRefs() == null)
+    {
+      return;
+    }
+    
+    Set<String> ensemblDivisions = new EnsemblInfo().getDivisions();
+    
+    /*
+     * first look for direct dbrefs from sequence to Ensembl
+     */
+    String[] divisionsArray = ensemblDivisions
+            .toArray(new String[ensemblDivisions.size()]);
+    DBRefEntry[] seqRefs = seq.getDBRefs();
+    DBRefEntry[] directEnsemblRefs = DBRefUtils.selectRefs(seqRefs,
+            divisionsArray);
+    if (directEnsemblRefs != null)
+    {
+      for (DBRefEntry ensemblRef : directEnsemblRefs)
+      {
+        if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+        {
+          return;
+        }
+      }
+    }
+
+    /*
+     * else look for indirect dbrefs from sequence to Ensembl
+     */
+    for (DBRefEntry dbref : seq.getDBRefs())
+    {
+      if (dbref.getMap() != null && dbref.getMap().getTo() != null)
+      {
+        DBRefEntry[] dbrefs = dbref.getMap().getTo().getDBRefs();
+        DBRefEntry[] indirectEnsemblRefs = DBRefUtils.selectRefs(dbrefs,
+                divisionsArray);
+        if (indirectEnsemblRefs != null)
+        {
+          for (DBRefEntry ensemblRef : indirectEnsemblRefs)
+          {
+            if (fetchGeneLoci(seq, ensemblRef, retrievedLoci))
+            {
+              return;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Retrieves chromosomal coordinates for the Ensembl (or EnsemblGenomes)
+   * identifier in dbref. If successful, and the sequence length matches gene
+   * loci length, then add it to the sequence, and to the retrievedLoci map.
+   * Answers true if successful, else false.
+   * 
+   * @param seq
+   * @param dbref
+   * @param retrievedLoci
+   * @return
+   */
+  static boolean fetchGeneLoci(SequenceI seq, DBRefEntry dbref,
+          Map<DBRefEntry, GeneLociI> retrievedLoci)
+  {
+    String accession = dbref.getAccessionId();
+    String division = dbref.getSource();
+
+    /*
+     * hack: ignore cross-references to Ensembl protein ids
+     * (or use map/translation perhaps?)
+     * todo: is there an equivalent in EnsemblGenomes?
+     */
+    if (accession.startsWith("ENSP"))
+    {
+      return false;
+    }
+    EnsemblMap mapper = new EnsemblMap();
+
+    /*
+     * try CDS mapping first
+     */
+    GeneLociI geneLoci = mapper.getCdsMapping(division, accession, 1,
+            seq.getLength());
+    if (geneLoci != null)
+    {
+      MapList map = geneLoci.getMap();
+      int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+      if (mappedFromLength == seq.getLength())
+      {
+        seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+                geneLoci.getChromosomeId(), geneLoci.getMap());
+        retrievedLoci.put(dbref, geneLoci);
+        return true;
+      }
+    }
+
+    /*
+     * else try CDNA mapping
+     */
+    geneLoci = mapper.getCdnaMapping(division, accession, 1,
+            seq.getLength());
+    if (geneLoci != null)
+    {
+      MapList map = geneLoci.getMap();
+      int mappedFromLength = MappingUtils.getLength(map.getFromRanges());
+      if (mappedFromLength == seq.getLength())
+      {
+        seq.setGeneLoci(geneLoci.getSpeciesId(), geneLoci.getAssemblyId(),
+                geneLoci.getChromosomeId(), geneLoci.getMap());
+        retrievedLoci.put(dbref, geneLoci);
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * @param alignment
+   * @param dataset
+   * @param dna
+   * @param xrefs
+   * @param xrefsAlignment
+   * @return
+   */
+  protected AlignmentI copyAlignmentForSplitFrame(AlignmentI alignment,
+          AlignmentI dataset, boolean dna, AlignmentI xrefs,
+          AlignmentI xrefsAlignment)
+  {
+    AlignmentI copyAlignment;
+    boolean copyAlignmentIsAligned = false;
+    if (dna)
+    {
+      copyAlignment = AlignmentUtils.makeCdsAlignment(sel, dataset,
+              xrefsAlignment.getSequencesArray());
+      if (copyAlignment.getHeight() == 0)
+      {
+        JvOptionPane.showMessageDialog(alignFrame,
+                MessageManager.getString("label.cant_map_cds"),
+                MessageManager.getString("label.operation_failed"),
+                JvOptionPane.OK_OPTION);
+        System.err.println("Failed to make CDS alignment");
+        return null;
+      }
+
+      /*
+       * pending getting Embl transcripts to 'align', 
+       * we are only doing this for Ensembl
+       */
+      // TODO proper criteria for 'can align as cdna'
+      if (DBRefSource.ENSEMBL.equalsIgnoreCase(source)
+              || AlignmentUtils.looksLikeEnsembl(alignment))
+      {
+        copyAlignment.alignAs(alignment);
+        copyAlignmentIsAligned = true;
+      }
+    }
+    else
+    {
+      copyAlignment = AlignmentUtils.makeCopyAlignment(sel,
+              xrefs.getSequencesArray(), dataset);
+    }
+    copyAlignment
+            .setGapCharacter(alignFrame.viewport.getGapCharacter());
+
+    StructureSelectionManager ssm = StructureSelectionManager
+            .getStructureSelectionManager(Desktop.instance);
+
+    /*
+     * register any new mappings for sequence mouseover etc
+     * (will not duplicate any previously registered mappings)
+     */
+    ssm.registerMappings(dataset.getCodonFrames());
+
+    if (copyAlignment.getHeight() <= 0)
+    {
+      System.err.println(
+              "No Sequences generated for xRef type " + source);
+      return null;
+    }
+
+    /*
+     * align protein to dna
+     */
+    if (dna && copyAlignmentIsAligned)
+    {
+      xrefsAlignment.alignAs(copyAlignment);
+    }
+    else
+    {
+      /*
+       * align cdna to protein - currently only if 
+       * fetching and aligning Ensembl transcripts!
+       */
+      // TODO: generalise for other sources of locus/transcript/cds data
+      if (dna && DBRefSource.ENSEMBL.equalsIgnoreCase(source))
+      {
+        copyAlignment.alignAs(xrefsAlignment);
+      }
+    }
+
+    return copyAlignment;
+  }
+
+  /**
    * Makes an alignment containing the given sequences, and adds them to the
    * given dataset, which is also set as the dataset for the new alignment
    * 
@@ -291,20 +504,28 @@ public class CrossRefAction implements Runnable
     return al;
   }
 
-  public CrossRefAction(AlignFrame alignFrame, SequenceI[] sel,
-          boolean _odna, String source)
+  /**
+   * Constructor
+   * 
+   * @param af
+   * @param seqs
+   * @param fromDna
+   * @param dbSource
+   */
+  CrossRefAction(AlignFrame af, SequenceI[] seqs, boolean fromDna,
+          String dbSource)
   {
-    this.alignFrame = alignFrame;
-    this.sel = sel;
-    this._odna = _odna;
-    this.source = source;
+    this.alignFrame = af;
+    this.sel = seqs;
+    this._odna = fromDna;
+    this.source = dbSource;
   }
 
-  public static CrossRefAction showProductsFor(final SequenceI[] sel,
-          final boolean _odna, final String source,
+  public static CrossRefAction getHandlerFor(final SequenceI[] sel,
+          final boolean fromDna, final String source,
           final AlignFrame alignFrame)
   {
-    return new CrossRefAction(alignFrame, sel, _odna, source);
+    return new CrossRefAction(alignFrame, sel, fromDna, source);
   }
 
 }
index 71a1520..2e51bce 100644 (file)
@@ -141,6 +141,7 @@ public class CutAndPasteHtmlTransfer extends GCutAndPasteHtmlTransfer
    */
   public void setText(String text)
   {
+    textarea.setDocument(textarea.getEditorKit().createDefaultDocument());
     textarea.setText(text);
   }
 
index c8d900d..569257f 100644 (file)
@@ -32,6 +32,7 @@ import jalview.io.FileFormatException;
 import jalview.io.FileFormatI;
 import jalview.io.FileFormats;
 import jalview.io.FileLoader;
+import jalview.io.FormatAdapter;
 import jalview.io.IdentifyFile;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
@@ -68,8 +69,7 @@ import java.awt.dnd.DropTargetEvent;
 import java.awt.dnd.DropTargetListener;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.awt.event.FocusEvent;
-import java.awt.event.FocusListener;
+import java.awt.event.InputEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
@@ -93,10 +93,13 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Semaphore;
 
 import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ActionMap;
 import javax.swing.Box;
 import javax.swing.BoxLayout;
 import javax.swing.DefaultDesktopManager;
 import javax.swing.DesktopManager;
+import javax.swing.InputMap;
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
@@ -118,6 +121,8 @@ import javax.swing.event.InternalFrameEvent;
 import javax.swing.event.MenuEvent;
 import javax.swing.event.MenuListener;
 
+import org.stackoverflowusers.file.WindowsShortcut;
+
 /**
  * Jalview Desktop
  * 
@@ -269,12 +274,14 @@ public class Desktop extends jalview.jbgui.GDesktop
     public void endDraggingFrame(JComponent f)
     {
       delegate.endDraggingFrame(f);
+      desktop.repaint();
     }
 
     @Override
     public void endResizingFrame(JComponent f)
     {
       delegate.endResizingFrame(f);
+      desktop.repaint();
     }
 
     @Override
@@ -344,10 +351,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     boolean showjconsole = jalview.bin.Cache.getDefault("SHOW_JAVA_CONSOLE",
             false);
     desktop = new MyDesktopPane(selmemusage);
-    if (Platform.isAMac())
-    {
-      desktop.setDoubleBuffered(false);
-    }
     showMemusage.setSelected(selmemusage);
     desktop.setBackground(Color.white);
     getContentPane().setLayout(new BorderLayout());
@@ -361,7 +364,12 @@ public class Desktop extends jalview.jbgui.GDesktop
     // This line prevents Windows Look&Feel resizing all new windows to maximum
     // if previous window was maximised
     desktop.setDesktopManager(
-            new MyDesktopManager(new DefaultDesktopManager()));
+            new MyDesktopManager(
+                    (Platform.isWindows() ? new DefaultDesktopManager()
+                            : Platform.isAMac()
+                                    ? new AquaInternalFrameManager(
+                                            desktop.getDesktopManager())
+                                    : desktop.getDesktopManager())));
 
     Rectangle dims = getLastKnownDimensions("");
     if (dims != null)
@@ -431,24 +439,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     });
     desktop.addMouseListener(ma);
 
-    this.addFocusListener(new FocusListener()
-    {
-
-      @Override
-      public void focusLost(FocusEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void focusGained(FocusEvent e)
-      {
-        Cache.log.debug("Relaying windows after focus gain");
-        // make sure that we sort windows properly after we gain focus
-        instance.relayerWindows();
-      }
-    });
     this.setDropTarget(new java.awt.dnd.DropTarget(desktop, this));
     // Spawn a thread that shows the splashscreen
     SwingUtilities.invokeLater(new Runnable()
@@ -866,13 +856,8 @@ public class Desktop extends jalview.jbgui.GDesktop
     frame.setResizable(resizable);
     frame.setMaximizable(resizable);
     frame.setIconifiable(resizable);
-    if (Platform.isAMac())
-    {
-      frame.setIconifiable(false);
-      frame.setFrameIcon(null);
-      // frame.setDesktopIcon(null);
-      frame.setDoubleBuffered(false);
-    }
+    frame.setOpaque(false);
+
     if (frame.getX() < 1 && frame.getY() < 1)
     {
       frame.setLocation(xOffset * openFrameCount,
@@ -892,6 +877,10 @@ public class Desktop extends jalview.jbgui.GDesktop
         JInternalFrame itf = desktop.getSelectedFrame();
         if (itf != null)
         {
+          if (itf instanceof AlignFrame)
+          {
+            Jalview.setCurrentAlignFrame((AlignFrame) itf);
+          }
           itf.requestFocus();
         }
       }
@@ -918,16 +907,6 @@ public class Desktop extends jalview.jbgui.GDesktop
           menuItem.removeActionListener(menuItem.getActionListeners()[0]);
         }
         windowMenu.remove(menuItem);
-        JInternalFrame itf = desktop.getSelectedFrame();
-        if (itf != null)
-        {
-          itf.requestFocus();
-          if (itf instanceof AlignFrame)
-          {
-            Jalview.setCurrentAlignFrame((AlignFrame) itf);
-          }
-        }
-        System.gc();
       };
     });
 
@@ -947,6 +926,8 @@ public class Desktop extends jalview.jbgui.GDesktop
       }
     });
 
+    setKeyBindings(frame);
+
     desktop.add(frame);
 
     windowMenu.add(menuItem);
@@ -966,6 +947,42 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
   }
 
+  /**
+   * Add key bindings to a JInternalFrame so that Ctrl-W and Cmd-W will close
+   * the window
+   * 
+   * @param frame
+   */
+  private static void setKeyBindings(JInternalFrame frame)
+  {
+    @SuppressWarnings("serial")
+    final Action closeAction = new AbstractAction()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        frame.dispose();
+      }
+    };
+
+    /*
+     * 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,
+            Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
+
+    InputMap inputMap = frame
+            .getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
+    String ctrlW = ctrlWKey.toString();
+    inputMap.put(ctrlWKey, ctrlW);
+    inputMap.put(cmdWKey, ctrlW);
+
+    ActionMap actionMap = frame.getActionMap();
+    actionMap.put(ctrlW, closeAction);
+  }
+
   @Override
   public void lostOwnership(Clipboard clipboard, Transferable contents)
   {
@@ -1011,8 +1028,8 @@ public class Desktop extends jalview.jbgui.GDesktop
     // Java's Transferable for native dnd
     evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
     Transferable t = evt.getTransferable();
-    List<String> files = new ArrayList<String>();
-    List<DataSourceType> protocols = new ArrayList<DataSourceType>();
+    List<String> files = new ArrayList<>();
+    List<DataSourceType> protocols = new ArrayList<>();
 
     try
     {
@@ -1415,7 +1432,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       ssm.resetAll();
     }
-    System.gc();
   }
 
   @Override
@@ -1720,7 +1736,7 @@ public class Desktop extends jalview.jbgui.GDesktop
 
   JPanel progressPanel;
 
-  ArrayList<JPanel> fileLoadingPanels = new ArrayList<JPanel>();
+  ArrayList<JPanel> fileLoadingPanels = new ArrayList<>();
 
   public void startLoading(final String fileName)
   {
@@ -1812,7 +1828,7 @@ public class Desktop extends jalview.jbgui.GDesktop
       // TODO: verify that frames are recoverable when in headless mode
       return null;
     }
-    List<AlignmentPanel> aps = new ArrayList<AlignmentPanel>();
+    List<AlignmentPanel> aps = new ArrayList<>();
     AlignFrame[] frames = getAlignFrames();
     if (frames == null)
     {
@@ -1847,7 +1863,7 @@ public class Desktop extends jalview.jbgui.GDesktop
    */
   public static AlignmentViewport[] getViewports(String sequenceSetId)
   {
-    List<AlignmentViewport> viewp = new ArrayList<AlignmentViewport>();
+    List<AlignmentViewport> viewp = new ArrayList<>();
     if (desktop != null)
     {
       AlignFrame[] frames = Desktop.getAlignFrames();
@@ -2360,12 +2376,12 @@ public class Desktop extends jalview.jbgui.GDesktop
           // SEQUENCE_ID which is not the default EMBL_EBI link
           ListIterator<String> li = links.listIterator();
           boolean check = false;
-          List<JLabel> urls = new ArrayList<JLabel>();
+          List<JLabel> urls = new ArrayList<>();
           while (li.hasNext())
           {
             String link = li.next();
             if (link.contains(SEQUENCE_ID)
-                    && !link.equals(UrlConstants.DEFAULT_STRING))
+                    && !UrlConstants.isDefaultString(link))
             {
               check = true;
               int barPos = link.indexOf("|");
@@ -2454,6 +2470,7 @@ public class Desktop extends jalview.jbgui.GDesktop
         Thread worker = new Thread(this);
         worker.start();
       }
+      repaint();
     }
 
     public boolean isShowMemoryUsage()
@@ -2517,14 +2534,6 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
   }
 
-  /**
-   * fixes stacking order after a modal dialog to ensure windows that should be
-   * on top actually are
-   */
-  public void relayerWindows()
-  {
-
-  }
 
   /**
    * Accessor method to quickly get all the AlignmentFrames loaded.
@@ -2545,7 +2554,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       return null;
     }
-    List<AlignFrame> avp = new ArrayList<AlignFrame>();
+    List<AlignFrame> avp = new ArrayList<>();
     // REVERSE ORDER
     for (int i = frames.length - 1; i > -1; i--)
     {
@@ -2590,7 +2599,7 @@ public class Desktop extends jalview.jbgui.GDesktop
     {
       return null;
     }
-    List<GStructureViewer> avp = new ArrayList<GStructureViewer>();
+    List<GStructureViewer> avp = new ArrayList<>();
     // REVERSE ORDER
     for (int i = frames.length - 1; i > -1; i--)
     {
@@ -2733,8 +2742,8 @@ public class Desktop extends jalview.jbgui.GDesktop
   {
     if (progressBars == null)
     {
-      progressBars = new Hashtable<Long, JPanel>();
-      progressBarHandlers = new Hashtable<Long, IProgressIndicatorHandler>();
+      progressBars = new Hashtable<>();
+      progressBarHandlers = new Hashtable<>();
     }
 
     if (progressBars.get(new Long(id)) != null)
@@ -3327,13 +3336,67 @@ public class Desktop extends jalview.jbgui.GDesktop
     return groovyConsole;
   }
 
+  /**
+   * handles the payload of a drag and drop event.
+   * 
+   * TODO refactor to desktop utilities class
+   * 
+   * @param files
+   *          - Data source strings extracted from the drop event
+   * @param protocols
+   *          - protocol for each data source extracted from the drop event
+   * @param evt
+   *          - the drop event
+   * @param t
+   *          - the payload from the drop event
+   * @throws Exception
+   */
   public static void transferFromDropTarget(List<String> files,
           List<DataSourceType> protocols, DropTargetDropEvent evt,
           Transferable t) throws Exception
   {
 
     DataFlavor uriListFlavor = new DataFlavor(
-            "text/uri-list;class=java.lang.String");
+            "text/uri-list;class=java.lang.String"), urlFlavour = null;
+    try
+    {
+      urlFlavour = new DataFlavor(
+              "application/x-java-url; class=java.net.URL");
+    } catch (ClassNotFoundException cfe)
+    {
+      Cache.log.debug("Couldn't instantiate the URL dataflavor.", cfe);
+    }
+
+    if (urlFlavour != null && t.isDataFlavorSupported(urlFlavour))
+    {
+
+      try
+      {
+      java.net.URL url = (URL) t.getTransferData(urlFlavour);
+        // nb: java 8 osx bug https://bugs.openjdk.java.net/browse/JDK-8156099
+        // means url may be null.
+      if (url != null)
+      {
+        protocols.add(DataSourceType.URL);
+        files.add(url.toString());
+        Cache.log.debug("Drop handled as URL dataflavor "
+                + files.get(files.size() - 1));
+          return;
+        }
+        else
+        {
+          if (Platform.isAMac())
+          {
+            System.err.println(
+                    "Please ignore plist error - occurs due to problem with java 8 on OSX");
+          }
+          ;
+      }
+      } catch (Throwable ex)
+      {
+        Cache.log.debug("URL drop handler failed.", ex);
+      }
+    }
     if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor))
     {
       // Works on Windows and MacOSX
@@ -3361,63 +3424,112 @@ public class Desktop extends jalview.jbgui.GDesktop
         // fallback to text: workaround - on OSX where there's a JVM bug
         Cache.log.debug("standard URIListFlavor failed. Trying text");
         // try text fallback
-        data = (String) t.getTransferData(
-                new DataFlavor("text/plain;class=java.lang.String"));
-        if (Cache.log.isDebugEnabled())
+        DataFlavor textDf = new DataFlavor(
+                "text/plain;class=java.lang.String");
+        if (t.isDataFlavorSupported(textDf))
         {
-          Cache.log.debug("fallback returned " + data);
+          data = (String) t.getTransferData(textDf);
         }
+
+        Cache.log.debug("Plain text drop content returned "
+                + (data == null ? "Null - failed" : data));
+
       }
-      while (protocols.size() < files.size())
-      {
-        Cache.log.debug("Adding missing FILE protocol for "
-                + files.get(protocols.size()));
-        protocols.add(DataSourceType.FILE);
-      }
-      for (java.util.StringTokenizer st = new java.util.StringTokenizer(
-              data, "\r\n"); st.hasMoreTokens();)
+      if (data != null)
       {
-        added = true;
-        String s = st.nextToken();
-        if (s.startsWith("#"))
+        while (protocols.size() < files.size())
         {
-          // the line is a comment (as per the RFC 2483)
-          continue;
-        }
-        java.net.URI uri = new java.net.URI(s);
-        if (uri.getScheme().toLowerCase().startsWith("http"))
-        {
-          protocols.add(DataSourceType.URL);
-          files.add(uri.toString());
+          Cache.log.debug("Adding missing FILE protocol for "
+                  + files.get(protocols.size()));
+          protocols.add(DataSourceType.FILE);
         }
-        else
+        for (java.util.StringTokenizer st = new java.util.StringTokenizer(
+                data, "\r\n"); st.hasMoreTokens();)
         {
-          // otherwise preserve old behaviour: catch all for file objects
-          java.io.File file = new java.io.File(uri);
-          protocols.add(DataSourceType.FILE);
-          files.add(file.toString());
+          added = true;
+          String s = st.nextToken();
+          if (s.startsWith("#"))
+          {
+            // the line is a comment (as per the RFC 2483)
+            continue;
+          }
+          java.net.URI uri = new java.net.URI(s);
+          if (uri.getScheme().toLowerCase().startsWith("http"))
+          {
+            protocols.add(DataSourceType.URL);
+            files.add(uri.toString());
+          }
+          else
+          {
+            // otherwise preserve old behaviour: catch all for file objects
+            java.io.File file = new java.io.File(uri);
+            protocols.add(DataSourceType.FILE);
+            files.add(file.toString());
+          }
         }
       }
+
       if (Cache.log.isDebugEnabled())
       {
         if (data == null || !added)
         {
-          Cache.log.debug(
-                  "Couldn't resolve drop data. Here are the supported flavors:");
-          for (DataFlavor fl : t.getTransferDataFlavors())
+
+          if (t.getTransferDataFlavors() != null
+                  && t.getTransferDataFlavors().length > 0)
           {
             Cache.log.debug(
-                    "Supported transfer dataflavor: " + fl.toString());
-            Object df = t.getTransferData(fl);
-            if (df != null)
+                    "Couldn't resolve drop data. Here are the supported flavors:");
+            for (DataFlavor fl : t.getTransferDataFlavors())
             {
-              Cache.log.debug("Retrieves: " + df);
-            }
-            else
-            {
-              Cache.log.debug("Retrieved nothing");
+              Cache.log.debug(
+                      "Supported transfer dataflavor: " + fl.toString());
+              Object df = t.getTransferData(fl);
+              if (df != null)
+              {
+                Cache.log.debug("Retrieves: " + df);
+              }
+              else
+              {
+                Cache.log.debug("Retrieved nothing");
+              }
             }
           }
+          else
+          {
+            Cache.log.debug("Couldn't resolve dataflavor for drop: "
+                    + t.toString());
+          }
+        }
+      }
+    }
+    if (Platform.isWindows())
+
+    {
+      Cache.log.debug("Scanning dropped content for Windows Link Files");
+
+      // resolve any .lnk files in the file drop
+      for (int f = 0; f < files.size(); f++)
+      {
+        String source = files.get(f).toLowerCase();
+        if (protocols.get(f).equals(DataSourceType.FILE)
+                && (source.endsWith(".lnk") || source.endsWith(".url")
+                        || source.endsWith(".site")))
+        {
+          try {
+            File lf = new File(files.get(f));
+            // process link file to get a URL
+            Cache.log.debug("Found potential link file: " + lf);
+            WindowsShortcut wscfile = new WindowsShortcut(lf);
+            String fullname = wscfile.getRealFilename();
+            protocols.set(f, FormatAdapter.checkProtocol(fullname));
+            files.set(f, fullname);
+            Cache.log.debug("Parsed real filename " + fullname
+                    + " to extract protocol: " + protocols.get(f));
+          }
+          catch (Exception ex)
+          {
+            Cache.log.error("Couldn't parse "+files.get(f)+" as a link file.",ex);
+          }
         }
       }
     }
@@ -3432,4 +3544,41 @@ public class Desktop extends jalview.jbgui.GDesktop
   {
     Cache.setProperty(EXPERIMENTAL_FEATURES, Boolean.toString(selected));
   }
+
+  /**
+   * Answers a (possibly empty) list of any structure viewer frames (currently
+   * for either Jmol or Chimera) which are currently open. This may optionally
+   * be restricted to viewers of a specified class, or viewers linked to a
+   * specified alignment panel.
+   * 
+   * @param apanel
+   *          if not null, only return viewers linked to this panel
+   * @param structureViewerClass
+   *          if not null, only return viewers of this class
+   * @return
+   */
+  public List<StructureViewerBase> getStructureViewers(
+          AlignmentPanel apanel,
+          Class<? extends StructureViewerBase> structureViewerClass)
+  {
+    List<StructureViewerBase> result = new ArrayList<>();
+    JInternalFrame[] frames = Desktop.instance.getAllFrames();
+
+    for (JInternalFrame frame : frames)
+    {
+      if (frame instanceof StructureViewerBase)
+      {
+        if (structureViewerClass == null
+                || structureViewerClass.isInstance(frame))
+        {
+          if (apanel == null
+                  || ((StructureViewerBase) frame).isLinkedWith(apanel))
+          {
+            result.add((StructureViewerBase) frame);
+          }
+        }
+      }
+    }
+    return result;
+  }
 }
diff --git a/src/jalview/gui/FeatureColourChooser.java b/src/jalview/gui/FeatureColourChooser.java
deleted file mode 100644 (file)
index b27328d..0000000
+++ /dev/null
@@ -1,632 +0,0 @@
-/*
- * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
- * Copyright (C) $$Year-Rel$$ The Jalview Authors
- * 
- * This file is part of Jalview.
- * 
- * Jalview is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License 
- * as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
- *  
- * Jalview is distributed in the hope that it will be useful, but 
- * WITHOUT ANY WARRANTY; without even the implied warranty 
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
- * PURPOSE.  See the GNU General Public License for more details.
- * 
- * You should have received a copy of the GNU General Public License
- * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
- * The Jalview Authors are detailed in the 'AUTHORS' file.
- */
-package jalview.gui;
-
-import jalview.api.FeatureColourI;
-import jalview.datamodel.GraphLine;
-import jalview.schemes.FeatureColour;
-import jalview.util.MessageManager;
-
-import java.awt.BorderLayout;
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.FlowLayout;
-import java.awt.event.ActionEvent;
-import java.awt.event.ActionListener;
-import java.awt.event.FocusAdapter;
-import java.awt.event.FocusEvent;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-
-import javax.swing.BorderFactory;
-import javax.swing.JCheckBox;
-import javax.swing.JColorChooser;
-import javax.swing.JComboBox;
-import javax.swing.JLabel;
-import javax.swing.JPanel;
-import javax.swing.JSlider;
-import javax.swing.JTextField;
-import javax.swing.border.LineBorder;
-import javax.swing.event.ChangeEvent;
-import javax.swing.event.ChangeListener;
-
-public class FeatureColourChooser extends JalviewDialog
-{
-  // FeatureSettings fs;
-  private FeatureRenderer fr;
-
-  private FeatureColourI cs;
-
-  private FeatureColourI oldcs;
-
-  private AlignmentPanel ap;
-
-  private boolean adjusting = false;
-
-  final private float min;
-
-  final private float max;
-
-  final private float scaleFactor;
-
-  private String type = null;
-
-  private JPanel minColour = new JPanel();
-
-  private JPanel maxColour = new JPanel();
-
-  private JComboBox<String> threshold = new JComboBox<>();
-
-  private JSlider slider = new JSlider();
-
-  private JTextField thresholdValue = new JTextField(20);
-
-  // TODO implement GUI for tolower flag
-  // JCheckBox toLower = new JCheckBox();
-
-  private JCheckBox thresholdIsMin = new JCheckBox();
-
-  private JCheckBox colourByLabel = new JCheckBox();
-
-  private GraphLine threshline;
-
-  private Color oldmaxColour;
-
-  private Color oldminColour;
-
-  private ActionListener colourEditor = null;
-
-  /**
-   * Constructor
-   * 
-   * @param frender
-   * @param theType
-   */
-  public FeatureColourChooser(FeatureRenderer frender, String theType)
-  {
-    this(frender, false, theType);
-  }
-
-  /**
-   * Constructor, with option to make a blocking dialog (has to complete in the
-   * AWT event queue thread). Currently this option is always set to false.
-   * 
-   * @param frender
-   * @param blocking
-   * @param theType
-   */
-  FeatureColourChooser(FeatureRenderer frender, boolean blocking,
-          String theType)
-  {
-    this.fr = frender;
-    this.type = theType;
-    ap = fr.ap;
-    String title = MessageManager
-            .formatMessage("label.graduated_color_for_params", new String[]
-            { theType });
-    initDialogFrame(this, true, blocking, title, 480, 185);
-
-    slider.addChangeListener(new ChangeListener()
-    {
-      @Override
-      public void stateChanged(ChangeEvent evt)
-      {
-        if (!adjusting)
-        {
-          thresholdValue.setText((slider.getValue() / scaleFactor) + "");
-          sliderValueChanged();
-        }
-      }
-    });
-    slider.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mouseReleased(MouseEvent evt)
-      {
-        /*
-         * only update Overview and/or structure colouring
-         * when threshold slider drag ends (mouse up)
-         */
-        if (ap != null)
-        {
-          ap.paintAlignment(true);
-        }
-      }
-    });
-
-    float mm[] = fr.getMinMax().get(theType)[0];
-    min = mm[0];
-    max = mm[1];
-
-    /*
-     * ensure scale factor allows a scaled range with
-     * 10 integer divisions ('ticks'); if we have got here,
-     * we should expect that max != min
-     */
-    scaleFactor = (max == min) ? 1f : 100f / (max - min);
-
-    oldcs = fr.getFeatureColours().get(theType);
-    if (!oldcs.isSimpleColour())
-    {
-      if (oldcs.isAutoScaled())
-      {
-        // update the scale
-        cs = new FeatureColour((FeatureColour) oldcs, min, max);
-      }
-      else
-      {
-        cs = new FeatureColour((FeatureColour) oldcs);
-      }
-    }
-    else
-    {
-      // promote original color to a graduated color
-      Color bl = oldcs.getColour();
-      if (bl == null)
-      {
-        bl = Color.BLACK;
-      }
-      // original colour becomes the maximum colour
-      cs = new FeatureColour(Color.white, bl, mm[0], mm[1]);
-      cs.setColourByLabel(false);
-    }
-    minColour.setBackground(oldminColour = cs.getMinColour());
-    maxColour.setBackground(oldmaxColour = cs.getMaxColour());
-    adjusting = true;
-
-    try
-    {
-      jbInit();
-    } catch (Exception ex)
-    {
-    }
-    // update the gui from threshold state
-    thresholdIsMin.setSelected(!cs.isAutoScaled());
-    colourByLabel.setSelected(cs.isColourByLabel());
-    if (cs.hasThreshold())
-    {
-      // initialise threshold slider and selector
-      threshold.setSelectedIndex(cs.isAboveThreshold() ? 1 : 2);
-      slider.setEnabled(true);
-      slider.setValue((int) (cs.getThreshold() * scaleFactor));
-      thresholdValue.setEnabled(true);
-      threshline = new GraphLine((max - min) / 2f, "Threshold",
-              Color.black);
-      threshline.value = cs.getThreshold();
-    }
-
-    adjusting = false;
-
-    changeColour(false);
-    waitForInput();
-  }
-
-  private void jbInit() throws Exception
-  {
-
-    minColour.setFont(JvSwingUtils.getLabelFont());
-    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    minColour.setPreferredSize(new Dimension(40, 20));
-    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
-    minColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (minColour.isEnabled())
-        {
-          minColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setFont(JvSwingUtils.getLabelFont());
-    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
-    maxColour.setPreferredSize(new Dimension(40, 20));
-    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
-    maxColour.addMouseListener(new MouseAdapter()
-    {
-      @Override
-      public void mousePressed(MouseEvent e)
-      {
-        if (maxColour.isEnabled())
-        {
-          maxColour_actionPerformed();
-        }
-      }
-    });
-    maxColour.setBorder(new LineBorder(Color.black));
-    JLabel minText = new JLabel(MessageManager.getString("label.min"));
-    minText.setFont(JvSwingUtils.getLabelFont());
-    JLabel maxText = new JLabel(MessageManager.getString("label.max"));
-    maxText.setFont(JvSwingUtils.getLabelFont());
-    this.setLayout(new BorderLayout());
-    JPanel jPanel1 = new JPanel();
-    jPanel1.setBackground(Color.white);
-    JPanel jPanel2 = new JPanel();
-    jPanel2.setLayout(new FlowLayout());
-    jPanel2.setBackground(Color.white);
-    threshold.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        threshold_actionPerformed();
-      }
-    });
-    threshold.setToolTipText(MessageManager
-            .getString("label.threshold_feature_display_by_score"));
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_no_threshold")); // index 0
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_above_threshold")); // index 1
-    threshold.addItem(MessageManager
-            .getString("label.threshold_feature_below_threshold")); // index 2
-
-    JPanel jPanel3 = new JPanel();
-    jPanel3.setLayout(new FlowLayout());
-    thresholdValue.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent e)
-      {
-        thresholdValue_actionPerformed();
-      }
-    });
-    thresholdValue.addFocusListener(new FocusAdapter()
-    {
-      @Override
-      public void focusLost(FocusEvent e)
-      {
-        thresholdValue_actionPerformed();
-      }
-    });
-    slider.setPaintLabels(false);
-    slider.setPaintTicks(true);
-    slider.setBackground(Color.white);
-    slider.setEnabled(false);
-    slider.setOpaque(false);
-    slider.setPreferredSize(new Dimension(100, 32));
-    slider.setToolTipText(
-            MessageManager.getString("label.adjust_threshold"));
-    thresholdValue.setEnabled(false);
-    thresholdValue.setColumns(7);
-    jPanel3.setBackground(Color.white);
-    thresholdIsMin.setBackground(Color.white);
-    thresholdIsMin
-            .setText(MessageManager.getString("label.threshold_minmax"));
-    thresholdIsMin.setToolTipText(MessageManager
-            .getString("label.toggle_absolute_relative_display_threshold"));
-    thresholdIsMin.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        thresholdIsMin_actionPerformed();
-      }
-    });
-    colourByLabel.setBackground(Color.white);
-    colourByLabel
-            .setText(MessageManager.getString("label.colour_by_label"));
-    colourByLabel.setToolTipText(MessageManager.getString(
-            "label.display_features_same_type_different_label_using_different_colour"));
-    colourByLabel.addActionListener(new ActionListener()
-    {
-      @Override
-      public void actionPerformed(ActionEvent actionEvent)
-      {
-        colourByLabel_actionPerformed();
-      }
-    });
-
-    JPanel colourPanel = new JPanel();
-    colourPanel.setBackground(Color.white);
-    jPanel1.add(ok);
-    jPanel1.add(cancel);
-    jPanel2.add(colourByLabel, BorderLayout.WEST);
-    jPanel2.add(colourPanel, BorderLayout.EAST);
-    colourPanel.add(minText);
-    colourPanel.add(minColour);
-    colourPanel.add(maxText);
-    colourPanel.add(maxColour);
-    this.add(jPanel3, BorderLayout.CENTER);
-    jPanel3.add(threshold);
-    jPanel3.add(slider);
-    jPanel3.add(thresholdValue);
-    jPanel3.add(thresholdIsMin);
-    this.add(jPanel1, BorderLayout.SOUTH);
-    this.add(jPanel2, BorderLayout.NORTH);
-  }
-
-  /**
-   * Action on clicking the 'minimum colour' - open a colour chooser dialog, and
-   * set the selected colour (if the user does not cancel out of the dialog)
-   */
-  protected void minColour_actionPerformed()
-  {
-    Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_colour_minimum_value"),
-            minColour.getBackground());
-    if (col != null)
-    {
-      minColour.setBackground(col);
-      minColour.setForeground(col);
-    }
-    minColour.repaint();
-    changeColour(true);
-  }
-
-  /**
-   * Action on clicking the 'maximum colour' - open a colour chooser dialog, and
-   * set the selected colour (if the user does not cancel out of the dialog)
-   */
-  protected void maxColour_actionPerformed()
-  {
-    Color col = JColorChooser.showDialog(this,
-            MessageManager.getString("label.select_colour_maximum_value"),
-            maxColour.getBackground());
-    if (col != null)
-    {
-      maxColour.setBackground(col);
-      maxColour.setForeground(col);
-    }
-    maxColour.repaint();
-    changeColour(true);
-  }
-
-  /**
-   * Constructs and sets the selected colour options as the colour for the
-   * feature type, and repaints the alignment, and optionally the Overview
-   * and/or structure viewer if open
-   * 
-   * @param updateOverview
-   */
-  void changeColour(boolean updateOverview)
-  {
-    // Check if combobox is still adjusting
-    if (adjusting)
-    {
-      return;
-    }
-
-    boolean aboveThreshold = false;
-    boolean belowThreshold = false;
-    if (threshold.getSelectedIndex() == 1)
-    {
-      aboveThreshold = true;
-    }
-    else if (threshold.getSelectedIndex() == 2)
-    {
-      belowThreshold = true;
-    }
-    boolean hasThreshold = aboveThreshold || belowThreshold;
-
-    slider.setEnabled(true);
-    thresholdValue.setEnabled(true);
-
-    FeatureColourI acg;
-    if (cs.isColourByLabel())
-    {
-      acg = new FeatureColour(oldminColour, oldmaxColour, min, max);
-    }
-    else
-    {
-      acg = new FeatureColour(oldminColour = minColour.getBackground(),
-              oldmaxColour = maxColour.getBackground(), min, max);
-    }
-
-    if (!hasThreshold)
-    {
-      slider.setEnabled(false);
-      thresholdValue.setEnabled(false);
-      thresholdValue.setText("");
-      thresholdIsMin.setEnabled(false);
-    }
-    else if (threshline == null)
-    {
-      /*
-       * todo not yet implemented: visual indication of feature threshold
-       */
-      threshline = new GraphLine((max - min) / 2f, "Threshold",
-              Color.black);
-    }
-
-    if (hasThreshold)
-    {
-      adjusting = true;
-      acg.setThreshold(threshline.value);
-
-      float range = (max - min) * scaleFactor;
-
-      slider.setMinimum((int) (min * scaleFactor));
-      slider.setMaximum((int) (max * scaleFactor));
-      // slider.setValue((int) (threshline.value * scaleFactor));
-      slider.setValue(Math.round(threshline.value * scaleFactor));
-      thresholdValue.setText(threshline.value + "");
-      slider.setMajorTickSpacing((int) (range / 10f));
-      slider.setEnabled(true);
-      thresholdValue.setEnabled(true);
-      thresholdIsMin.setEnabled(!colourByLabel.isSelected());
-      adjusting = false;
-    }
-
-    acg.setAboveThreshold(aboveThreshold);
-    acg.setBelowThreshold(belowThreshold);
-    if (thresholdIsMin.isSelected() && hasThreshold)
-    {
-      acg.setAutoScaled(false);
-      if (aboveThreshold)
-      {
-        acg = new FeatureColour((FeatureColour) acg, threshline.value, max);
-      }
-      else
-      {
-        acg = new FeatureColour((FeatureColour) acg, min, threshline.value);
-      }
-    }
-    else
-    {
-      acg.setAutoScaled(true);
-    }
-    acg.setColourByLabel(colourByLabel.isSelected());
-    if (acg.isColourByLabel())
-    {
-      maxColour.setEnabled(false);
-      minColour.setEnabled(false);
-      maxColour.setBackground(this.getBackground());
-      maxColour.setForeground(this.getBackground());
-      minColour.setBackground(this.getBackground());
-      minColour.setForeground(this.getBackground());
-
-    }
-    else
-    {
-      maxColour.setEnabled(true);
-      minColour.setEnabled(true);
-      maxColour.setBackground(oldmaxColour);
-      minColour.setBackground(oldminColour);
-      maxColour.setForeground(oldmaxColour);
-      minColour.setForeground(oldminColour);
-    }
-    fr.setColour(type, acg);
-    cs = acg;
-    ap.paintAlignment(updateOverview);
-  }
-
-  @Override
-  protected void raiseClosed()
-  {
-    if (this.colourEditor != null)
-    {
-      colourEditor.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
-    }
-  }
-
-  @Override
-  public void okPressed()
-  {
-    changeColour(false);
-  }
-
-  @Override
-  public void cancelPressed()
-  {
-    reset();
-  }
-
-  /**
-   * Action when the user cancels the dialog. All previous settings should be
-   * restored and rendered on the alignment, and any linked Overview window or
-   * structure.
-   */
-  void reset()
-  {
-    fr.setColour(type, oldcs);
-    ap.paintAlignment(true);
-    cs = null;
-  }
-
-  /**
-   * Action on change of choice of No / Above / Below Threshold
-   */
-  protected void threshold_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  /**
-   * Action on text entry of a threshold value
-   */
-  protected void thresholdValue_actionPerformed()
-  {
-    try
-    {
-      float f = Float.parseFloat(thresholdValue.getText());
-      slider.setValue((int) (f * scaleFactor));
-      threshline.value = f;
-
-      /*
-       * force repaint of any Overview window or structure
-       */
-      ap.paintAlignment(true);
-    } catch (NumberFormatException ex)
-    {
-    }
-  }
-
-  /**
-   * Action on change of threshold slider value. This may be done interactively
-   * (by moving the slider), or programmatically (to update the slider after
-   * manual input of a threshold value).
-   */
-  protected void sliderValueChanged()
-  {
-    /*
-     * squash rounding errors by forcing min/max of slider to 
-     * actual min/max of feature score range
-     */
-    int value = slider.getValue();
-    threshline.value = value == slider.getMaximum() ? max
-            : (value == slider.getMinimum() ? min : value / scaleFactor);
-    cs.setThreshold(threshline.value);
-
-    /*
-     * repaint alignment, but not Overview or structure,
-     * to avoid overload while dragging the slider
-     */
-    changeColour(false);
-  }
-
-  protected void thresholdIsMin_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  protected void colourByLabel_actionPerformed()
-  {
-    changeColour(true);
-  }
-
-  void addActionListener(ActionListener graduatedColorEditor)
-  {
-    if (colourEditor != null)
-    {
-      System.err.println(
-              "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
-    }
-    colourEditor = graduatedColorEditor;
-  }
-
-  /**
-   * Answers the last colour setting selected by user - either oldcs (which may
-   * be a java.awt.Color) or the new GraduatedColor
-   * 
-   * @return
-   */
-  FeatureColourI getLastColour()
-  {
-    if (cs == null)
-    {
-      return oldcs;
-    }
-    return cs;
-  }
-
-}
index 17f5a71..46f574e 100644 (file)
@@ -180,15 +180,15 @@ public class FeatureRenderer
     final JSpinner end = new JSpinner();
     start.setPreferredSize(new Dimension(80, 20));
     end.setPreferredSize(new Dimension(80, 20));
-    final FeatureRenderer me = this;
     final JLabel colour = new JLabel();
     colour.setOpaque(true);
     // colour.setBorder(BorderFactory.createEtchedBorder());
     colour.setMaximumSize(new Dimension(30, 16));
     colour.addMouseListener(new MouseAdapter()
     {
-      FeatureColourChooser fcc = null;
-
+      /*
+       * open colour chooser on click in colour panel
+       */
       @Override
       public void mousePressed(MouseEvent evt)
       {
@@ -205,28 +205,26 @@ public class FeatureRenderer
         }
         else
         {
-          if (fcc == null)
+          /*
+           * variable colour dialog - on OK, refetch the updated
+           * feature colour and update this display
+           */
+          final String ft = features.get(featureIndex).getType();
+          final String type = ft == null ? lastFeatureAdded : ft;
+          FeatureTypeSettings fcc = new FeatureTypeSettings(
+                  FeatureRenderer.this, type);
+          fcc.setRequestFocusEnabled(true);
+          fcc.requestFocus();
+          fcc.addActionListener(new ActionListener()
           {
-            final String ft = features.get(featureIndex).getType();
-            final String type = ft == null ? lastFeatureAdded : ft;
-            fcc = new FeatureColourChooser(me, type);
-            fcc.setRequestFocusEnabled(true);
-            fcc.requestFocus();
-
-            fcc.addActionListener(new ActionListener()
+            @Override
+            public void actionPerformed(ActionEvent e)
             {
-
-              @Override
-              public void actionPerformed(ActionEvent e)
-              {
-                fcol = fcc.getLastColour();
-                fcc = null;
-                setColour(type, fcol);
-                updateColourButton(mainPanel, colour, fcol);
-              }
-            });
-
-          }
+              fcol = FeatureRenderer.this.getFeatureStyle(ft);
+              setColour(type, fcol);
+              updateColourButton(mainPanel, colour, fcol);
+            }
+          });
         }
       }
     });
@@ -243,7 +241,7 @@ public class FeatureRenderer
       JPanel choosePanel = new JPanel();
       choosePanel.add(new JLabel(
               MessageManager.getString("label.select_feature") + ":"));
-      final JComboBox<String> overlaps = new JComboBox<String>();
+      final JComboBox<String> overlaps = new JComboBox<>();
       List<String> added = new ArrayList<>();
       for (SequenceFeature sf : features)
       {
@@ -487,7 +485,7 @@ public class FeatureRenderer
 
         featuresAdded();
 
-        alignPanel.paintAlignment(true);
+        alignPanel.paintAlignment(true, true);
 
         return true;
       }
@@ -497,7 +495,7 @@ public class FeatureRenderer
       }
     }
 
-    alignPanel.paintAlignment(true);
+    alignPanel.paintAlignment(true, true);
 
     return true;
   }
index bd74db5..821454f 100644 (file)
@@ -22,19 +22,23 @@ package jalview.gui;
 
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureSettingsControllerI;
-import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.Help.HelpId;
 import jalview.io.JalviewFileChooser;
 import jalview.io.JalviewFileView;
+import jalview.schemabinding.version2.Filter;
 import jalview.schemabinding.version2.JalviewUserColours;
+import jalview.schemabinding.version2.MatcherSet;
 import jalview.schemes.FeatureColour;
-import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
-import jalview.util.QuickSort;
 import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
+import jalview.ws.DasSequenceFeatureFetcher;
 import jalview.ws.dbsources.das.api.jalviewSourceI;
 
 import java.awt.BorderLayout;
@@ -44,6 +48,7 @@ import java.awt.Dimension;
 import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.GridLayout;
+import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -61,6 +66,8 @@ import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.Iterator;
@@ -86,7 +93,6 @@ import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.JScrollPane;
 import javax.swing.JSlider;
-import javax.swing.JTabbedPane;
 import javax.swing.JTable;
 import javax.swing.ListSelectionModel;
 import javax.swing.SwingConstants;
@@ -96,15 +102,34 @@ import javax.swing.event.ChangeListener;
 import javax.swing.table.AbstractTableModel;
 import javax.swing.table.TableCellEditor;
 import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
 
 public class FeatureSettings extends JPanel
         implements FeatureSettingsControllerI
 {
-  DasSourceBrowser dassourceBrowser;
+  private static final String SEQUENCE_FEATURE_COLOURS = MessageManager
+          .getString("label.sequence_feature_colours");
+
+  /*
+   * column indices of fields in Feature Settings table
+   */
+  static final int TYPE_COLUMN = 0;
+
+  static final int COLOUR_COLUMN = 1;
+
+  static final int FILTER_COLUMN = 2;
+
+  static final int SHOW_COLUMN = 3;
+
+  private static final int COLUMN_COUNT = 4;
 
-  jalview.ws.DasSequenceFeatureFetcher dasFeatureFetcher;
+  private static final int MIN_WIDTH = 400;
+
+  private static final int MIN_HEIGHT = 400;
 
-  JPanel settingsPane = new JPanel();
+  DasSourceBrowser dassourceBrowser;
+
+  DasSequenceFeatureFetcher dasFeatureFetcher;
 
   JPanel dasSettingsPane = new JPanel();
 
@@ -112,10 +137,15 @@ public class FeatureSettings extends JPanel
 
   public final AlignFrame af;
 
+  /*
+   * 'original' fields hold settings to restore on Cancel
+   */
   Object[][] originalData;
 
   private float originalTransparency;
 
+  private Map<String, FeatureMatcherSetI> originalFilters;
+
   final JInternalFrame frame;
 
   JScrollPane scrollPane = new JScrollPane();
@@ -126,24 +156,47 @@ public class FeatureSettings extends JPanel
 
   JSlider transparency = new JSlider();
 
-  JPanel transPanel = new JPanel(new GridLayout(1, 2));
+  /*
+   * when true, constructor is still executing - so ignore UI events
+   */
+  protected volatile boolean inConstruction = true;
 
-  private static final int MIN_WIDTH = 400;
+  int selectedRow = -1;
 
-  private static final int MIN_HEIGHT = 400;
+  JButton fetchDAS = new JButton();
+
+  JButton saveDAS = new JButton();
+
+  JButton cancelDAS = new JButton();
+
+  boolean resettingTable = false;
+
+  /*
+   * true when Feature Settings are updating from feature renderer
+   */
+  private boolean handlingUpdate = false;
+
+  /*
+   * holds {featureCount, totalExtent} for each feature type
+   */
+  Map<String, float[]> typeWidth = null;
 
   /**
    * Constructor
    * 
    * @param af
    */
-  public FeatureSettings(AlignFrame af)
+  public FeatureSettings(AlignFrame alignFrame)
   {
-    this.af = af;
+    this.af = alignFrame;
     fr = af.getFeatureRenderer();
-    // allow transparency to be recovered
-    transparency.setMaximum(100
-            - (int) ((originalTransparency = fr.getTransparency()) * 100));
+
+    // save transparency for restore on Cancel
+    originalTransparency = fr.getTransparency();
+    int originalTransparencyAsPercent = (int) (originalTransparency * 100);
+    transparency.setMaximum(100 - originalTransparencyAsPercent);
+
+    originalFilters = new HashMap<>(fr.getFeatureFilters()); // shallow copy
 
     try
     {
@@ -158,25 +211,48 @@ public class FeatureSettings extends JPanel
       @Override
       public String getToolTipText(MouseEvent e)
       {
-        if (table.columnAtPoint(e.getPoint()) == 0)
+        String tip = null;
+        int column = table.columnAtPoint(e.getPoint());
+        switch (column)
         {
-          /*
-           * Tooltip for feature name only
-           */
-          return JvSwingUtils.wrapTooltip(true, MessageManager
+        case TYPE_COLUMN:
+          tip = JvSwingUtils.wrapTooltip(true, MessageManager
                   .getString("label.feature_settings_click_drag"));
+          break;
+        case FILTER_COLUMN:
+          int row = table.rowAtPoint(e.getPoint());
+          FeatureMatcherSet o = (FeatureMatcherSet) table.getValueAt(row,
+                  column);
+          tip = o.isEmpty()
+                  ? MessageManager.getString("label.filters_tooltip")
+                  : o.toString();
+          break;
+        default:
+          break;
         }
-        return null;
+        return tip;
       }
     };
     table.getTableHeader().setFont(new Font("Verdana", Font.PLAIN, 12));
     table.setFont(new Font("Verdana", Font.PLAIN, 12));
-    table.setDefaultRenderer(Color.class, new ColorRenderer());
-
-    table.setDefaultEditor(Color.class, new ColorEditor(this));
 
+    // table.setDefaultRenderer(Color.class, new ColorRenderer());
+    // table.setDefaultEditor(Color.class, new ColorEditor(this));
+    //
     table.setDefaultEditor(FeatureColour.class, new ColorEditor(this));
     table.setDefaultRenderer(FeatureColour.class, new ColorRenderer());
+
+    table.setDefaultEditor(FeatureMatcherSet.class, new FilterEditor(this));
+    table.setDefaultRenderer(FeatureMatcherSet.class, new FilterRenderer());
+
+    TableColumn colourColumn = new TableColumn(COLOUR_COLUMN, 75,
+            new ColorRenderer(), new ColorEditor(this));
+    table.addColumn(colourColumn);
+
+    TableColumn filterColumn = new TableColumn(FILTER_COLUMN, 75,
+            new FilterRenderer(), new FilterEditor(this));
+    table.addColumn(filterColumn);
+
     table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
 
     table.addMouseListener(new MouseAdapter()
@@ -185,11 +261,12 @@ public class FeatureSettings extends JPanel
       public void mousePressed(MouseEvent evt)
       {
         selectedRow = table.rowAtPoint(evt.getPoint());
+        String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
         if (evt.isPopupTrigger())
         {
-          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
-                  evt.getX(), evt.getY());
+          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
+          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
+                  evt.getY());
         }
         else if (evt.getClickCount() == 2)
         {
@@ -197,8 +274,7 @@ public class FeatureSettings extends JPanel
           boolean toggleSelection = Platform.isControlDown(evt);
           boolean extendSelection = evt.isShiftDown();
           fr.ap.alignFrame.avc.markColumnsContainingFeatures(
-                  invertSelection, extendSelection, toggleSelection,
-                  (String) table.getValueAt(selectedRow, 0));
+                  invertSelection, extendSelection, toggleSelection, type);
         }
       }
 
@@ -209,9 +285,10 @@ public class FeatureSettings extends JPanel
         selectedRow = table.rowAtPoint(evt.getPoint());
         if (evt.isPopupTrigger())
         {
-          popupSort(selectedRow, (String) table.getValueAt(selectedRow, 0),
-                  table.getValueAt(selectedRow, 1), fr.getMinMax(),
-                  evt.getX(), evt.getY());
+          String type = (String) table.getValueAt(selectedRow, TYPE_COLUMN);
+          Object colour = table.getValueAt(selectedRow, COLOUR_COLUMN);
+          popupSort(selectedRow, type, colour, fr.getMinMax(), evt.getX(),
+                  evt.getY());
         }
       }
     });
@@ -267,8 +344,8 @@ public class FeatureSettings extends JPanel
         if (!fs.resettingTable && !fs.handlingUpdate)
         {
           fs.handlingUpdate = true;
-          fs.resetTable(null); // new groups may be added with new seuqence
-          // feature types only
+          fs.resetTable(null);
+          // new groups may be added with new sequence feature types only
           fs.handlingUpdate = false;
         }
       }
@@ -281,13 +358,13 @@ public class FeatureSettings extends JPanel
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              475, 480);
+              600, 480);
     }
     else
     {
       Desktop.addInternalFrame(frame,
               MessageManager.getString("label.sequence_feature_settings"),
-              400, 450);
+              600, 450);
     }
     frame.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
 
@@ -303,9 +380,10 @@ public class FeatureSettings extends JPanel
               };
             });
     frame.setLayer(JLayeredPane.PALETTE_LAYER);
+    inConstruction = false;
   }
 
-  protected void popupSort(final int selectedRow, final String type,
+  protected void popupSort(final int rowSelected, final String type,
           final Object typeCol, final Map<String, float[][]> minmax, int x,
           int y)
   {
@@ -345,84 +423,70 @@ public class FeatureSettings extends JPanel
 
     });
     men.add(dens);
-    if (minmax != null)
+
+    /*
+     * variable colour options include colour by label, by score,
+     * by selected attribute text, or attribute value
+     */
+    final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
+            MessageManager.getString("label.variable_colour"));
+    mxcol.setSelected(!featureColour.isSimpleColour());
+    men.add(mxcol);
+    mxcol.addActionListener(new ActionListener()
     {
-      final float[][] typeMinMax = minmax.get(type);
-      /*
-       * final JCheckBoxMenuItem chb = new JCheckBoxMenuItem("Vary Height"); //
-       * this is broken at the moment and isn't that useful anyway!
-       * chb.setSelected(minmax.get(type) != null); chb.addActionListener(new
-       * ActionListener() {
-       * 
-       * public void actionPerformed(ActionEvent e) {
-       * chb.setState(chb.getState()); if (chb.getState()) { minmax.put(type,
-       * null); } else { minmax.put(type, typeMinMax); } }
-       * 
-       * });
-       * 
-       * men.add(chb);
-       */
-      if (typeMinMax != null && typeMinMax[0] != null)
-      {
-        // if (table.getValueAt(row, column));
-        // graduated colourschemes for those where minmax exists for the
-        // positional features
-        final JCheckBoxMenuItem mxcol = new JCheckBoxMenuItem(
-                "Graduated Colour");
-        mxcol.setSelected(!featureColour.isSimpleColour());
-        men.add(mxcol);
-        mxcol.addActionListener(new ActionListener()
-        {
-          JColorChooser colorChooser;
+      JColorChooser colorChooser;
 
-          @Override
-          public void actionPerformed(ActionEvent e)
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (e.getSource() == mxcol)
+        {
+          if (featureColour.isSimpleColour())
           {
-            if (e.getSource() == mxcol)
-            {
-              if (featureColour.isSimpleColour())
-              {
-                FeatureColourChooser fc = new FeatureColourChooser(me.fr,
-                        type);
-                fc.addActionListener(this);
-              }
-              else
-              {
-                // bring up simple color chooser
-                colorChooser = new JColorChooser();
-                JDialog dialog = JColorChooser.createDialog(me,
-                        "Select new Colour", true, // modal
-                        colorChooser, this, // OK button handler
-                        null); // no CANCEL button handler
-                colorChooser.setColor(featureColour.getMaxColour());
-                dialog.setVisible(true);
-              }
-            }
-            else
-            {
-              if (e.getSource() instanceof FeatureColourChooser)
-              {
-                FeatureColourChooser fc = (FeatureColourChooser) e
-                        .getSource();
-                table.setValueAt(fc.getLastColour(), selectedRow, 1);
-                table.validate();
-              }
-              else
-              {
-                // probably the color chooser!
-                table.setValueAt(new FeatureColour(colorChooser.getColor()),
-                        selectedRow, 1);
-                table.validate();
-                me.updateFeatureRenderer(
-                        ((FeatureTableModel) table.getModel()).getData(),
-                        false);
-              }
-            }
+            FeatureTypeSettings fc = new FeatureTypeSettings(me.fr, type);
+            fc.addActionListener(this);
           }
-
-        });
+          else
+          {
+            // bring up simple color chooser
+            colorChooser = new JColorChooser();
+            String title = MessageManager
+                    .getString("label.select_colour");
+            JDialog dialog = JColorChooser.createDialog(me,
+                    title, true, // modal
+                    colorChooser, this, // OK button handler
+                    null); // no CANCEL button handler
+            colorChooser.setColor(featureColour.getMaxColour());
+            dialog.setVisible(true);
+          }
+        }
+        else
+        {
+          if (e.getSource() instanceof FeatureTypeSettings)
+          {
+            /*
+             * update after OK in feature colour dialog; the updated
+             * colour will have already been set in the FeatureRenderer
+             */
+            FeatureColourI fci = fr.getFeatureColours().get(type);
+            table.setValueAt(fci, rowSelected, 1);
+            table.validate();
+          }
+          else
+          {
+            // probably the color chooser!
+            table.setValueAt(new FeatureColour(colorChooser.getColor()),
+                    rowSelected, 1);
+            table.validate();
+            me.updateFeatureRenderer(
+                    ((FeatureTableModel) table.getModel()).getData(),
+                    false);
+          }
+        }
       }
-    }
+
+    });
+
     JMenuItem selCols = new JMenuItem(
             MessageManager.getString("label.select_columns_containing"));
     selCols.addActionListener(new ActionListener()
@@ -472,20 +536,10 @@ public class FeatureSettings extends JPanel
     men.show(table, x, y);
   }
 
-  /**
-   * true when Feature Settings are updating from feature renderer
-   */
-  private boolean handlingUpdate = false;
-
-  /**
-   * holds {featureCount, totalExtent} for each feature type
-   */
-  Map<String, float[]> typeWidth = null;
-
   @Override
   synchronized public void discoverAllFeatureData()
   {
-    Set<String> allGroups = new HashSet<String>();
+    Set<String> allGroups = new HashSet<>();
     AlignmentI alignment = af.getViewport().getAlignment();
 
     for (int i = 0; i < alignment.getHeight(); i++)
@@ -528,27 +582,21 @@ public class FeatureSettings extends JPanel
     final String grp = group;
     final JCheckBox check = new JCheckBox(group, visible);
     check.setFont(new Font("Serif", Font.BOLD, 12));
+    check.setToolTipText(group);
     check.addItemListener(new ItemListener()
     {
       @Override
       public void itemStateChanged(ItemEvent evt)
       {
         fr.setGroupVisibility(check.getText(), check.isSelected());
-        af.alignPanel.getSeqPanel().seqCanvas.repaint();
-        if (af.alignPanel.overviewPanel != null)
-        {
-          af.alignPanel.overviewPanel.updateOverviewImage();
-        }
-
         resetTable(new String[] { grp });
+        af.alignPanel.paintAlignment(true, true);
       }
     });
     groupPanel.add(check);
     return visible;
   }
 
-  boolean resettingTable = false;
-
   synchronized void resetTable(String[] groupChanged)
   {
     if (resettingTable)
@@ -556,12 +604,12 @@ public class FeatureSettings extends JPanel
       return;
     }
     resettingTable = true;
-    typeWidth = new Hashtable<String, float[]>();
+    typeWidth = new Hashtable<>();
     // TODO: change avWidth calculation to 'per-sequence' average and use long
     // rather than float
 
-    Set<String> displayableTypes = new HashSet<String>();
-    Set<String> foundGroups = new HashSet<String>();
+    Set<String> displayableTypes = new HashSet<>();
+    Set<String> foundGroups = new HashSet<>();
 
     /*
      * determine which feature types may be visible depending on 
@@ -577,7 +625,7 @@ public class FeatureSettings extends JPanel
        * and keep track of which groups are visible
        */
       Set<String> groups = seq.getFeatures().getFeatureGroups(true);
-      Set<String> visibleGroups = new HashSet<String>();
+      Set<String> visibleGroups = new HashSet<>();
       for (String group : groups)
       {
         if (group == null || checkGroupState(group))
@@ -611,7 +659,7 @@ public class FeatureSettings extends JPanel
       }
     }
 
-    Object[][] data = new Object[displayableTypes.size()][3];
+    Object[][] data = new Object[displayableTypes.size()][COLUMN_COUNT];
     int dataIndex = 0;
 
     if (fr.hasRenderOrder())
@@ -634,9 +682,13 @@ public class FeatureSettings extends JPanel
           continue;
         }
 
-        data[dataIndex][0] = type;
-        data[dataIndex][1] = fr.getFeatureStyle(type);
-        data[dataIndex][2] = new Boolean(
+        data[dataIndex][TYPE_COLUMN] = type;
+        data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
+        FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
+        data[dataIndex][FILTER_COLUMN] = featureFilter == null
+                ? new FeatureMatcherSet()
+                : featureFilter;
+        data[dataIndex][SHOW_COLUMN] = new Boolean(
                 af.getViewport().getFeaturesDisplayed().isVisible(type));
         dataIndex++;
         displayableTypes.remove(type);
@@ -650,27 +702,30 @@ public class FeatureSettings extends JPanel
     while (!displayableTypes.isEmpty())
     {
       String type = displayableTypes.iterator().next();
-      data[dataIndex][0] = type;
+      data[dataIndex][TYPE_COLUMN] = type;
 
-      data[dataIndex][1] = fr.getFeatureStyle(type);
-      if (data[dataIndex][1] == null)
+      data[dataIndex][COLOUR_COLUMN] = fr.getFeatureStyle(type);
+      if (data[dataIndex][COLOUR_COLUMN] == null)
       {
         // "Colour has been updated in another view!!"
         fr.clearRenderOrder();
         return;
       }
-
-      data[dataIndex][2] = new Boolean(true);
+      FeatureMatcherSetI featureFilter = fr.getFeatureFilter(type);
+      data[dataIndex][FILTER_COLUMN] = featureFilter == null
+              ? new FeatureMatcherSet()
+              : featureFilter;
+      data[dataIndex][SHOW_COLUMN] = new Boolean(true);
       dataIndex++;
       displayableTypes.remove(type);
     }
 
     if (originalData == null)
     {
-      originalData = new Object[data.length][3];
+      originalData = new Object[data.length][COLUMN_COUNT];
       for (int i = 0; i < data.length; i++)
       {
-        System.arraycopy(data[i], 0, originalData[i], 0, 3);
+        System.arraycopy(data[i], 0, originalData[i], 0, COLUMN_COUNT);
       }
     }
     else
@@ -691,8 +746,8 @@ public class FeatureSettings extends JPanel
   }
 
   /**
-   * Updates 'originalData' (used for restore on Cancel) if we detect that
-   * changes have been made outwith this dialog
+   * Updates 'originalData' (used for restore on Cancel) if we detect that changes
+   * have been made outwith this dialog
    * <ul>
    * <li>a new feature type added (and made visible)</li>
    * <li>a feature colour changed (in the Amend Features dialog)</li>
@@ -708,27 +763,27 @@ public class FeatureSettings extends JPanel
             .getData();
     for (Object[] row : foundData)
     {
-      String type = (String) row[0];
+      String type = (String) row[TYPE_COLUMN];
       boolean found = false;
       for (Object[] current : currentData)
       {
-        if (type.equals(current[0]))
+        if (type.equals(current[TYPE_COLUMN]))
         {
           found = true;
           /*
            * currently dependent on object equality here;
            * really need an equals method on FeatureColour
            */
-          if (!row[1].equals(current[1]))
+          if (!row[COLOUR_COLUMN].equals(current[COLOUR_COLUMN]))
           {
             /*
              * feature colour has changed externally - update originalData
              */
             for (Object[] original : originalData)
             {
-              if (type.equals(original[0]))
+              if (type.equals(original[TYPE_COLUMN]))
               {
-                original[1] = row[1];
+                original[COLOUR_COLUMN] = row[COLOUR_COLUMN];
                 break;
               }
             }
@@ -741,10 +796,12 @@ public class FeatureSettings extends JPanel
         /*
          * new feature detected - add to original data (on top)
          */
-        Object[][] newData = new Object[originalData.length + 1][3];
+        Object[][] newData = new Object[originalData.length
+                + 1][COLUMN_COUNT];
         for (int i = 0; i < originalData.length; i++)
         {
-          System.arraycopy(originalData[i], 0, newData[i + 1], 0, 3);
+          System.arraycopy(originalData[i], 0, newData[i + 1], 0,
+                  COLUMN_COUNT);
         }
         newData[0] = row;
         originalData = newData;
@@ -754,8 +811,8 @@ public class FeatureSettings extends JPanel
 
   /**
    * Remove from the groups panel any checkboxes for groups that are not in the
-   * foundGroups set. This enables removing a group from the display when the
-   * last feature in that group is deleted.
+   * foundGroups set. This enables removing a group from the display when the last
+   * feature in that group is deleted.
    * 
    * @param foundGroups
    */
@@ -798,10 +855,14 @@ public class FeatureSettings extends JPanel
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then loads the feature colours and
+   * filters from file in XML format and unmarshals to Jalview feature settings
+   */
   void load()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
-            "Sequence Feature Colours");
+            SEQUENCE_FEATURE_COLOURS);
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
             MessageManager.getString("label.load_feature_colours"));
@@ -812,88 +873,78 @@ public class FeatureSettings extends JPanel
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
       File file = chooser.getSelectedFile();
+      load(file);
+    }
+  }
 
-      try
-      {
-        InputStreamReader in = new InputStreamReader(
-                new FileInputStream(file), "UTF-8");
+  /**
+   * Loads feature colours and filters from XML stored in the given file
+   * 
+   * @param file
+   */
+  void load(File file)
+  {
+    try
+    {
+      InputStreamReader in = new InputStreamReader(
+              new FileInputStream(file), "UTF-8");
 
-        JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
+      JalviewUserColours jucs = JalviewUserColours.unmarshal(in);
 
-        for (int i = jucs.getColourCount() - 1; i >= 0; i--)
-        {
-          String name;
-          jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
-          if (newcol.hasMax())
-          {
-            Color mincol = null, maxcol = null;
-            try
-            {
-              mincol = new Color(Integer.parseInt(newcol.getMinRGB(), 16));
-              maxcol = new Color(Integer.parseInt(newcol.getRGB(), 16));
+      /*
+       * load feature colours
+       */
+      for (int i = jucs.getColourCount() - 1; i >= 0; i--)
+      {
+        jalview.schemabinding.version2.Colour newcol = jucs.getColour(i);
+        FeatureColourI colour = Jalview2XML.unmarshalColour(newcol);
+        fr.setColour(newcol.getName(), colour);
+        fr.setOrder(newcol.getName(), i / (float) jucs.getColourCount());
+      }
 
-            } catch (Exception e)
-            {
-              Cache.log.warn("Couldn't parse out graduated feature color.",
-                      e);
-            }
-            FeatureColourI gcol = new FeatureColour(mincol, maxcol,
-                    newcol.getMin(), newcol.getMax());
-            if (newcol.hasAutoScale())
-            {
-              gcol.setAutoScaled(newcol.getAutoScale());
-            }
-            if (newcol.hasColourByLabel())
-            {
-              gcol.setColourByLabel(newcol.getColourByLabel());
-            }
-            if (newcol.hasThreshold())
-            {
-              gcol.setThreshold(newcol.getThreshold());
-            }
-            if (newcol.getThreshType().length() > 0)
-            {
-              String ttyp = newcol.getThreshType();
-              if (ttyp.equalsIgnoreCase("ABOVE"))
-              {
-                gcol.setAboveThreshold(true);
-              }
-              if (ttyp.equalsIgnoreCase("BELOW"))
-              {
-                gcol.setBelowThreshold(true);
-              }
-            }
-            fr.setColour(name = newcol.getName(), gcol);
-          }
-          else
-          {
-            Color color = new Color(
-                    Integer.parseInt(jucs.getColour(i).getRGB(), 16));
-            fr.setColour(name = jucs.getColour(i).getName(),
-                    new FeatureColour(color));
-          }
-          fr.setOrder(name, (i == 0) ? 0 : i / jucs.getColourCount());
-        }
-        if (table != null)
+      /*
+       * load feature filters; loaded filters will replace any that are
+       * currently defined, other defined filters are left unchanged 
+       */
+      for (int i = 0; i < jucs.getFilterCount(); i++)
+      {
+        jalview.schemabinding.version2.Filter filterModel = jucs
+                .getFilter(i);
+        String featureType = filterModel.getFeatureType();
+        FeatureMatcherSetI filter = Jalview2XML.unmarshalFilter(featureType,
+                filterModel.getMatcherSet());
+        if (!filter.isEmpty())
         {
-          resetTable(null);
-          Object[][] data = ((FeatureTableModel) table.getModel())
-                  .getData();
-          ensureOrder(data);
-          updateFeatureRenderer(data, false);
-          table.repaint();
+          fr.setFeatureFilter(featureType, filter);
         }
-      } catch (Exception ex)
-      {
-        System.out.println("Error loading User Colour File\n" + ex);
       }
+
+      /*
+       * update feature settings table
+       */
+      if (table != null)
+      {
+        resetTable(null);
+        Object[][] data = ((FeatureTableModel) table.getModel())
+                .getData();
+        ensureOrder(data);
+        updateFeatureRenderer(data, false);
+        table.repaint();
+      }
+    } catch (Exception ex)
+    {
+      System.out.println("Error loading User Colour File\n" + ex);
     }
   }
 
+  /**
+   * Offers a file chooser dialog, and then saves the current feature colours
+   * and any filters to the selected file in XML format
+   */
   void save()
   {
     JalviewFileChooser chooser = new JalviewFileChooser("fc",
-            "Sequence Feature Colours");
+            SEQUENCE_FEATURE_COLOURS);
     chooser.setFileView(new JalviewFileView());
     chooser.setDialogTitle(
             MessageManager.getString("label.save_feature_colours"));
@@ -903,68 +954,87 @@ public class FeatureSettings extends JPanel
 
     if (value == JalviewFileChooser.APPROVE_OPTION)
     {
-      String choice = chooser.getSelectedFile().getPath();
-      jalview.schemabinding.version2.JalviewUserColours ucs = new jalview.schemabinding.version2.JalviewUserColours();
-      ucs.setSchemeName("Sequence Features");
-      try
-      {
-        PrintWriter out = new PrintWriter(new OutputStreamWriter(
-                new FileOutputStream(choice), "UTF-8"));
+      save(chooser.getSelectedFile());
+    }
+  }
+
+  /**
+   * Saves feature colours and filters to the given file
+   * 
+   * @param file
+   */
+  void save(File file)
+  {
+    JalviewUserColours ucs = new JalviewUserColours();
+    ucs.setSchemeName("Sequence Features");
+    try
+    {
+      PrintWriter out = new PrintWriter(new OutputStreamWriter(
+              new FileOutputStream(file), "UTF-8"));
 
-        Set<String> fr_colours = fr.getAllFeatureColours();
-        Iterator<String> e = fr_colours.iterator();
-        float[] sortOrder = new float[fr_colours.size()];
-        String[] sortTypes = new String[fr_colours.size()];
-        int i = 0;
-        while (e.hasNext())
+      /*
+       * sort feature types by colour order, from 0 (highest)
+       * to 1 (lowest)
+       */
+      Set<String> fr_colours = fr.getAllFeatureColours();
+      String[] sortedTypes = fr_colours
+              .toArray(new String[fr_colours.size()]);
+      Arrays.sort(sortedTypes, new Comparator<String>()
+      {
+        @Override
+        public int compare(String type1, String type2)
         {
-          sortTypes[i] = e.next();
-          sortOrder[i] = fr.getOrder(sortTypes[i]);
-          i++;
+          return Float.compare(fr.getOrder(type1), fr.getOrder(type2));
         }
-        QuickSort.sort(sortOrder, sortTypes);
-        sortOrder = null;
-        for (i = 0; i < sortTypes.length; i++)
+      });
+
+      /*
+       * save feature colours
+       */
+      for (String featureType : sortedTypes)
+      {
+        FeatureColourI fcol = fr.getFeatureStyle(featureType);
+        jalview.schemabinding.version2.Colour col = Jalview2XML.marshalColour(
+                featureType, fcol);
+        ucs.addColour(col);
+      }
+
+      /*
+       * save any feature filters
+       */
+      for (String featureType : sortedTypes)
+      {
+        FeatureMatcherSetI filter = fr.getFeatureFilter(featureType);
+        if (filter != null && !filter.isEmpty())
         {
-          jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
-          col.setName(sortTypes[i]);
-          FeatureColourI fcol = fr.getFeatureStyle(sortTypes[i]);
-          if (fcol.isSimpleColour())
-          {
-            col.setRGB(Format.getHexString(fcol.getColour()));
-          }
-          else
-          {
-            col.setRGB(Format.getHexString(fcol.getMaxColour()));
-            col.setMin(fcol.getMin());
-            col.setMax(fcol.getMax());
-            col.setMinRGB(
-                    jalview.util.Format.getHexString(fcol.getMinColour()));
-            col.setAutoScale(fcol.isAutoScaled());
-            col.setThreshold(fcol.getThreshold());
-            col.setColourByLabel(fcol.isColourByLabel());
-            col.setThreshType(fcol.isAboveThreshold() ? "ABOVE"
-                    : (fcol.isBelowThreshold() ? "BELOW" : "NONE"));
-          }
-          ucs.addColour(col);
+          Iterator<FeatureMatcherI> iterator = filter.getMatchers().iterator();
+          FeatureMatcherI firstMatcher = iterator.next();
+          MatcherSet ms = Jalview2XML.marshalFilter(firstMatcher, iterator,
+                  filter.isAnded());
+          Filter filterModel = new Filter();
+          filterModel.setFeatureType(featureType);
+          filterModel.setMatcherSet(ms);
+          ucs.addFilter(filterModel);
         }
-        ucs.marshal(out);
-        out.close();
-      } catch (Exception ex)
-      {
-        ex.printStackTrace();
       }
+
+      ucs.marshal(out);
+      out.close();
+    } catch (Exception ex)
+    {
+      ex.printStackTrace();
     }
   }
 
   public void invertSelection()
   {
-    for (int i = 0; i < table.getRowCount(); i++)
+    Object[][] data = ((FeatureTableModel) table.getModel()).getData();
+    for (int i = 0; i < data.length; i++)
     {
-      Boolean value = (Boolean) table.getValueAt(i, 2);
-
-      table.setValueAt(new Boolean(!value.booleanValue()), i, 2);
+      data[i][SHOW_COLUMN] = !(Boolean) data[i][SHOW_COLUMN];
     }
+    updateFeatureRenderer(data, true);
+    table.repaint();
   }
 
   public void orderByAvWidth()
@@ -977,17 +1047,16 @@ public class FeatureSettings extends JPanel
     float[] width = new float[data.length];
     float[] awidth;
     float max = 0;
-    int num = 0;
+
     for (int i = 0; i < data.length; i++)
     {
-      awidth = typeWidth.get(data[i][0]);
+      awidth = typeWidth.get(data[i][TYPE_COLUMN]);
       if (awidth[0] > 0)
       {
         width[i] = awidth[1] / awidth[0];// *awidth[0]*awidth[2]; - better
         // weight - but have to make per
         // sequence, too (awidth[2])
         // if (width[i]==1) // hack to distinguish single width sequences.
-        num++;
       }
       else
       {
@@ -1004,16 +1073,17 @@ public class FeatureSettings extends JPanel
       // awidth = (float[]) typeWidth.get(data[i][0]);
       if (width[i] == 0)
       {
-        width[i] = fr.getOrder(data[i][0].toString());
+        width[i] = fr.getOrder(data[i][TYPE_COLUMN].toString());
         if (width[i] < 0)
         {
-          width[i] = fr.setOrder(data[i][0].toString(), i / data.length);
+          width[i] = fr.setOrder(data[i][TYPE_COLUMN].toString(),
+                  i / data.length);
         }
       }
       else
       {
         width[i] /= max; // normalize
-        fr.setOrder(data[i][0].toString(), width[i]); // store for later
+        fr.setOrder(data[i][TYPE_COLUMN].toString(), width[i]); // store for later
       }
       if (i > 0)
       {
@@ -1047,76 +1117,58 @@ public class FeatureSettings extends JPanel
   }
 
   /**
-   * Update the priority order of features; only repaint if this changed the
-   * order of visible features
+   * Update the priority order of features; only repaint if this changed the order
+   * of visible features
    * 
    * @param data
    * @param visibleNew
    */
   private void updateFeatureRenderer(Object[][] data, boolean visibleNew)
   {
-    if (fr.setFeaturePriority(data, visibleNew))
+    FeatureSettingsBean[] rowData = getTableAsBeans(data);
+
+    if (fr.setFeaturePriority(rowData, visibleNew))
     {
-      af.alignPanel.paintAlignment(true);
+      af.alignPanel.paintAlignment(true, true);
     }
   }
 
-  int selectedRow = -1;
-
-  JTabbedPane tabbedPane = new JTabbedPane();
-
-  BorderLayout borderLayout1 = new BorderLayout();
-
-  BorderLayout borderLayout2 = new BorderLayout();
-
-  BorderLayout borderLayout3 = new BorderLayout();
-
-  JPanel bigPanel = new JPanel();
-
-  BorderLayout borderLayout4 = new BorderLayout();
-
-  JButton invert = new JButton();
-
-  JPanel buttonPanel = new JPanel();
-
-  JButton cancel = new JButton();
-
-  JButton ok = new JButton();
-
-  JButton loadColours = new JButton();
-
-  JButton saveColours = new JButton();
-
-  JPanel dasButtonPanel = new JPanel();
-
-  JButton fetchDAS = new JButton();
-
-  JButton saveDAS = new JButton();
-
-  JButton cancelDAS = new JButton();
-
-  JButton optimizeOrder = new JButton();
-
-  JButton sortByScore = new JButton();
+  /**
+   * Converts table data into an array of data beans
+   */
+  private FeatureSettingsBean[] getTableAsBeans(Object[][] data)
+  {
+    FeatureSettingsBean[] rowData = new FeatureSettingsBean[data.length];
+    for (int i = 0; i < data.length; i++)
+    {
+      String type = (String) data[i][TYPE_COLUMN];
+      FeatureColourI colour = (FeatureColourI) data[i][COLOUR_COLUMN];
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) data[i][FILTER_COLUMN];
+      Boolean isShown = (Boolean) data[i][SHOW_COLUMN];
+      rowData[i] = new FeatureSettingsBean(type, colour, theFilter,
+              isShown);
+    }
+    return rowData;
+  }
 
-  JButton sortByDens = new JButton();
+  private void jbInit() throws Exception
+  {
+    this.setLayout(new BorderLayout());
 
-  JButton help = new JButton();
+    JPanel settingsPane = new JPanel();
+    settingsPane.setLayout(new BorderLayout());
 
-  JPanel transbuttons = new JPanel(new GridLayout(5, 1));
+    dasSettingsPane.setLayout(new BorderLayout());
 
-  private void jbInit() throws Exception
-  {
-    this.setLayout(borderLayout1);
-    settingsPane.setLayout(borderLayout2);
-    dasSettingsPane.setLayout(borderLayout3);
-    bigPanel.setLayout(borderLayout4);
+    JPanel bigPanel = new JPanel();
+    bigPanel.setLayout(new BorderLayout());
 
     groupPanel = new JPanel();
     bigPanel.add(groupPanel, BorderLayout.NORTH);
 
+    JButton invert = new JButton(
+            MessageManager.getString("label.invert_selection"));
     invert.setFont(JvSwingUtils.getLabelFont());
-    invert.setText(MessageManager.getString("label.invert_selection"));
     invert.addActionListener(new ActionListener()
     {
       @Override
@@ -1125,8 +1177,10 @@ public class FeatureSettings extends JPanel
         invertSelection();
       }
     });
+
+    JButton optimizeOrder = new JButton(
+            MessageManager.getString("label.optimise_order"));
     optimizeOrder.setFont(JvSwingUtils.getLabelFont());
-    optimizeOrder.setText(MessageManager.getString("label.optimise_order"));
     optimizeOrder.addActionListener(new ActionListener()
     {
       @Override
@@ -1135,9 +1189,10 @@ public class FeatureSettings extends JPanel
         orderByAvWidth();
       }
     });
+
+    JButton sortByScore = new JButton(
+            MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.setFont(JvSwingUtils.getLabelFont());
-    sortByScore
-            .setText(MessageManager.getString("label.seq_sort_by_score"));
     sortByScore.addActionListener(new ActionListener()
     {
       @Override
@@ -1146,9 +1201,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureScore(null);
       }
     });
-    sortByDens.setFont(JvSwingUtils.getLabelFont());
-    sortByDens.setText(
+    JButton sortByDens = new JButton(
             MessageManager.getString("label.sequence_sort_by_density"));
+    sortByDens.setFont(JvSwingUtils.getLabelFont());
     sortByDens.addActionListener(new ActionListener()
     {
       @Override
@@ -1157,8 +1212,9 @@ public class FeatureSettings extends JPanel
         af.avc.sortAlignmentByFeatureDensity(null);
       }
     });
+
+    JButton help = new JButton(MessageManager.getString("action.help"));
     help.setFont(JvSwingUtils.getLabelFont());
-    help.setText(MessageManager.getString("action.help"));
     help.addActionListener(new ActionListener()
     {
       @Override
@@ -1189,20 +1245,23 @@ public class FeatureSettings extends JPanel
         }
       }
     });
+
+    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     cancel.setFont(JvSwingUtils.getLabelFont());
-    cancel.setText(MessageManager.getString("action.cancel"));
     cancel.addActionListener(new ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
         fr.setTransparency(originalTransparency);
+        fr.setFeatureFilters(originalFilters);
         updateFeatureRenderer(originalData);
         close();
       }
     });
+
+    JButton ok = new JButton(MessageManager.getString("action.ok"));
     ok.setFont(JvSwingUtils.getLabelFont());
-    ok.setText(MessageManager.getString("action.ok"));
     ok.addActionListener(new ActionListener()
     {
       @Override
@@ -1211,8 +1270,12 @@ public class FeatureSettings extends JPanel
         close();
       }
     });
+
+    JButton loadColours = new JButton(
+            MessageManager.getString("label.load_colours"));
     loadColours.setFont(JvSwingUtils.getLabelFont());
-    loadColours.setText(MessageManager.getString("label.load_colours"));
+    loadColours.setToolTipText(
+            MessageManager.getString("label.load_colours_tooltip"));
     loadColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1221,8 +1284,12 @@ public class FeatureSettings extends JPanel
         load();
       }
     });
+
+    JButton saveColours = new JButton(
+            MessageManager.getString("label.save_colours"));
     saveColours.setFont(JvSwingUtils.getLabelFont());
-    saveColours.setText(MessageManager.getString("label.save_colours"));
+    saveColours.setToolTipText(
+            MessageManager.getString("label.save_colours_tooltip"));
     saveColours.addActionListener(new ActionListener()
     {
       @Override
@@ -1236,8 +1303,11 @@ public class FeatureSettings extends JPanel
       @Override
       public void stateChanged(ChangeEvent evt)
       {
-        fr.setTransparency((100 - transparency.getValue()) / 100f);
-        af.alignPanel.paintAlignment(true);
+        if (!inConstruction)
+        {
+          fr.setTransparency((100 - transparency.getValue()) / 100f);
+          af.alignPanel.paintAlignment(true, true);
+        }
       }
     });
 
@@ -1262,6 +1332,8 @@ public class FeatureSettings extends JPanel
         saveDAS_actionPerformed(e);
       }
     });
+
+    JPanel dasButtonPanel = new JPanel();
     dasButtonPanel.setBorder(BorderFactory.createEtchedBorder());
     dasSettingsPane.setBorder(null);
     cancelDAS.setEnabled(false);
@@ -1274,32 +1346,32 @@ public class FeatureSettings extends JPanel
         cancelDAS_actionPerformed(e);
       }
     });
-    this.add(tabbedPane, java.awt.BorderLayout.CENTER);
-    tabbedPane.addTab(MessageManager.getString("label.feature_settings"),
-            settingsPane);
-    tabbedPane.addTab(MessageManager.getString("label.das_settings"),
-            dasSettingsPane);
-    bigPanel.add(transPanel, java.awt.BorderLayout.SOUTH);
+
+    JPanel transPanel = new JPanel(new GridLayout(1, 2));
+    bigPanel.add(transPanel, BorderLayout.SOUTH);
+
+    JPanel transbuttons = new JPanel(new GridLayout(5, 1));
     transbuttons.add(optimizeOrder);
     transbuttons.add(invert);
     transbuttons.add(sortByScore);
     transbuttons.add(sortByDens);
     transbuttons.add(help);
-    JPanel sliderPanel = new JPanel();
-    sliderPanel.add(transparency);
     transPanel.add(transparency);
     transPanel.add(transbuttons);
+
+    JPanel buttonPanel = new JPanel();
     buttonPanel.add(ok);
     buttonPanel.add(cancel);
     buttonPanel.add(loadColours);
     buttonPanel.add(saveColours);
-    bigPanel.add(scrollPane, java.awt.BorderLayout.CENTER);
-    dasSettingsPane.add(dasButtonPanel, java.awt.BorderLayout.SOUTH);
+    bigPanel.add(scrollPane, BorderLayout.CENTER);
+    dasSettingsPane.add(dasButtonPanel, BorderLayout.SOUTH);
     dasButtonPanel.add(fetchDAS);
     dasButtonPanel.add(cancelDAS);
     dasButtonPanel.add(saveDAS);
-    settingsPane.add(bigPanel, java.awt.BorderLayout.CENTER);
-    settingsPane.add(buttonPanel, java.awt.BorderLayout.SOUTH);
+    settingsPane.add(bigPanel, BorderLayout.CENTER);
+    settingsPane.add(buttonPanel, BorderLayout.SOUTH);
+    this.add(settingsPane);
   }
 
   public void fetchDAS_actionPerformed(ActionEvent e)
@@ -1464,18 +1536,19 @@ public class FeatureSettings extends JPanel
   // ///////////////////////////////////////////////////////////////////////
   class FeatureTableModel extends AbstractTableModel
   {
-    FeatureTableModel(Object[][] data)
-    {
-      this.data = data;
-    }
-
     private String[] columnNames = {
         MessageManager.getString("label.feature_type"),
         MessageManager.getString("action.colour"),
-        MessageManager.getString("label.display") };
+        MessageManager.getString("label.filter"),
+        MessageManager.getString("label.show") };
 
     private Object[][] data;
 
+    FeatureTableModel(Object[][] data)
+    {
+      this.data = data;
+    }
+
     public Object[][] getData()
     {
       return data;
@@ -1515,10 +1588,14 @@ public class FeatureSettings extends JPanel
       return data[row][col];
     }
 
+    /**
+     * Answers the class of the object in column c of the first row of the table
+     */
     @Override
-    public Class getColumnClass(int c)
+    public Class<?> getColumnClass(int c)
     {
-      return getValueAt(0, c).getClass();
+      Object v = getValueAt(0, c);
+      return v == null ? null : v.getClass();
     }
 
     @Override
@@ -1557,12 +1634,7 @@ public class FeatureSettings extends JPanel
             boolean isSelected, boolean hasFocus, int row, int column)
     {
       FeatureColourI cellColour = (FeatureColourI) color;
-      // JLabel comp = new JLabel();
-      // comp.
       setOpaque(true);
-      // comp.
-      // setBounds(getBounds());
-      Color newColor;
       setToolTipText(baseTT);
       setBackground(tbl.getBackground());
       if (!cellColour.isSimpleColour())
@@ -1570,14 +1642,12 @@ public class FeatureSettings extends JPanel
         Rectangle cr = tbl.getCellRect(row, column, false);
         FeatureSettings.renderGraduatedColor(this, cellColour,
                 (int) cr.getWidth(), (int) cr.getHeight());
-
       }
       else
       {
         this.setText("");
         this.setIcon(null);
-        newColor = cellColour.getColour();
-        setBackground(newColor);
+        setBackground(cellColour.getColour());
       }
       if (isSelected)
       {
@@ -1602,6 +1672,54 @@ public class FeatureSettings extends JPanel
     }
   }
 
+  class FilterRenderer extends JLabel implements TableCellRenderer
+  {
+    javax.swing.border.Border unselectedBorder = null;
+
+    javax.swing.border.Border selectedBorder = null;
+
+    public FilterRenderer()
+    {
+      setOpaque(true); // MUST do this for background to show up.
+      setHorizontalTextPosition(SwingConstants.CENTER);
+      setVerticalTextPosition(SwingConstants.CENTER);
+    }
+
+    @Override
+    public Component getTableCellRendererComponent(JTable tbl,
+            Object filter, boolean isSelected, boolean hasFocus, int row,
+            int column)
+    {
+      FeatureMatcherSetI theFilter = (FeatureMatcherSetI) filter;
+      setOpaque(true);
+      String asText = theFilter.toString();
+      setBackground(tbl.getBackground());
+      this.setText(asText);
+      this.setIcon(null);
+
+      if (isSelected)
+      {
+        if (selectedBorder == null)
+        {
+          selectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
+                  tbl.getSelectionBackground());
+        }
+        setBorder(selectedBorder);
+      }
+      else
+      {
+        if (unselectedBorder == null)
+        {
+          unselectedBorder = BorderFactory.createMatteBorder(2, 5, 2, 5,
+                  tbl.getBackground());
+        }
+        setBorder(unselectedBorder);
+      }
+
+      return this;
+    }
+  }
+
   /**
    * update comp using rendering settings from gcol
    * 
@@ -1628,28 +1746,43 @@ public class FeatureSettings extends JPanel
           int w, int h)
   {
     boolean thr = false;
-    String tt = "";
-    String tx = "";
+    StringBuilder tt = new StringBuilder();
+    StringBuilder tx = new StringBuilder();
+
+    if (gcol.isColourByAttribute())
+    {
+      tx.append(String.join(":", gcol.getAttributeName()));
+    }
+    else if (!gcol.isColourByLabel())
+    {
+      tx.append(MessageManager.getString("label.score"));
+    }
+    tx.append(" ");
     if (gcol.isAboveThreshold())
     {
       thr = true;
-      tx += ">";
-      tt += "Thresholded (Above " + gcol.getThreshold() + ") ";
+      tx.append(">");
+      tt.append("Thresholded (Above ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isBelowThreshold())
     {
       thr = true;
-      tx += "<";
-      tt += "Thresholded (Below " + gcol.getThreshold() + ") ";
+      tx.append("<");
+      tt.append("Thresholded (Below ").append(gcol.getThreshold())
+              .append(") ");
     }
     if (gcol.isColourByLabel())
     {
-      tt = "Coloured by label text. " + tt;
+      tt.append("Coloured by label text. ").append(tt);
       if (thr)
       {
-        tx += " ";
+        tx.append(" ");
+      }
+      if (!gcol.isColourByAttribute())
+      {
+        tx.append("Label");
       }
-      tx += "Label";
       comp.setIcon(null);
     }
     else
@@ -1665,17 +1798,257 @@ public class FeatureSettings extends JPanel
       // + ", " + minCol.getBlue() + ")");
     }
     comp.setHorizontalAlignment(SwingConstants.CENTER);
-    comp.setText(tx);
+    comp.setText(tx.toString());
     if (tt.length() > 0)
     {
       if (comp.getToolTipText() == null)
       {
-        comp.setToolTipText(tt);
+        comp.setToolTipText(tt.toString());
+      }
+      else
+      {
+        comp.setToolTipText(
+                tt.append(" ").append(comp.getToolTipText()).toString());
+      }
+    }
+  }
+
+  class ColorEditor extends AbstractCellEditor
+          implements TableCellEditor, ActionListener
+  {
+    FeatureSettings me;
+
+    FeatureColourI currentColor;
+
+    FeatureTypeSettings chooser;
+
+    String type;
+
+    JButton button;
+
+    JColorChooser colorChooser;
+
+    JDialog dialog;
+
+    protected static final String EDIT = "edit";
+
+    int rowSelected = 0;
+
+    public ColorEditor(FeatureSettings me)
+    {
+      this.me = me;
+      // Set up the editor (from the table's point of view),
+      // which is a button.
+      // This button brings up the color chooser dialog,
+      // which is the editor from the user's point of view.
+      button = new JButton();
+      button.setActionCommand(EDIT);
+      button.addActionListener(this);
+      button.setBorderPainted(false);
+      // Set up the dialog that the button brings up.
+      colorChooser = new JColorChooser();
+      dialog = JColorChooser.createDialog(button,
+              MessageManager.getString("label.select_colour"), true, // modal
+              colorChooser, this, // OK button handler
+              null); // no CANCEL button handler
+    }
+
+    /**
+     * Handles events from the editor button and from the dialog's OK button.
+     */
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      // todo test e.getSource() instead here
+      if (EDIT.equals(e.getActionCommand()))
+      {
+        // The user has clicked the cell, so
+        // bring up the dialog.
+        if (currentColor.isSimpleColour())
+        {
+          // bring up simple color chooser
+          button.setBackground(currentColor.getColour());
+          colorChooser.setColor(currentColor.getColour());
+          dialog.setVisible(true);
+        }
+        else
+        {
+          // bring up graduated chooser.
+          chooser = new FeatureTypeSettings(me.fr, type);
+          chooser.setRequestFocusEnabled(true);
+          chooser.requestFocus();
+          chooser.addActionListener(this);
+          chooser.showTab(true);
+        }
+        // Make the renderer reappear.
+        fireEditingStopped();
+
+      }
+      else
+      {
+        if (currentColor.isSimpleColour())
+        {
+          /*
+           * read off colour picked in colour chooser after OK pressed
+           */
+          currentColor = new FeatureColour(colorChooser.getColor());
+          me.table.setValueAt(currentColor, rowSelected, COLOUR_COLUMN);
+        }
+        else
+        {
+          /*
+           * after OK in variable colour dialog, any changes to colour 
+           * (or filters!) are already set in FeatureRenderer, so just
+           * update table data without triggering updateFeatureRenderer
+           */
+          currentColor = fr.getFeatureColours().get(type);
+          FeatureMatcherSetI currentFilter = me.fr.getFeatureFilter(type);
+          if (currentFilter == null)
+          {
+            currentFilter = new FeatureMatcherSet();
+          }
+          Object[] data = ((FeatureTableModel) table.getModel())
+                  .getData()[rowSelected];
+          data[COLOUR_COLUMN] = currentColor;
+          data[FILTER_COLUMN] = currentFilter;
+        }
+        fireEditingStopped();
+        me.table.validate();
+      }
+    }
+
+    // Implement the one CellEditor method that AbstractCellEditor doesn't.
+    @Override
+    public Object getCellEditorValue()
+    {
+      return currentColor;
+    }
+
+    // Implement the one method defined by TableCellEditor.
+    @Override
+    public Component getTableCellEditorComponent(JTable theTable, Object value,
+            boolean isSelected, int row, int column)
+    {
+      currentColor = (FeatureColourI) value;
+      this.rowSelected = row;
+      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+      button.setOpaque(true);
+      button.setBackground(me.getBackground());
+      if (!currentColor.isSimpleColour())
+      {
+        JLabel btn = new JLabel();
+        btn.setSize(button.getSize());
+        FeatureSettings.renderGraduatedColor(btn, currentColor);
+        button.setBackground(btn.getBackground());
+        button.setIcon(btn.getIcon());
+        button.setText(btn.getText());
       }
       else
       {
-        comp.setToolTipText(tt + " " + comp.getToolTipText());
+        button.setText("");
+        button.setIcon(null);
+        button.setBackground(currentColor.getColour());
       }
+      return button;
+    }
+  }
+
+  /**
+   * The cell editor for the Filter column. It displays the text of any filters
+   * for the feature type in that row (in full as a tooltip, possible abbreviated
+   * as display text). On click in the cell, opens the Feature Display Settings
+   * dialog at the Filters tab.
+   */
+  class FilterEditor extends AbstractCellEditor
+          implements TableCellEditor, ActionListener
+  {
+    FeatureSettings me;
+
+    FeatureMatcherSetI currentFilter;
+
+    Point lastLocation;
+
+    String type;
+
+    JButton button;
+
+    protected static final String EDIT = "edit";
+
+    int rowSelected = 0;
+
+    public FilterEditor(FeatureSettings me)
+    {
+      this.me = me;
+      button = new JButton();
+      button.setActionCommand(EDIT);
+      button.addActionListener(this);
+      button.setBorderPainted(false);
+    }
+
+    /**
+     * Handles events from the editor button
+     */
+    @Override
+    public void actionPerformed(ActionEvent e)
+    {
+      if (button == e.getSource())
+      {
+        FeatureTypeSettings chooser = new FeatureTypeSettings(me.fr, type);
+        chooser.addActionListener(this);
+        chooser.setRequestFocusEnabled(true);
+        chooser.requestFocus();
+        if (lastLocation != null)
+        {
+          // todo open at its last position on screen
+          chooser.setBounds(lastLocation.x, lastLocation.y,
+                  chooser.getWidth(), chooser.getHeight());
+          chooser.validate();
+        }
+        chooser.showTab(false);
+        fireEditingStopped();
+      }
+      else if (e.getSource() instanceof Component)
+      {
+
+        /*
+         * after OK in variable colour dialog, any changes to filter
+         * (or colours!) are already set in FeatureRenderer, so just
+         * update table data without triggering updateFeatureRenderer
+         */
+        FeatureColourI currentColor = fr.getFeatureColours().get(type);
+        currentFilter = me.fr.getFeatureFilter(type);
+        if (currentFilter == null)
+        {
+          currentFilter = new FeatureMatcherSet();
+        }
+        Object[] data = ((FeatureTableModel) table.getModel())
+                .getData()[rowSelected];
+        data[COLOUR_COLUMN] = currentColor;
+        data[FILTER_COLUMN] = currentFilter;
+        fireEditingStopped();
+        me.table.validate();
+      }
+    }
+
+    @Override
+    public Object getCellEditorValue()
+    {
+      return currentFilter;
+    }
+
+    @Override
+    public Component getTableCellEditorComponent(JTable theTable, Object value,
+            boolean isSelected, int row, int column)
+    {
+      currentFilter = (FeatureMatcherSetI) value;
+      this.rowSelected = row;
+      type = me.table.getValueAt(row, TYPE_COLUMN).toString();
+      button.setOpaque(true);
+      button.setBackground(me.getBackground());
+      button.setText(currentFilter.toString());
+      button.setToolTipText(currentFilter.toString());
+      button.setIcon(null);
+      return button;
     }
   }
 }
@@ -1761,124 +2134,3 @@ class FeatureIcon implements Icon
     }
   }
 }
-
-class ColorEditor extends AbstractCellEditor
-        implements TableCellEditor, ActionListener
-{
-  FeatureSettings me;
-
-  FeatureColourI currentColor;
-
-  FeatureColourChooser chooser;
-
-  String type;
-
-  JButton button;
-
-  JColorChooser colorChooser;
-
-  JDialog dialog;
-
-  protected static final String EDIT = "edit";
-
-  int selectedRow = 0;
-
-  public ColorEditor(FeatureSettings me)
-  {
-    this.me = me;
-    // Set up the editor (from the table's point of view),
-    // which is a button.
-    // This button brings up the color chooser dialog,
-    // which is the editor from the user's point of view.
-    button = new JButton();
-    button.setActionCommand(EDIT);
-    button.addActionListener(this);
-    button.setBorderPainted(false);
-    // Set up the dialog that the button brings up.
-    colorChooser = new JColorChooser();
-    dialog = JColorChooser.createDialog(button, "Select new Colour", true, // modal
-            colorChooser, this, // OK button handler
-            null); // no CANCEL button handler
-  }
-
-  /**
-   * Handles events from the editor button and from the dialog's OK button.
-   */
-  @Override
-  public void actionPerformed(ActionEvent e)
-  {
-
-    if (EDIT.equals(e.getActionCommand()))
-    {
-      // The user has clicked the cell, so
-      // bring up the dialog.
-      if (currentColor.isSimpleColour())
-      {
-        // bring up simple color chooser
-        button.setBackground(currentColor.getColour());
-        colorChooser.setColor(currentColor.getColour());
-        dialog.setVisible(true);
-      }
-      else
-      {
-        // bring up graduated chooser.
-        chooser = new FeatureColourChooser(me.fr, type);
-        chooser.setRequestFocusEnabled(true);
-        chooser.requestFocus();
-        chooser.addActionListener(this);
-      }
-      // Make the renderer reappear.
-      fireEditingStopped();
-
-    }
-    else
-    { // User pressed dialog's "OK" button.
-      if (currentColor.isSimpleColour())
-      {
-        currentColor = new FeatureColour(colorChooser.getColor());
-      }
-      else
-      {
-        currentColor = chooser.getLastColour();
-      }
-      me.table.setValueAt(getCellEditorValue(), selectedRow, 1);
-      fireEditingStopped();
-      me.table.validate();
-    }
-  }
-
-  // Implement the one CellEditor method that AbstractCellEditor doesn't.
-  @Override
-  public Object getCellEditorValue()
-  {
-    return currentColor;
-  }
-
-  // Implement the one method defined by TableCellEditor.
-  @Override
-  public Component getTableCellEditorComponent(JTable table, Object value,
-          boolean isSelected, int row, int column)
-  {
-    currentColor = (FeatureColourI) value;
-    this.selectedRow = row;
-    type = me.table.getValueAt(row, 0).toString();
-    button.setOpaque(true);
-    button.setBackground(me.getBackground());
-    if (!currentColor.isSimpleColour())
-    {
-      JLabel btn = new JLabel();
-      btn.setSize(button.getSize());
-      FeatureSettings.renderGraduatedColor(btn, currentColor);
-      button.setBackground(btn.getBackground());
-      button.setIcon(btn.getIcon());
-      button.setText(btn.getText());
-    }
-    else
-    {
-      button.setText("");
-      button.setIcon(null);
-      button.setBackground(currentColor.getColour());
-    }
-    return button;
-  }
-}
diff --git a/src/jalview/gui/FeatureTypeSettings.java b/src/jalview/gui/FeatureTypeSettings.java
new file mode 100644 (file)
index 0000000..6eb583c
--- /dev/null
@@ -0,0 +1,1737 @@
+/*
+ * 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.AlignmentViewPanel;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.GraphLine;
+import jalview.datamodel.features.FeatureAttributes;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.schemes.FeatureColour;
+import jalview.util.ColorUtils;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusAdapter;
+import java.awt.event.FocusEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSlider;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicArrowButton;
+
+/**
+ * A dialog where the user can configure colour scheme, and any filters, for one
+ * feature type
+ * <p>
+ * (Was FeatureColourChooser prior to Jalview 1.11, renamed with the addition of
+ * filter options)
+ */
+public class FeatureTypeSettings extends JalviewDialog
+{
+  private final static String LABEL_18N = MessageManager
+          .getString("label.label");
+
+  private final static String SCORE_18N = MessageManager
+          .getString("label.score");
+
+  private static final int RADIO_WIDTH = 130;
+
+  private static final String COLON = ":";
+
+  private static final int MAX_TOOLTIP_LENGTH = 50;
+
+  private static final int NO_COLOUR_OPTION = 0;
+
+  private static final int MIN_COLOUR_OPTION = 1;
+
+  private static final int MAX_COLOUR_OPTION = 2;
+
+  private static final int ABOVE_THRESHOLD_OPTION = 1;
+
+  private static final int BELOW_THRESHOLD_OPTION = 2;
+
+  private static final DecimalFormat DECFMT_2_2 = new DecimalFormat(
+          "##.##");
+
+  /*
+   * FeatureRenderer holds colour scheme and filters for feature types
+   */
+  private final FeatureRenderer fr; // todo refactor to allow interface type here
+
+  /*
+   * the view panel to update when settings change
+   */
+  private final AlignmentViewPanel ap;
+
+  private final String featureType;
+
+  /*
+   * the colour and filters to reset to on Cancel
+   */
+  private final FeatureColourI originalColour;
+
+  private final FeatureMatcherSetI originalFilter;
+
+  /*
+   * set flag to true when setting values programmatically,
+   * to avoid invocation of action handlers
+   */
+  private boolean adjusting = false;
+
+  /*
+   * minimum of the value range for graduated colour
+   * (may be for feature score or for a numeric attribute)
+   */
+  private float min;
+
+  /*
+   * maximum of the value range for graduated colour
+   */
+  private float max;
+
+  /*
+   * scale factor for conversion between absolute min-max and slider
+   */
+  private float scaleFactor;
+
+  /*
+   * radio button group, to select what to colour by:
+   * simple colour, by category (text), or graduated
+   */
+  private JRadioButton simpleColour = new JRadioButton();
+
+  private JRadioButton byCategory = new JRadioButton();
+
+  private JRadioButton graduatedColour = new JRadioButton();
+
+  private JPanel singleColour = new JPanel();
+
+  private JPanel minColour = new JPanel();
+
+  private JPanel maxColour = new JPanel();
+
+  private JComboBox<String> threshold = new JComboBox<>();
+
+  private JSlider slider = new JSlider();
+
+  private JTextField thresholdValue = new JTextField(20);
+
+  private JCheckBox thresholdIsMin = new JCheckBox();
+
+  private GraphLine threshline;
+
+  private ActionListener featureSettings = null;
+
+  private ActionListener changeColourAction;
+
+  /*
+   * choice of option for 'colour for no value'
+   */
+  private JComboBox<String> noValueCombo;
+
+  /*
+   * choice of what to colour by text (Label or attribute)
+   */
+  private JComboBox<String> colourByTextCombo;
+
+  /*
+   * choice of what to colour by range (Score or attribute)
+   */
+  private JComboBox<String> colourByRangeCombo;
+
+  private JRadioButton andFilters;
+
+  private JRadioButton orFilters;
+
+  /*
+   * filters for the currently selected feature type
+   */
+  private List<FeatureMatcherI> filters;
+
+  // set white normally, black to debug layout
+  private Color debugBorderColour = Color.white;
+
+  private JPanel chooseFiltersPanel;
+
+  private JTabbedPane tabbedPane;
+
+  /**
+   * Constructor
+   * 
+   * @param frender
+   * @param theType
+   */
+  public FeatureTypeSettings(FeatureRenderer frender, String theType)
+  {
+    this(frender, false, theType);
+  }
+
+  /**
+   * Constructor, with option to make a blocking dialog (has to complete in the
+   * AWT event queue thread). Currently this option is always set to false.
+   * 
+   * @param frender
+   * @param blocking
+   * @param theType
+   */
+  FeatureTypeSettings(FeatureRenderer frender, boolean blocking,
+          String theType)
+  {
+    this.fr = frender;
+    this.featureType = theType;
+    ap = fr.ap;
+    originalFilter = fr.getFeatureFilter(theType);
+    originalColour = fr.getFeatureColours().get(theType);
+
+    adjusting = true;
+
+    try
+    {
+      initialise();
+    } catch (Exception ex)
+    {
+      ex.printStackTrace();
+      return;
+    }
+
+    updateColoursTab();
+
+    updateFiltersTab();
+
+    adjusting = false;
+
+    colourChanged(false);
+
+    String title = MessageManager
+            .formatMessage("label.display_settings_for", new String[]
+            { theType });
+    initDialogFrame(this, true, blocking, title, 600, 360);
+
+    waitForInput();
+  }
+
+  /**
+   * Configures the widgets on the Colours tab according to the current feature
+   * colour scheme
+   */
+  private void updateColoursTab()
+  {
+    FeatureColourI fc = fr.getFeatureColours().get(featureType);
+
+    /*
+     * suppress action handling while updating values programmatically
+     */
+    adjusting = true;
+    try
+    {
+      /*
+       * single colour
+       */
+      if (fc.isSimpleColour())
+      {
+        simpleColour.setSelected(true);
+        singleColour.setBackground(fc.getColour());
+        singleColour.setForeground(fc.getColour());
+      }
+
+      /*
+       * colour by text (Label or attribute text)
+       */
+      if (fc.isColourByLabel())
+      {
+        byCategory.setSelected(true);
+        colourByTextCombo.setEnabled(colourByTextCombo.getItemCount() > 1);
+        if (fc.isColourByAttribute())
+        {
+          String[] attributeName = fc.getAttributeName();
+          colourByTextCombo.setSelectedItem(
+                  FeatureMatcher.toAttributeDisplayName(attributeName));
+        }
+        else
+        {
+          colourByTextCombo.setSelectedItem(LABEL_18N);
+        }
+      }
+      else
+      {
+        colourByTextCombo.setEnabled(false);
+      }
+
+      if (!fc.isGraduatedColour())
+      {
+        colourByRangeCombo.setEnabled(false);
+        minColour.setEnabled(false);
+        maxColour.setEnabled(false);
+        noValueCombo.setEnabled(false);
+        threshold.setEnabled(false);
+        slider.setEnabled(false);
+        thresholdValue.setEnabled(false);
+        thresholdIsMin.setEnabled(false);
+        return;
+      }
+
+      /*
+       * Graduated colour, by score or attribute value range
+       */
+      graduatedColour.setSelected(true);
+      updateColourMinMax(); // ensure min, max are set
+      colourByRangeCombo.setEnabled(colourByRangeCombo.getItemCount() > 1);
+      minColour.setEnabled(true);
+      maxColour.setEnabled(true);
+      noValueCombo.setEnabled(true);
+      threshold.setEnabled(true);
+      minColour.setBackground(fc.getMinColour());
+      maxColour.setBackground(fc.getMaxColour());
+
+      if (fc.isColourByAttribute())
+      {
+        String[] attributeName = fc.getAttributeName();
+        colourByRangeCombo.setSelectedItem(
+                FeatureMatcher.toAttributeDisplayName(attributeName));
+      }
+      else
+      {
+        colourByRangeCombo.setSelectedItem(SCORE_18N);
+      }
+      Color noColour = fc.getNoColour();
+      if (noColour == null)
+      {
+        noValueCombo.setSelectedIndex(NO_COLOUR_OPTION);
+      }
+      else if (noColour.equals(fc.getMinColour()))
+      {
+        noValueCombo.setSelectedIndex(MIN_COLOUR_OPTION);
+      }
+      else if (noColour.equals(fc.getMaxColour()))
+      {
+        noValueCombo.setSelectedIndex(MAX_COLOUR_OPTION);
+      }
+
+      /*
+       * update min-max scaling if there is a range to work with,
+       * else disable the widgets (this shouldn't happen if only 
+       * valid options are offered in the combo box)
+       */
+      scaleFactor = (max == min) ? 1f : 100f / (max - min);
+      float range = (max - min) * scaleFactor;
+      slider.setMinimum((int) (min * scaleFactor));
+      slider.setMaximum((int) (max * scaleFactor));
+      slider.setMajorTickSpacing((int) (range / 10f));
+
+      threshline = new GraphLine((max - min) / 2f, "Threshold",
+              Color.black);
+      threshline.value = fc.getThreshold();
+
+      if (fc.hasThreshold())
+      {
+        threshold.setSelectedIndex(
+                fc.isAboveThreshold() ? ABOVE_THRESHOLD_OPTION
+                        : BELOW_THRESHOLD_OPTION);
+        slider.setEnabled(true);
+        slider.setValue((int) (fc.getThreshold() * scaleFactor));
+        thresholdValue.setText(String.valueOf(getRoundedSliderValue()));
+        thresholdValue.setEnabled(true);
+        thresholdIsMin.setEnabled(true);
+      }
+      else
+      {
+        slider.setEnabled(false);
+        thresholdValue.setEnabled(false);
+        thresholdIsMin.setEnabled(false);
+      }
+      thresholdIsMin.setSelected(!fc.isAutoScaled());
+    } finally
+    {
+      adjusting = false;
+    }
+  }
+
+  /**
+   * Configures the initial layout
+   */
+  private void initialise()
+  {
+    this.setLayout(new BorderLayout());
+    tabbedPane = new JTabbedPane();
+    this.add(tabbedPane, BorderLayout.CENTER);
+
+    /*
+     * an ActionListener that applies colour changes
+     */
+    changeColourAction = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        colourChanged(true);
+      }
+    };
+
+    /*
+     * first tab: colour options
+     */
+    JPanel coloursPanel = initialiseColoursPanel();
+    tabbedPane.addTab(MessageManager.getString("action.colour"),
+            coloursPanel);
+
+    /*
+     * second tab: filter options
+     */
+    JPanel filtersPanel = initialiseFiltersPanel();
+    tabbedPane.addTab(MessageManager.getString("label.filters"),
+            filtersPanel);
+
+    JPanel okCancelPanel = initialiseOkCancelPanel();
+
+    this.add(okCancelPanel, BorderLayout.SOUTH);
+  }
+
+  /**
+   * Updates the min-max range if Colour By selected item is Score, or an
+   * attribute, with a min-max range
+   */
+  protected void updateColourMinMax()
+  {
+    if (!graduatedColour.isSelected())
+    {
+      return;
+    }
+
+    String colourBy = (String) colourByRangeCombo.getSelectedItem();
+    float[] minMax = getMinMax(colourBy);
+
+    if (minMax != null)
+    {
+      min = minMax[0];
+      max = minMax[1];
+    }
+  }
+
+  /**
+   * Retrieves the min-max range:
+   * <ul>
+   * <li>of feature score, if colour or filter is by Score</li>
+   * <li>else of the selected attribute</li>
+   * </ul>
+   * 
+   * @param attName
+   * @return
+   */
+  private float[] getMinMax(String attName)
+  {
+    float[] minMax = null;
+    if (SCORE_18N.equals(attName))
+    {
+      minMax = fr.getMinMax().get(featureType)[0];
+    }
+    else
+    {
+      // colour by attribute range
+      minMax = FeatureAttributes.getInstance().getMinMax(featureType,
+              FeatureMatcher.fromAttributeDisplayName(attName));
+    }
+    return minMax;
+  }
+
+  /**
+   * Lay out fields for graduated colour (by score or attribute value)
+   * 
+   * @return
+   */
+  private JPanel initialiseGraduatedColourPanel()
+  {
+    JPanel graduatedColourPanel = new JPanel();
+    graduatedColourPanel.setLayout(
+            new BoxLayout(graduatedColourPanel, BoxLayout.Y_AXIS));
+    JvSwingUtils.createTitledBorder(graduatedColourPanel,
+            MessageManager.getString("label.graduated_colour"), true);
+    graduatedColourPanel.setBackground(Color.white);
+
+    /*
+     * first row: graduated colour radio button, score/attribute drop-down
+     */
+    JPanel graduatedChoicePanel = new JPanel(
+            new FlowLayout(FlowLayout.LEFT));
+    graduatedChoicePanel.setBackground(Color.white);
+    graduatedColour = new JRadioButton(
+            MessageManager.getString("label.by_range_of") + COLON);
+    graduatedColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    graduatedColour.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (graduatedColour.isSelected())
+        {
+          colourChanged(true);
+        }
+      }
+    });
+    graduatedChoicePanel.add(graduatedColour);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+    colourByRangeCombo = populateAttributesDropdown(attNames, true, false);
+    colourByRangeCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+
+    /*
+     * disable graduated colour option if no range found
+     */
+    graduatedColour.setEnabled(colourByRangeCombo.getItemCount() > 0);
+
+    graduatedChoicePanel.add(colourByRangeCombo);
+    graduatedColourPanel.add(graduatedChoicePanel);
+
+    /*
+     * second row - min/max/no colours
+     */
+    JPanel colourRangePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    colourRangePanel.setBackground(Color.white);
+    graduatedColourPanel.add(colourRangePanel);
+
+    minColour.setFont(JvSwingUtils.getLabelFont());
+    minColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    minColour.setPreferredSize(new Dimension(40, 20));
+    minColour.setToolTipText(MessageManager.getString("label.min_colour"));
+    minColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (minColour.isEnabled())
+        {
+          showColourChooser(minColour, "label.select_colour_minimum_value");
+        }
+      }
+    });
+
+    maxColour.setFont(JvSwingUtils.getLabelFont());
+    maxColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    maxColour.setPreferredSize(new Dimension(40, 20));
+    maxColour.setToolTipText(MessageManager.getString("label.max_colour"));
+    maxColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (maxColour.isEnabled())
+        {
+          showColourChooser(maxColour, "label.select_colour_maximum_value");
+        }
+      }
+    });
+    maxColour.setBorder(new LineBorder(Color.black));
+
+    /*
+     * default max colour to current colour (if a plain colour),
+     * or to Black if colour by label;  make min colour a pale
+     * version of max colour
+     */
+    FeatureColourI fc = fr.getFeatureColours().get(featureType);
+    Color bg = fc.isSimpleColour() ? fc.getColour() : Color.BLACK;
+    maxColour.setBackground(bg);
+    minColour.setBackground(ColorUtils.bleachColour(bg, 0.9f));
+
+    noValueCombo = new JComboBox<>();
+    noValueCombo.addItem(MessageManager.getString("label.no_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.min_colour"));
+    noValueCombo.addItem(MessageManager.getString("label.max_colour"));
+    noValueCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+
+    JLabel minText = new JLabel(
+            MessageManager.getString("label.min_value") + COLON);
+    minText.setFont(JvSwingUtils.getLabelFont());
+    JLabel maxText = new JLabel(
+            MessageManager.getString("label.max_value") + COLON);
+    maxText.setFont(JvSwingUtils.getLabelFont());
+    JLabel noText = new JLabel(
+            MessageManager.getString("label.no_value") + COLON);
+    noText.setFont(JvSwingUtils.getLabelFont());
+
+    colourRangePanel.add(minText);
+    colourRangePanel.add(minColour);
+    colourRangePanel.add(maxText);
+    colourRangePanel.add(maxColour);
+    colourRangePanel.add(noText);
+    colourRangePanel.add(noValueCombo);
+
+    /*
+     * third row - threshold options and value
+     */
+    JPanel thresholdPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    thresholdPanel.setBackground(Color.white);
+    graduatedColourPanel.add(thresholdPanel);
+
+    threshold.addActionListener(changeColourAction);
+    threshold.setToolTipText(MessageManager
+            .getString("label.threshold_feature_display_by_score"));
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_no_threshold")); // index 0
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_above_threshold")); // index 1
+    threshold.addItem(MessageManager
+            .getString("label.threshold_feature_below_threshold")); // index 2
+
+    thresholdValue.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+    thresholdValue.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        thresholdValue_actionPerformed();
+      }
+    });
+    slider.setPaintLabels(false);
+    slider.setPaintTicks(true);
+    slider.setBackground(Color.white);
+    slider.setEnabled(false);
+    slider.setOpaque(false);
+    slider.setPreferredSize(new Dimension(100, 32));
+    slider.setToolTipText(
+            MessageManager.getString("label.adjust_threshold"));
+
+    slider.addChangeListener(new ChangeListener()
+    {
+      @Override
+      public void stateChanged(ChangeEvent evt)
+      {
+        if (!adjusting)
+        {
+          thresholdValue
+                  .setText(String.valueOf(slider.getValue() / scaleFactor));
+          sliderValueChanged();
+        }
+      }
+    });
+    slider.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mouseReleased(MouseEvent evt)
+      {
+        /*
+         * only update Overview and/or structure colouring
+         * when threshold slider drag ends (mouse up)
+         */
+        if (ap != null)
+        {
+          ap.paintAlignment(true, true);
+        }
+      }
+    });
+
+    thresholdValue.setEnabled(false);
+    thresholdValue.setColumns(7);
+
+    thresholdPanel.add(threshold);
+    thresholdPanel.add(slider);
+    thresholdPanel.add(thresholdValue);
+
+    thresholdIsMin.setBackground(Color.white);
+    thresholdIsMin
+            .setText(MessageManager.getString("label.threshold_minmax"));
+    thresholdIsMin.setToolTipText(MessageManager
+            .getString("label.toggle_absolute_relative_display_threshold"));
+    thresholdIsMin.addActionListener(changeColourAction);
+    thresholdPanel.add(thresholdIsMin);
+
+    return graduatedColourPanel;
+  }
+
+  /**
+   * Lay out OK and Cancel buttons
+   * 
+   * @return
+   */
+  private JPanel initialiseOkCancelPanel()
+  {
+    JPanel okCancelPanel = new JPanel();
+    // okCancelPanel.setBackground(Color.white);
+    okCancelPanel.add(ok);
+    okCancelPanel.add(cancel);
+    return okCancelPanel;
+  }
+
+  /**
+   * Lay out Colour options panel, containing
+   * <ul>
+   * <li>plain colour, with colour picker</li>
+   * <li>colour by text, with choice of Label or other attribute</li>
+   * <li>colour by range, of score or other attribute, when available</li>
+   * </ul>
+   * 
+   * @return
+   */
+  private JPanel initialiseColoursPanel()
+  {
+    JPanel colourByPanel = new JPanel();
+    colourByPanel.setLayout(new BoxLayout(colourByPanel, BoxLayout.Y_AXIS));
+
+    /*
+     * simple colour radio button and colour picker
+     */
+    JPanel simpleColourPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    simpleColourPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(simpleColourPanel,
+            MessageManager.getString("label.simple"), true);
+    colourByPanel.add(simpleColourPanel);
+
+    simpleColour = new JRadioButton(
+            MessageManager.getString("label.simple_colour"));
+    simpleColour.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    simpleColour.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (simpleColour.isSelected() && !adjusting)
+        {
+          showColourChooser(singleColour, "label.select_colour");
+        }
+      }
+
+    });
+    
+    singleColour.setFont(JvSwingUtils.getLabelFont());
+    singleColour.setBorder(BorderFactory.createLineBorder(Color.black));
+    singleColour.setPreferredSize(new Dimension(40, 20));
+    singleColour.addMouseListener(new MouseAdapter()
+    {
+      @Override
+      public void mousePressed(MouseEvent e)
+      {
+        if (simpleColour.isSelected())
+        {
+          showColourChooser(singleColour, "label.select_colour");
+        }
+      }
+    });
+    simpleColourPanel.add(simpleColour); // radio button
+    simpleColourPanel.add(singleColour); // colour picker button
+
+    /*
+     * colour by text (category) radio button and drop-down choice list
+     */
+    JPanel byTextPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    byTextPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(byTextPanel,
+            MessageManager.getString("label.colour_by_text"), true);
+    colourByPanel.add(byTextPanel);
+    byCategory = new JRadioButton(
+            MessageManager.getString("label.by_text_of") + COLON);
+    byCategory.setPreferredSize(new Dimension(RADIO_WIDTH, 20));
+    byCategory.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        if (byCategory.isSelected())
+        {
+          colourChanged(true);
+        }
+      }
+    });
+    byTextPanel.add(byCategory);
+
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+    colourByTextCombo = populateAttributesDropdown(attNames, false, true);
+    colourByTextCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        colourChanged(true);
+      }
+    });
+    byTextPanel.add(colourByTextCombo);
+
+    /*
+     * graduated colour panel
+     */
+    JPanel graduatedColourPanel = initialiseGraduatedColourPanel();
+    colourByPanel.add(graduatedColourPanel);
+
+    /*
+     * 3 radio buttons select between simple colour, 
+     * by category (text), or graduated
+     */
+    ButtonGroup bg = new ButtonGroup();
+    bg.add(simpleColour);
+    bg.add(byCategory);
+    bg.add(graduatedColour);
+
+    return colourByPanel;
+  }
+
+  private void showColourChooser(JPanel colourPanel, String key)
+  {
+    Color col = JColorChooser.showDialog(this,
+            MessageManager.getString(key), colourPanel.getBackground());
+    if (col != null)
+    {
+      colourPanel.setBackground(col);
+      colourPanel.setForeground(col);
+    }
+    colourPanel.repaint();
+    colourChanged(true);
+  }
+
+  /**
+   * Constructs and sets the selected colour options as the colour for the feature
+   * type, and repaints the alignment, and optionally the Overview and/or
+   * structure viewer if open
+   * 
+   * @param updateStructsAndOverview
+   */
+  void colourChanged(boolean updateStructsAndOverview)
+  {
+    if (adjusting)
+    {
+      /*
+       * ignore action handlers while setting values programmatically
+       */
+      return;
+    }
+
+    /*
+     * ensure min-max range is for the latest choice of 
+     * 'graduated colour by'
+     */
+    updateColourMinMax();
+
+    FeatureColourI acg = makeColourFromInputs();
+
+    /*
+     * save the colour, and repaint stuff
+     */
+    fr.setColour(featureType, acg);
+    ap.paintAlignment(updateStructsAndOverview, updateStructsAndOverview);
+
+    updateColoursTab();
+  }
+
+  /**
+   * Converts the input values into an instance of FeatureColour
+   * 
+   * @return
+   */
+  private FeatureColourI makeColourFromInputs()
+  {
+    /*
+     * easiest case - a single colour
+     */
+    if (simpleColour.isSelected())
+    {
+      return new FeatureColour(singleColour.getBackground());
+    }
+
+    /*
+     * next easiest case - colour by Label, or attribute text
+     */
+    if (byCategory.isSelected())
+    {
+      Color c = this.getBackground();
+      FeatureColourI fc = new FeatureColour(c, c, null, 0f, 0f);
+      fc.setColourByLabel(true);
+      String byWhat = (String) colourByTextCombo.getSelectedItem();
+      if (!LABEL_18N.equals(byWhat))
+      {
+        fc.setAttributeName(
+                FeatureMatcher.fromAttributeDisplayName(byWhat));
+      }
+      return fc;
+    }
+
+    /*
+     * remaining case - graduated colour by score, or attribute value
+     */
+    Color noColour = null;
+    if (noValueCombo.getSelectedIndex() == MIN_COLOUR_OPTION)
+    {
+      noColour = minColour.getBackground();
+    }
+    else if (noValueCombo.getSelectedIndex() == MAX_COLOUR_OPTION)
+    {
+      noColour = maxColour.getBackground();
+    }
+
+    float thresh = 0f;
+    try
+    {
+      thresh = Float.valueOf(thresholdValue.getText());
+    } catch (NumberFormatException e)
+    {
+      // invalid inputs are already handled on entry
+    }
+
+    /*
+     * min-max range is to (or from) threshold value if 
+     * 'threshold is min/max' is selected 
+     */
+    float minValue = min;
+    float maxValue = max;
+    final int thresholdOption = threshold.getSelectedIndex();
+    if (thresholdIsMin.isSelected()
+            && thresholdOption == ABOVE_THRESHOLD_OPTION)
+    {
+      minValue = thresh;
+    }
+    if (thresholdIsMin.isSelected()
+            && thresholdOption == BELOW_THRESHOLD_OPTION)
+    {
+      maxValue = thresh;
+    }
+
+    /*
+     * make the graduated colour
+     */
+    FeatureColourI fc = new FeatureColour(minColour.getBackground(),
+            maxColour.getBackground(), noColour, minValue, maxValue);
+
+    /*
+     * set attribute to colour by if selected
+     */
+    String byWhat = (String) colourByRangeCombo.getSelectedItem();
+    if (!SCORE_18N.equals(byWhat))
+    {
+      fc.setAttributeName(FeatureMatcher.fromAttributeDisplayName(byWhat));
+    }
+
+    /*
+     * set threshold options and 'autoscaled' which is
+     * false if 'threshold is min/max' is selected
+     * else true (colour range is on actual range of values)
+     */
+    fc.setThreshold(thresh);
+    fc.setAutoScaled(!thresholdIsMin.isSelected());
+    fc.setAboveThreshold(thresholdOption == ABOVE_THRESHOLD_OPTION);
+    fc.setBelowThreshold(thresholdOption == BELOW_THRESHOLD_OPTION);
+
+    if (threshline == null)
+    {
+      /*
+       * todo not yet implemented: visual indication of feature threshold
+       */
+      threshline = new GraphLine((max - min) / 2f, "Threshold",
+              Color.black);
+    }
+
+    return fc;
+  }
+
+  @Override
+  protected void raiseClosed()
+  {
+    if (this.featureSettings != null)
+    {
+      featureSettings.actionPerformed(new ActionEvent(this, 0, "CLOSED"));
+    }
+  }
+
+  /**
+   * Action on OK is just to dismiss the dialog - any changes have already been
+   * applied
+   */
+  @Override
+  public void okPressed()
+  {
+  }
+
+  /**
+   * Action on Cancel is to restore colour scheme and filters as they were when
+   * the dialog was opened
+   */
+  @Override
+  public void cancelPressed()
+  {
+    fr.setColour(featureType, originalColour);
+    fr.setFeatureFilter(featureType, originalFilter);
+    ap.paintAlignment(true, true);
+  }
+
+  /**
+   * Action on text entry of a threshold value
+   */
+  protected void thresholdValue_actionPerformed()
+  {
+    try
+    {
+      adjusting = true;
+      float f = Float.parseFloat(thresholdValue.getText());
+      slider.setValue((int) (f * scaleFactor));
+      threshline.value = f;
+      thresholdValue.setBackground(Color.white); // ok
+
+      /*
+       * force repaint of any Overview window or structure
+       */
+      ap.paintAlignment(true, true);
+    } catch (NumberFormatException ex)
+    {
+      thresholdValue.setBackground(Color.red); // not ok
+    } finally
+    {
+      adjusting = false;
+    }
+  }
+
+  /**
+   * Action on change of threshold slider value. This may be done interactively
+   * (by moving the slider), or programmatically (to update the slider after
+   * manual input of a threshold value).
+   */
+  protected void sliderValueChanged()
+  {
+    threshline.value = getRoundedSliderValue();
+
+    /*
+     * repaint alignment, but not Overview or structure,
+     * to avoid overload while dragging the slider
+     */
+    colourChanged(false);
+  }
+
+  /**
+   * Converts the slider value to its absolute value by dividing by the
+   * scaleFactor. Rounding errors are squashed by forcing min/max of slider range
+   * to the actual min/max of feature score range
+   * 
+   * @return
+   */
+  private float getRoundedSliderValue()
+  {
+    int value = slider.getValue();
+    float f = value == slider.getMaximum() ? max
+            : (value == slider.getMinimum() ? min : value / scaleFactor);
+    return f;
+  }
+
+  void addActionListener(ActionListener listener)
+  {
+    if (featureSettings != null)
+    {
+      System.err.println(
+              "IMPLEMENTATION ISSUE: overwriting action listener for FeatureColourChooser");
+    }
+    featureSettings = listener;
+  }
+
+  /**
+   * A helper method to build the drop-down choice of attributes for a feature. If
+   * 'withRange' is true, then Score, and any attributes with a min-max range, are
+   * added. If 'withText' is true, Label and any known attributes are added. This
+   * allows 'categorical numerical' attributes e.g. codon position to be coloured
+   * by text.
+   * <p>
+   * Where metadata is available with a description for an attribute, that is
+   * added as a tooltip.
+   * <p>
+   * Attribute names may be 'simple' e.g. "AC" or 'compound' e.g. {"CSQ",
+   * "Allele"}. Compound names are rendered for display as (e.g.) CSQ:Allele.
+   * <p>
+   * This method does not add any ActionListener to the JComboBox.
+   * 
+   * @param attNames
+   * @param withRange
+   * @param withText
+   */
+  protected JComboBox<String> populateAttributesDropdown(
+          List<String[]> attNames, boolean withRange, boolean withText)
+  {
+    List<String> displayAtts = new ArrayList<>();
+    List<String> tooltips = new ArrayList<>();
+
+    if (withText)
+    {
+      displayAtts.add(LABEL_18N);
+      tooltips.add(MessageManager.getString("label.description"));
+    }
+    if (withRange)
+    {
+      float[][] minMax = fr.getMinMax().get(featureType);
+      if (minMax != null && minMax[0][0] != minMax[0][1])
+      {
+        displayAtts.add(SCORE_18N);
+        tooltips.add(SCORE_18N);
+      }
+    }
+
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    for (String[] attName : attNames)
+    {
+      float[] minMax = fa.getMinMax(featureType, attName);
+      boolean hasRange = minMax != null && minMax[0] != minMax[1];
+      if (!withText && !hasRange)
+      {
+        continue;
+      }
+      displayAtts.add(FeatureMatcher.toAttributeDisplayName(attName));
+      String desc = fa.getDescription(featureType, attName);
+      if (desc != null && desc.length() > MAX_TOOLTIP_LENGTH)
+      {
+        desc = desc.substring(0, MAX_TOOLTIP_LENGTH) + "...";
+      }
+      tooltips.add(desc == null ? "" : desc);
+    }
+
+    JComboBox<String> attCombo = JvSwingUtils
+            .buildComboWithTooltips(displayAtts, tooltips);
+
+    return attCombo;
+  }
+
+  /**
+   * Populates initial layout of the feature attribute filters panel
+   */
+  private JPanel initialiseFiltersPanel()
+  {
+    filters = new ArrayList<>();
+
+    JPanel filtersPanel = new JPanel();
+    filtersPanel.setLayout(new BoxLayout(filtersPanel, BoxLayout.Y_AXIS));
+    filtersPanel.setBackground(Color.white);
+    JvSwingUtils.createTitledBorder(filtersPanel,
+            MessageManager.getString("label.filters"), true);
+
+    JPanel andOrPanel = initialiseAndOrPanel();
+    filtersPanel.add(andOrPanel);
+
+    /*
+     * panel with filters - populated by refreshFiltersDisplay, 
+     * which also sets the layout manager
+     */
+    chooseFiltersPanel = new JPanel();
+    chooseFiltersPanel.setBackground(Color.white);
+    filtersPanel.add(chooseFiltersPanel);
+
+    return filtersPanel;
+  }
+
+  /**
+   * Lays out the panel with radio buttons to AND or OR filter conditions
+   * 
+   * @return
+   */
+  private JPanel initialiseAndOrPanel()
+  {
+    JPanel andOrPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    andOrPanel.setBackground(Color.white);
+    andOrPanel.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+    andFilters = new JRadioButton(MessageManager.getString("label.and"));
+    orFilters = new JRadioButton(MessageManager.getString("label.or"));
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        filtersChanged();
+      }
+    };
+    andFilters.addActionListener(actionListener);
+    orFilters.addActionListener(actionListener);
+    ButtonGroup andOr = new ButtonGroup();
+    andOr.add(andFilters);
+    andOr.add(orFilters);
+    andFilters.setSelected(true);
+    andOrPanel.add(
+            new JLabel(MessageManager.getString("label.join_conditions")));
+    andOrPanel.add(andFilters);
+    andOrPanel.add(orFilters);
+    return andOrPanel;
+  }
+
+  /**
+   * Refreshes the display to show any filters currently configured for the
+   * selected feature type (editable, with 'remove' option), plus one extra row
+   * for adding a condition. This should be called after a filter has been
+   * removed, added or amended.
+   */
+  private void updateFiltersTab()
+  {
+    /*
+     * clear the panel and list of filter conditions
+     */
+    chooseFiltersPanel.removeAll();
+    filters.clear();
+
+    /*
+     * look up attributes known for feature type
+     */
+    List<String[]> attNames = FeatureAttributes.getInstance()
+            .getAttributes(featureType);
+
+    /*
+     * if this feature type has filters set, load them first
+     */
+    FeatureMatcherSetI featureFilters = fr.getFeatureFilter(featureType);
+    if (featureFilters != null)
+    {
+      if (!featureFilters.isAnded())
+      {
+        orFilters.setSelected(true);
+      }
+      featureFilters.getMatchers().forEach(matcher -> filters.add(matcher));
+    }
+
+    /*
+     * and an empty filter for the user to populate (add)
+     */
+    filters.add(FeatureMatcher.NULL_MATCHER);
+
+    /*
+     * use GridLayout to 'justify' rows to the top of the panel, until
+     * there are too many to fit in, then fall back on BoxLayout
+     */
+    if (filters.size() <= 5)
+    {
+      chooseFiltersPanel.setLayout(new GridLayout(5, 1));
+    }
+    else
+    {
+      chooseFiltersPanel.setLayout(
+              new BoxLayout(chooseFiltersPanel, BoxLayout.Y_AXIS));
+    }
+
+    /*
+     * render the conditions in rows, each in its own JPanel
+     */
+    int filterIndex = 0;
+    for (FeatureMatcherI filter : filters)
+    {
+      JPanel row = addFilter(filter, attNames, filterIndex);
+      row.setBorder(BorderFactory.createLineBorder(debugBorderColour));
+      chooseFiltersPanel.add(row);
+      filterIndex++;
+    }
+
+    this.validate();
+    this.repaint();
+  }
+
+  /**
+   * A helper method that constructs a row (panel) with one filter condition:
+   * <ul>
+   * <li>a drop-down list of Label, Score and attribute names to choose from</li>
+   * <li>a drop-down list of conditions to choose from</li>
+   * <li>a text field for input of a match pattern</li>
+   * <li>optionally, a 'remove' button</li>
+   * </ul>
+   * The filter values are set as defaults for the input fields. The 'remove'
+   * button is added unless the pattern is empty (incomplete filter condition).
+   * <p>
+   * Action handlers on these fields provide for
+   * <ul>
+   * <li>validate pattern field - should be numeric if condition is numeric</li>
+   * <li>save filters and refresh display on any (valid) change</li>
+   * <li>remove filter and refresh on 'Remove'</li>
+   * <li>update conditions list on change of Label/Score/Attribute</li>
+   * <li>refresh value field tooltip with min-max range on change of
+   * attribute</li>
+   * </ul>
+   * 
+   * @param filter
+   * @param attNames
+   * @param filterIndex
+   * @return
+   */
+  protected JPanel addFilter(FeatureMatcherI filter,
+          List<String[]> attNames, int filterIndex)
+  {
+    String[] attName = filter.getAttribute();
+    Condition cond = filter.getMatcher().getCondition();
+    String pattern = filter.getMatcher().getPattern();
+
+    JPanel filterRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
+    filterRow.setBackground(Color.white);
+
+    /*
+     * drop-down choice of attribute, with description as a tooltip 
+     * if we can obtain it
+     */
+    final JComboBox<String> attCombo = populateAttributesDropdown(attNames,
+            true, true);
+    String filterBy = setSelectedAttribute(attCombo, filter);
+
+    JComboBox<Condition> condCombo = new JComboBox<>();
+
+    JTextField patternField = new JTextField(8);
+    patternField.setText(pattern);
+
+    /*
+     * action handlers that validate and (if valid) apply changes
+     */
+    ActionListener actionListener = new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        if (validateFilter(patternField, condCombo))
+        {
+          if (updateFilter(attCombo, condCombo, patternField, filterIndex))
+          {
+            filtersChanged();
+          }
+        }
+      }
+    };
+    ItemListener itemListener = new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    };
+
+    if (filter == FeatureMatcher.NULL_MATCHER) // the 'add a condition' row
+    {
+      attCombo.setSelectedIndex(0);
+    }
+    else
+    {
+      attCombo.setSelectedItem(
+              FeatureMatcher.toAttributeDisplayName(attName));
+    }
+    attCombo.addItemListener(new ItemListener()
+    {
+      @Override
+      public void itemStateChanged(ItemEvent e)
+      {
+        /*
+         * on change of attribute, refresh the conditions list to
+         * ensure it is appropriate for the attribute datatype
+         */
+        populateConditions((String) attCombo.getSelectedItem(),
+                (Condition) condCombo.getSelectedItem(), condCombo,
+                patternField);
+        actionListener.actionPerformed(null);
+      }
+    });
+
+    filterRow.add(attCombo);
+
+    /*
+     * drop-down choice of test condition
+     */
+    populateConditions(filterBy, cond, condCombo, patternField);
+    condCombo.setPreferredSize(new Dimension(150, 20));
+    condCombo.addItemListener(itemListener);
+    filterRow.add(condCombo);
+
+    /*
+     * pattern to match against
+     */
+    patternField.addActionListener(actionListener);
+    patternField.addFocusListener(new FocusAdapter()
+    {
+      @Override
+      public void focusLost(FocusEvent e)
+      {
+        actionListener.actionPerformed(null);
+      }
+    });
+    filterRow.add(patternField);
+
+    /*
+     * disable pattern field for condition 'Present / NotPresent'
+     */
+    Condition selectedCondition = (Condition) condCombo.getSelectedItem();
+    patternField.setEnabled(selectedCondition.needsAPattern());
+
+    /*
+     * if a numeric condition is selected, show the value range
+     * as a tooltip on the value input field
+     */
+    setPatternTooltip(filterBy, selectedCondition, patternField);
+
+    /*
+     * add remove button if filter is populated (non-empty pattern)
+     */
+    if (!patternField.isEnabled()
+            || (pattern != null && pattern.trim().length() > 0))
+    {
+      // todo: gif for button drawing '-' or 'x'
+      JButton removeCondition = new BasicArrowButton(SwingConstants.WEST);
+      removeCondition
+              .setToolTipText(MessageManager.getString("label.delete_row"));
+      removeCondition.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          filters.remove(filterIndex);
+          filtersChanged();
+        }
+      });
+      filterRow.add(removeCondition);
+    }
+
+    return filterRow;
+  }
+
+  /**
+   * Sets the selected item in the Label/Score/Attribute drop-down to match the
+   * filter
+   * 
+   * @param attCombo
+   * @param filter
+   */
+  private String setSelectedAttribute(JComboBox<String> attCombo,
+          FeatureMatcherI filter)
+  {
+    String item = null;
+    if (filter.isByScore())
+    {
+      item = SCORE_18N;
+    }
+    else if (filter.isByLabel())
+    {
+      item = LABEL_18N;
+    }
+    else
+    {
+      item = FeatureMatcher.toAttributeDisplayName(filter.getAttribute());
+    }
+    attCombo.setSelectedItem(item);
+    return item;
+  }
+
+  /**
+   * If a numeric comparison condition is selected, retrieve the min-max range for
+   * the value (score or attribute), and set it as a tooltip on the value file
+   * 
+   * @param attName
+   * @param selectedCondition
+   * @param patternField
+   */
+  private void setPatternTooltip(String attName,
+          Condition selectedCondition, JTextField patternField)
+  {
+    patternField.setToolTipText("");
+
+    if (selectedCondition.isNumeric())
+    {
+      float[] minMax = getMinMax(attName);
+      if (minMax != null)
+      {
+        String tip = String.format("(%s - %s)",
+                DECFMT_2_2.format(minMax[0]), DECFMT_2_2.format(minMax[1]));
+        patternField.setToolTipText(tip);
+      }
+    }
+  }
+
+  /**
+   * Populates the drop-down list of comparison conditions for the given attribute
+   * name. The conditions added depend on the datatype of the attribute values.
+   * The supplied condition is set as the selected item in the list, provided it
+   * is in the list. If the pattern is now invalid (non-numeric pattern for a
+   * numeric condition), it is cleared.
+   * 
+   * @param attName
+   * @param cond
+   * @param condCombo
+   * @param patternField
+   */
+  private void populateConditions(String attName, Condition cond,
+          JComboBox<Condition> condCombo, JTextField patternField)
+  {
+    Datatype type = FeatureAttributes.getInstance().getDatatype(featureType,
+            FeatureMatcher.fromAttributeDisplayName(attName));
+    if (LABEL_18N.equals(attName))
+    {
+      type = Datatype.Character;
+    }
+    else if (SCORE_18N.equals(attName))
+    {
+      type = Datatype.Number;
+    }
+
+    /*
+     * remove itemListener before starting
+     */
+    ItemListener listener = condCombo.getItemListeners()[0];
+    condCombo.removeItemListener(listener);
+    boolean condIsValid = false;
+    condCombo.removeAllItems();
+    for (Condition c : Condition.values())
+    {
+      if ((c.isNumeric() && type != Datatype.Character)
+              || (!c.isNumeric() && type != Datatype.Number))
+      {
+        condCombo.addItem(c);
+        if (c == cond)
+        {
+          condIsValid = true;
+        }
+      }
+    }
+
+    /*
+     * set the selected condition (does nothing if not in the list)
+     */
+    if (condIsValid)
+    {
+      condCombo.setSelectedItem(cond);
+    }
+    else
+    {
+      condCombo.setSelectedIndex(0);
+    }
+
+    condCombo.addItemListener(listener);
+
+    /*
+     * clear pattern if it is now invalid for condition
+     */
+    if (((Condition) condCombo.getSelectedItem()).isNumeric())
+    {
+      try
+      {
+        String pattern = patternField.getText().trim();
+        if (pattern.length() > 0)
+        {
+          Float.valueOf(pattern);
+        }
+      } catch (NumberFormatException e)
+      {
+        patternField.setText("");
+      }
+    }
+  }
+
+  /**
+   * Answers true unless a numeric condition has been selected with a non-numeric
+   * value. Sets the value field to RED with a tooltip if in error.
+   * <p>
+   * If the pattern is expected but is empty, this method returns false, but does
+   * not mark the field as invalid. This supports selecting an attribute for a new
+   * condition before a match pattern has been entered.
+   * 
+   * @param value
+   * @param condCombo
+   */
+  protected boolean validateFilter(JTextField value,
+          JComboBox<Condition> condCombo)
+  {
+    if (value == null || condCombo == null)
+    {
+      return true; // fields not populated
+    }
+
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    if (!cond.needsAPattern())
+    {
+      return true;
+    }
+
+    value.setBackground(Color.white);
+    value.setToolTipText("");
+    String v1 = value.getText().trim();
+    if (v1.length() == 0)
+    {
+      // return false;
+    }
+
+    if (cond.isNumeric() && v1.length() > 0)
+    {
+      try
+      {
+        Float.valueOf(v1);
+      } catch (NumberFormatException e)
+      {
+        value.setBackground(Color.red);
+        value.setToolTipText(
+                MessageManager.getString("label.numeric_required"));
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Constructs a filter condition from the given input fields, and replaces the
+   * condition at filterIndex with the new one. Does nothing if the pattern field
+   * is blank (unless the match condition is one that doesn't require a pattern,
+   * e.g. 'Is present'). Answers true if the filter was updated, else false.
+   * <p>
+   * This method may update the tooltip on the filter value field to show the
+   * value range, if a numeric condition is selected. This ensures the tooltip is
+   * updated when a numeric valued attribute is chosen on the last 'add a filter'
+   * row.
+   * 
+   * @param attCombo
+   * @param condCombo
+   * @param valueField
+   * @param filterIndex
+   */
+  protected boolean updateFilter(JComboBox<String> attCombo,
+          JComboBox<Condition> condCombo, JTextField valueField,
+          int filterIndex)
+  {
+    String attName = (String) attCombo.getSelectedItem();
+    Condition cond = (Condition) condCombo.getSelectedItem();
+    String pattern = valueField.getText().trim();
+
+    setPatternTooltip(attName, cond, valueField);
+
+    if (pattern.length() == 0 && cond.needsAPattern())
+    {
+      valueField.setEnabled(true); // ensure pattern field is enabled!
+      return false;
+    }
+
+    /*
+     * Construct a matcher that operates on Label, Score, 
+     * or named attribute
+     */
+    FeatureMatcherI km = null;
+    if (LABEL_18N.equals(attName))
+    {
+      km = FeatureMatcher.byLabel(cond, pattern);
+    }
+    else if (SCORE_18N.equals(attName))
+    {
+      km = FeatureMatcher.byScore(cond, pattern);
+    }
+    else
+    {
+      km = FeatureMatcher.byAttribute(cond, pattern,
+              FeatureMatcher.fromAttributeDisplayName(attName));
+    }
+
+    filters.set(filterIndex, km);
+
+    return true;
+  }
+
+  /**
+   * Makes the dialog visible, at the Feature Colour tab or at the Filters tab
+   * 
+   * @param coloursTab
+   */
+  public void showTab(boolean coloursTab)
+  {
+    setVisible(true);
+    tabbedPane.setSelectedIndex(coloursTab ? 0 : 1);
+  }
+
+  /**
+   * Action on any change to feature filtering, namely
+   * <ul>
+   * <li>change of selected attribute</li>
+   * <li>change of selected condition</li>
+   * <li>change of match pattern</li>
+   * <li>removal of a condition</li>
+   * </ul>
+   * The inputs are parsed into a combined filter and this is set for the feature
+   * type, and the alignment redrawn.
+   */
+  protected void filtersChanged()
+  {
+    /*
+     * update the filter conditions for the feature type
+     */
+    boolean anded = andFilters.isSelected();
+    FeatureMatcherSetI combined = new FeatureMatcherSet();
+
+    for (FeatureMatcherI filter : filters)
+    {
+      String pattern = filter.getMatcher().getPattern();
+      Condition condition = filter.getMatcher().getCondition();
+      if (pattern.trim().length() > 0 || !condition.needsAPattern())
+      {
+        if (anded)
+        {
+          combined.and(filter);
+        }
+        else
+        {
+          combined.or(filter);
+        }
+      }
+    }
+
+    /*
+     * save the filter conditions in the FeatureRenderer
+     * (note this might now be an empty filter with no conditions)
+     */
+    fr.setFeatureFilter(featureType, combined.isEmpty() ? null : combined);
+    ap.paintAlignment(true, true);
+
+    updateFiltersTab();
+  }
+}
index 6cddcca..f3c8e8f 100755 (executable)
@@ -183,7 +183,7 @@ public class FontChooser extends GFontChooser
   {
     ap.av.antiAlias = smoothFont.isSelected();
     ap.getAnnotationPanel().image = null;
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, false);
     if (ap.av.getCodingComplement() != null && ap.av.isProteinFontAsCdna())
     {
       ((AlignViewport) ap.av
@@ -235,7 +235,7 @@ public class FontChooser extends GFontChooser
       ap.av.setScaleProteinAsCdna(oldProteinScale);
       ap.av.setProteinFontAsCdna(oldMirrorFont);
       ap.av.antiAlias = oldSmoothFont;
-      ap.paintAlignment(true);
+      ap.fontChanged();
 
       if (scaleAsCdna.isVisible() && scaleAsCdna.isEnabled())
       {
index 981e94c..35bd871 100644 (file)
@@ -34,7 +34,8 @@ public interface IProgressIndicator
    * is removed with a second call with same ID.
    * 
    * @param message
-   *          - displayed message for operation
+   *          - displayed message for operation. Please ensure message is
+   *          internationalised.
    * @param id
    *          - unique handle for this indicator
    */
index a7dff86..cd7b0b7 100755 (executable)
@@ -83,7 +83,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     this.av = av;
     PaintRefresher.Register(this, av.getSequenceSetId());
     av.getRanges().addPropertyChangeListener(this);
-  }
+    }
 
   /**
    * DOCUMENT ME!
@@ -204,7 +204,11 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     gg.translate(0, -transY);
 
     fastPaint = true;
-    repaint();
+
+    // Call repaint on alignment panel so that repaints from other alignment
+    // panel components can be aggregated. Otherwise performance of the overview
+    // window and others may be adversely affected.
+    av.getAlignPanel().repaint();
   }
 
   /**
@@ -216,41 +220,43 @@ public class IdCanvas extends JPanel implements ViewportListenerI
   @Override
   public void paintComponent(Graphics g)
   {
+    super.paintComponent(g);
+
     g.setColor(Color.white);
     g.fillRect(0, 0, getWidth(), getHeight());
-
+    
     if (fastPaint)
     {
       fastPaint = false;
       g.drawImage(image, 0, 0, this);
-
+    
       return;
     }
-
+    
     int oldHeight = imgHeight;
-
+    
     imgHeight = getHeight();
     imgHeight -= (imgHeight % av.getCharHeight());
-
+    
     if (imgHeight < 1)
     {
       return;
     }
-
+    
     if (oldHeight != imgHeight || image.getWidth(this) != getWidth())
     {
-      image = new BufferedImage(getWidth(), imgHeight,
-              BufferedImage.TYPE_INT_RGB);
+       image = new BufferedImage(getWidth(), imgHeight,
+                BufferedImage.TYPE_INT_RGB);
     }
-
+    
     gg = (Graphics2D) image.getGraphics();
-
+    
     // Fill in the background
     gg.setColor(Color.white);
     gg.fillRect(0, 0, getWidth(), imgHeight);
-
+    
     drawIds(av.getRanges().getStartSeq(), av.getRanges().getEndSeq());
-
+    
     g.drawImage(image, 0, 0, this);
   }
 
@@ -374,7 +380,7 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+              .absoluteToVisibleColumn(maxwidth) - 1;
     }
 
     int annotationHeight = 0;
@@ -564,5 +570,14 @@ public class IdCanvas extends JPanel implements ViewportListenerI
     {
       fastPaint((int) evt.getNewValue() - (int) evt.getOldValue());
     }
+    else if (propertyName.equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      fastPaint(((int[]) evt.getNewValue())[1]
+              - ((int[]) evt.getOldValue())[1]);
+    }
+    else if (propertyName.equals(ViewportRanges.MOVE_VIEWPORT))
+    {
+      repaint();
+    }
   }
 }
index 3cc0ed3..a183144 100755 (executable)
@@ -108,8 +108,7 @@ public class IdPanel extends JPanel
       SequenceI sequence = av.getAlignment().getSequenceAt(seq);
       StringBuilder tip = new StringBuilder(64);
       seqAnnotReport.createTooltipAnnotationReport(tip, sequence,
-              av.isShowDBRefs(), av.isShowNPFeats(),
-              sp.seqCanvas.fr.getMinMax());
+              av.isShowDBRefs(), av.isShowNPFeats(), sp.seqCanvas.fr);
       setToolTipText(JvSwingUtils.wrapTooltip(true,
               sequence.getDisplayId(true) + " " + tip.toString()));
     }
@@ -138,7 +137,7 @@ public class IdPanel extends JPanel
     }
 
     lastid = seq;
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -148,24 +147,25 @@ public class IdPanel extends JPanel
   public void mouseWheelMoved(MouseWheelEvent e)
   {
     e.consume();
-    if (e.getWheelRotation() > 0)
+    double wheelRotation = e.getPreciseWheelRotation();
+    if (wheelRotation > 0)
     {
       if (e.isShiftDown())
       {
         av.getRanges().scrollRight(true);
       }
-      else if (!av.getWrapAlignment())
+      else
       {
         av.getRanges().scrollUp(false);
       }
     }
-    else
+    else if (wheelRotation < 0)
     {
       if (e.isShiftDown())
       {
         av.getRanges().scrollRight(false);
       }
-      else if (!av.getWrapAlignment())
+      else
       {
         av.getRanges().scrollUp(true);
       }
@@ -313,7 +313,7 @@ public class IdPanel extends JPanel
 
     av.isSelectionGroupChanged(true);
 
-    alignPanel.paintAlignment(false);
+    alignPanel.paintAlignment(false, false);
   }
 
   /**
@@ -331,7 +331,8 @@ public class IdPanel extends JPanel
      *  and any non-positional features
      */
     List<String> nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
-    for (SequenceFeature sf : sq.getFeatures().getNonPositionalFeatures())
+    List<SequenceFeature> features = sq.getFeatures().getNonPositionalFeatures();
+    for (SequenceFeature sf : features)
     {
       if (sf.links != null)
       {
@@ -342,7 +343,7 @@ public class IdPanel extends JPanel
       }
     }
 
-    PopupMenu pop = new PopupMenu(alignPanel, sq, nlinks,
+    PopupMenu pop = new PopupMenu(alignPanel, sq, features,
             Preferences.getGroupURLLinks());
     pop.show(this, e.getX(), e.getY());
   }
@@ -507,7 +508,7 @@ public class IdPanel extends JPanel
           running = false;
         }
 
-        alignPanel.paintAlignment(false);
+        alignPanel.paintAlignment(false, false);
 
         try
         {
index 3c4107f..0cffc3b 100755 (executable)
@@ -23,8 +23,8 @@ package jalview.gui;
 import jalview.api.AlignViewportI;
 
 import java.awt.Color;
+import java.awt.Cursor;
 import java.awt.Graphics;
-import java.awt.Image;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
@@ -44,8 +44,6 @@ public class IdwidthAdjuster extends JPanel
 
   int oldX = 0;
 
-  Image image;
-
   AlignmentPanel ap;
 
   /**
@@ -57,14 +55,7 @@ public class IdwidthAdjuster extends JPanel
   public IdwidthAdjuster(AlignmentPanel ap)
   {
     this.ap = ap;
-
-    java.net.URL url = getClass().getResource("/images/idwidth.gif");
-
-    if (url != null)
-    {
-      image = java.awt.Toolkit.getDefaultToolkit().createImage(url);
-    }
-
+    setBackground(Color.white);
     addMouseListener(this);
     addMouseMotionListener(this);
   }
@@ -75,6 +66,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mousePressed(MouseEvent evt)
   {
     oldX = evt.getX();
@@ -86,6 +78,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseReleased(MouseEvent evt)
   {
     active = false;
@@ -112,6 +105,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseEntered(MouseEvent evt)
   {
     active = true;
@@ -124,6 +118,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseExited(MouseEvent evt)
   {
     active = false;
@@ -136,6 +131,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseDragged(MouseEvent evt)
   {
     active = true;
@@ -149,7 +145,7 @@ public class IdwidthAdjuster extends JPanel
     {
       viewport.setIdWidth(newWidth);
 
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, false);
     }
 
     oldX = evt.getX();
@@ -161,6 +157,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseMoved(MouseEvent evt)
   {
   }
@@ -171,6 +168,7 @@ public class IdwidthAdjuster extends JPanel
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
   }
@@ -181,6 +179,7 @@ public class IdwidthAdjuster extends JPanel
    * @param g
    *          DOCUMENT ME!
    */
+  @Override
   public void paintComponent(Graphics g)
   {
     g.setColor(Color.white);
@@ -188,10 +187,7 @@ public class IdwidthAdjuster extends JPanel
 
     if (active)
     {
-      if (image != null)
-      {
-        g.drawImage(image, getWidth() - 20, 2, this);
-      }
+        setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
     }
   }
 }
index 0a6b9d6..1018d6e 100644 (file)
@@ -101,10 +101,10 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
      * identical DB sources, and should be collapsed.
      */
     DefaultMutableTreeNode tn = null, root = new DefaultMutableTreeNode();
-    Hashtable<String, DefaultMutableTreeNode> source = new Hashtable<String, DefaultMutableTreeNode>();
+    Hashtable<String, DefaultMutableTreeNode> source = new Hashtable<>();
     sfetcher = sfetch;
     String dbs[] = sfetch.getSupportedDb();
-    Hashtable<String, String> ht = new Hashtable<String, String>();
+    Hashtable<String, String> ht = new Hashtable<>();
     for (int i = 0; i < dbs.length; i++)
     {
       tn = source.get(dbs[i]);
@@ -370,7 +370,7 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
 
     tsel = dbviews.getSelectionPaths();
     boolean forcedFirstChild = false;
-    List<DbSourceProxy> srcs = new ArrayList<DbSourceProxy>();
+    List<DbSourceProxy> srcs = new ArrayList<>();
     if (tsel != null)
     {
       for (TreePath tp : tsel)
@@ -489,7 +489,7 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
       return null;
     }
     StringBuffer sb = new StringBuffer();
-    HashSet<String> hs = new HashSet<String>();
+    HashSet<String> hs = new HashSet<>();
     for (DbSourceProxy dbs : getSelectedSources())
     {
       String tq = dbs.getTestQuery();
@@ -506,7 +506,7 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     return sb.toString();
   }
 
-  List<ActionListener> lstners = new Vector<ActionListener>();
+  List<ActionListener> lstners = new Vector<>();
 
   public void addActionListener(ActionListener actionListener)
   {
@@ -596,4 +596,11 @@ public class JDatabaseTree extends JalviewDialog implements KeyListener
     // TODO Auto-generated method stub
 
   }
+
+  @Override
+  public void setVisible(boolean arg0)
+  {
+    System.out.println("setVisible: " + arg0);
+    super.setVisible(arg0);
+  }
 }
index 1658f0f..9285754 100644 (file)
@@ -37,6 +37,10 @@ import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.datamodel.StructureViewerModel;
 import jalview.datamodel.StructureViewerModel.StructureData;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.ext.varna.RnaModel;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
@@ -48,6 +52,8 @@ import jalview.schemabinding.version2.Annotation;
 import jalview.schemabinding.version2.AnnotationColours;
 import jalview.schemabinding.version2.AnnotationElement;
 import jalview.schemabinding.version2.CalcIdParam;
+import jalview.schemabinding.version2.Colour;
+import jalview.schemabinding.version2.CompoundMatcher;
 import jalview.schemabinding.version2.DBRef;
 import jalview.schemabinding.version2.Features;
 import jalview.schemabinding.version2.Group;
@@ -60,6 +66,8 @@ import jalview.schemabinding.version2.MapListFrom;
 import jalview.schemabinding.version2.MapListTo;
 import jalview.schemabinding.version2.Mapping;
 import jalview.schemabinding.version2.MappingChoice;
+import jalview.schemabinding.version2.MatchCondition;
+import jalview.schemabinding.version2.MatcherSet;
 import jalview.schemabinding.version2.OtherData;
 import jalview.schemabinding.version2.PdbentryItem;
 import jalview.schemabinding.version2.Pdbids;
@@ -75,6 +83,9 @@ import jalview.schemabinding.version2.ThresholdLine;
 import jalview.schemabinding.version2.Tree;
 import jalview.schemabinding.version2.UserColours;
 import jalview.schemabinding.version2.Viewport;
+import jalview.schemabinding.version2.types.ColourThreshTypeType;
+import jalview.schemabinding.version2.types.FeatureMatcherByType;
+import jalview.schemabinding.version2.types.NoValueColour;
 import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -83,10 +94,12 @@ import jalview.schemes.ResidueProperties;
 import jalview.schemes.UserColourScheme;
 import jalview.structure.StructureSelectionManager;
 import jalview.structures.models.AAStructureBindingModel;
+import jalview.util.Format;
 import jalview.util.MessageManager;
 import jalview.util.Platform;
 import jalview.util.StringUtils;
 import jalview.util.jarInputStreamProvider;
+import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 import jalview.viewmodel.ViewportRanges;
 import jalview.viewmodel.seqfeatures.FeatureRendererSettings;
@@ -115,6 +128,7 @@ 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.HashMap;
 import java.util.HashSet;
@@ -216,34 +230,6 @@ public class Jalview2XML
     }
   }
 
-  void clearSeqRefs()
-  {
-    if (_cleartables)
-    {
-      if (seqRefIds != null)
-      {
-        seqRefIds.clear();
-      }
-      if (seqsToIds != null)
-      {
-        seqsToIds.clear();
-      }
-      if (incompleteSeqs != null)
-      {
-        incompleteSeqs.clear();
-      }
-      // seqRefIds = null;
-      // seqsToIds = null;
-    }
-    else
-    {
-      // do nothing
-      warn("clearSeqRefs called when _cleartables was not set. Doing nothing.");
-      // seqRefIds = new Hashtable();
-      // seqsToIds = new IdentityHashMap();
-    }
-  }
-
   void initSeqRefs()
   {
     if (seqsToIds == null)
@@ -907,15 +893,33 @@ public class Jalview2XML
         }
         if (sf.otherDetails != null)
         {
-          String key;
-          Iterator<String> keys = sf.otherDetails.keySet().iterator();
-          while (keys.hasNext())
+          /*
+           * save feature attributes, which may be simple strings or
+           * map valued (have sub-attributes)
+           */
+          for (Entry<String, Object> entry : sf.otherDetails.entrySet())
           {
-            key = keys.next();
-            OtherData keyValue = new OtherData();
-            keyValue.setKey(key);
-            keyValue.setValue(sf.otherDetails.get(key).toString());
-            features.addOtherData(keyValue);
+            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);
+              }
+            }
+            else
+            {
+              OtherData otherData = new OtherData();
+              otherData.setKey(key);
+              otherData.setValue(value.toString());
+              features.addOtherData(otherData);
+            }
           }
         }
 
@@ -1084,7 +1088,7 @@ public class Jalview2XML
 
     // SAVE TREES
     // /////////////////////////////////
-    if (!storeDS && av.currentTree != null)
+    if (!storeDS && av.getCurrentTree() != null)
     {
       // FIND ANY ASSOCIATED TREES
       // NOT IMPLEMENTED FOR HEADLESS STATE AT PRESENT
@@ -1102,7 +1106,7 @@ public class Jalview2XML
             {
               Tree tree = new Tree();
               tree.setTitle(tp.getTitle());
-              tree.setCurrentTree((av.currentTree == tp.getTree()));
+              tree.setCurrentTree((av.getCurrentTree() == tp.getTree()));
               tree.setNewick(tp.getTree().print());
               tree.setThreshold(tp.treeCanvas.threshold);
 
@@ -1341,19 +1345,33 @@ public class Jalview2XML
       {
         jalview.schemabinding.version2.FeatureSettings fs = new jalview.schemabinding.version2.FeatureSettings();
 
-        String[] renderOrder = ap.getSeqPanel().seqCanvas
-                .getFeatureRenderer().getRenderOrder()
-                .toArray(new String[0]);
+        FeatureRenderer 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)
           {
-            FeatureColourI fcol = ap.getSeqPanel().seqCanvas
-                    .getFeatureRenderer().getFeatureStyle(featureType);
             Setting setting = new 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());
@@ -1361,8 +1379,25 @@ public class Jalview2XML
               setting.setMin(fcol.getMin());
               setting.setMax(fcol.getMax());
               setting.setColourByLabel(fcol.isColourByLabel());
+              if (fcol.isColourByAttribute())
+              {
+                setting.setAttributeName(fcol.getAttributeName());
+              }
               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));
@@ -1374,7 +1409,7 @@ public class Jalview2XML
 
             setting.setDisplay(
                     av.getFeaturesDisplayed().isVisible(featureType));
-            float rorder = ap.getSeqPanel().seqCanvas.getFeatureRenderer()
+            float rorder = fr
                     .getOrder(featureType);
             if (rorder > -1)
             {
@@ -1386,8 +1421,7 @@ public class Jalview2XML
         }
 
         // is groups actually supposed to be a map here ?
-        Iterator<String> en = ap.getSeqPanel().seqCanvas
-                .getFeatureRenderer().getFeatureGroups().iterator();
+        Iterator<String> en = fr.getFeatureGroups().iterator();
         Vector<String> groupsAdded = new Vector<>();
         while (en.hasNext())
         {
@@ -1398,8 +1432,7 @@ public class Jalview2XML
           }
           Group g = new Group();
           g.setName(grp);
-          g.setDisplay(((Boolean) ap.getSeqPanel().seqCanvas
-                  .getFeatureRenderer().checkGroupVisibility(grp, false))
+          g.setDisplay(((Boolean) fr.checkGroupVisibility(grp, false))
                           .booleanValue());
           fs.addGroup(g);
           groupsAdded.addElement(grp);
@@ -1417,9 +1450,10 @@ public class Jalview2XML
         }
         else
         {
-          ArrayList<int[]> hiddenRegions = hidden.getHiddenColumnsCopy();
-          for (int[] region : hiddenRegions)
+          Iterator<int[]> hiddenRegions = hidden.iterator();
+          while (hiddenRegions.hasNext())
           {
+            int[] region = hiddenRegions.next();
             HiddenColumns hc = new HiddenColumns();
             hc.setStart(region[0]);
             hc.setEnd(region[1]);
@@ -2295,6 +2329,7 @@ public class Jalview2XML
 
       jarInputStreamProvider jprovider = createjarInputStreamProvider(file);
       af = loadJalviewAlign(jprovider);
+      af.setMenusForViewport();
 
     } catch (MalformedURLException e)
     {
@@ -2989,19 +3024,46 @@ public class Jalview2XML
                     features[f].getEnd(), features[f].getScore(),
                     features[f].getFeatureGroup());
             sf.setStatus(features[f].getStatus());
+
+            /*
+             * load any feature attributes - include map-valued attributes
+             */
+            Map<String, Map<String, String>> mapAttributes = new HashMap<>();
             for (int od = 0; od < features[f].getOtherDataCount(); od++)
             {
               OtherData keyValue = features[f].getOtherData(od);
-              if (keyValue.getKey().startsWith("LINK"))
+              String attributeName = keyValue.getKey();
+              String attributeValue = keyValue.getValue();
+              if (attributeName.startsWith("LINK"))
               {
-                sf.addLink(keyValue.getValue());
+                sf.addLink(attributeValue);
               }
               else
               {
-                sf.setValue(keyValue.getKey(), keyValue.getValue());
+                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);
           }
@@ -4249,7 +4311,8 @@ public class Jalview2XML
       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);
+      binding.getSsm().setMapping(seq, null, pdbFile, DataSourceType.FILE,
+              null);
       binding.addSequenceForStructFile(pdbFile, seq);
     }
     // and add the AlignmentPanel's reference to the view panel
@@ -4576,9 +4639,11 @@ public class Jalview2XML
       af.viewport.setShowGroupConservation(false);
     }
 
-    // recover featre settings
+    // recover feature settings
     if (jms.getFeatureSettings() != null)
     {
+      FeatureRenderer fr = af.alignPanel.getSeqPanel().seqCanvas
+              .getFeatureRenderer();
       FeaturesDisplayed fdi;
       af.viewport.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
       String[] renderOrder = new String[jms.getFeatureSettings()
@@ -4590,14 +4655,51 @@ public class Jalview2XML
               .getSettingCount(); fs++)
       {
         Setting setting = jms.getFeatureSettings().getSetting(fs);
+        String featureType = setting.getType();
+
+        /*
+         * restore feature filters (if any)
+         */
+        MatcherSet filters = setting.getMatcherSet();
+        if (filters != null)
+        {
+          FeatureMatcherSetI filter = Jalview2XML
+                  .unmarshalFilter(featureType, filters);
+          if (!filter.isEmpty())
+          {
+            fr.setFeatureFilter(featureType, filter);
+          }
+        }
+
+        /*
+         * restore feature colour scheme
+         */
+        Color maxColour = new Color(setting.getColour());
         if (setting.hasMincolour())
         {
-          FeatureColourI gc = setting.hasMin()
-                  ? new FeatureColour(new Color(setting.getMincolour()),
-                          new Color(setting.getColour()), setting.getMin(),
-                          setting.getMax())
-                  : new FeatureColour(new Color(setting.getMincolour()),
-                          new Color(setting.getColour()), 0, 1);
+          /*
+           * minColour is always set unless a simple colour
+           * (including for colour by label though it doesn't use it)
+           */
+          Color minColour = new Color(setting.getMincolour());
+          Color noValueColour = minColour;
+          NoValueColour noColour = setting.getNoValueColour();
+          if (noColour == NoValueColour.NONE)
+          {
+            noValueColour = null;
+          }
+          else if (noColour == NoValueColour.MAX)
+          {
+            noValueColour = maxColour;
+          }
+          float min = setting.hasMin() ? setting.getMin() : 0f;
+          float max = setting.hasMin() ? setting.getMax() : 1f;
+          FeatureColourI gc = new FeatureColour(minColour, maxColour,
+                  noValueColour, min, max);
+          if (setting.getAttributeNameCount() > 0)
+          {
+            gc.setAttributeName(setting.getAttributeName());
+          }
           if (setting.hasThreshold())
           {
             gc.setThreshold(setting.getThreshold());
@@ -4622,26 +4724,26 @@ public class Jalview2XML
             gc.setColourByLabel(setting.getColourByLabel());
           }
           // and put in the feature colour table.
-          featureColours.put(setting.getType(), gc);
+          featureColours.put(featureType, gc);
         }
         else
         {
-          featureColours.put(setting.getType(),
-                  new FeatureColour(new Color(setting.getColour())));
+          featureColours.put(featureType,
+                  new FeatureColour(maxColour));
         }
-        renderOrder[fs] = setting.getType();
+        renderOrder[fs] = featureType;
         if (setting.hasOrder())
         {
-          featureOrder.put(setting.getType(), setting.getOrder());
+          featureOrder.put(featureType, setting.getOrder());
         }
         else
         {
-          featureOrder.put(setting.getType(), new Float(
+          featureOrder.put(featureType, new Float(
                   fs / jms.getFeatureSettings().getSettingCount()));
         }
         if (setting.getDisplay())
         {
-          fdi.setVisible(setting.getType());
+          fdi.setVisible(featureType);
         }
       }
       Map<String, Boolean> fgtable = new Hashtable<>();
@@ -4655,9 +4757,7 @@ public class Jalview2XML
       // jms.getFeatureSettings().getTransparency() : 0.0, featureOrder);
       FeatureRendererSettings frs = new FeatureRendererSettings(renderOrder,
               fgtable, featureColours, 1.0f, featureOrder);
-      af.alignPanel.getSeqPanel().seqCanvas.getFeatureRenderer()
-              .transferSettings(frs);
-
+      fr.transferSettings(frs);
     }
 
     if (view.getHiddenColumnsCount() > 0)
@@ -5341,28 +5441,25 @@ public class Jalview2XML
 
   }
 
-  public jalview.gui.AlignmentPanel copyAlignPanel(AlignmentPanel ap,
-          boolean keepSeqRefs)
+  /**
+   * Provides a 'copy' of an alignment view (on action New View) by 'saving' the
+   * view as XML (but not to file), and then reloading it
+   * 
+   * @param ap
+   * @return
+   */
+  public AlignmentPanel copyAlignPanel(AlignmentPanel ap)
   {
     initSeqRefs();
     JalviewModel jm = saveState(ap, null, null, null);
 
-    if (!keepSeqRefs)
-    {
-      clearSeqRefs();
-      jm.getJalviewModelSequence().getViewport(0).setSequenceSetId(null);
-    }
-    else
-    {
-      uniqueSetSuffix = "";
-      jm.getJalviewModelSequence().getViewport(0).setId(null); // we don't
-      // overwrite the
-      // view we just
-      // copied
-    }
+    uniqueSetSuffix = "";
+    jm.getJalviewModelSequence().getViewport(0).setId(null);
+    // we don't overwrite the view we just copied
+
     if (this.frefedSequence == null)
     {
-      frefedSequence = new Vector();
+      frefedSequence = new Vector<>();
     }
 
     viewportsAdded.clear();
@@ -5382,32 +5479,8 @@ public class Jalview2XML
     return af.alignPanel;
   }
 
-  /**
-   * flag indicating if hashtables should be cleared on finalization TODO this
-   * flag may not be necessary
-   */
-  private final boolean _cleartables = true;
-
   private Hashtable jvids2vobj;
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see java.lang.Object#finalize()
-   */
-  @Override
-  protected void finalize() throws Throwable
-  {
-    // really make sure we have no buried refs left.
-    if (_cleartables)
-    {
-      clearSeqRefs();
-    }
-    this.seqRefIds = null;
-    this.seqsToIds = null;
-    super.finalize();
-  }
-
   private void warn(String msg)
   {
     warn(msg, null);
@@ -5638,4 +5711,289 @@ public class Jalview2XML
   {
     return counter++;
   }
+
+  /**
+   * Populates an XML model of the feature colour scheme for one feature type
+   * 
+   * @param featureType
+   * @param fcol
+   * @return
+   */
+  protected static jalview.schemabinding.version2.Colour marshalColour(
+          String featureType, FeatureColourI fcol)
+  {
+    jalview.schemabinding.version2.Colour col = new jalview.schemabinding.version2.Colour();
+    if (fcol.isSimpleColour())
+    {
+      col.setRGB(Format.getHexString(fcol.getColour()));
+    }
+    else
+    {
+      col.setRGB(Format.getHexString(fcol.getMaxColour()));
+      col.setMin(fcol.getMin());
+      col.setMax(fcol.getMax());
+      col.setMinRGB(jalview.util.Format.getHexString(fcol.getMinColour()));
+      col.setAutoScale(fcol.isAutoScaled());
+      col.setThreshold(fcol.getThreshold());
+      col.setColourByLabel(fcol.isColourByLabel());
+      col.setThreshType(fcol.isAboveThreshold() ? ColourThreshTypeType.ABOVE
+              : (fcol.isBelowThreshold() ? ColourThreshTypeType.BELOW
+                      : ColourThreshTypeType.NONE));
+      if (fcol.isColourByAttribute())
+      {
+        col.setAttributeName(fcol.getAttributeName());
+      }
+      Color noColour = fcol.getNoColour();
+      if (noColour == null)
+      {
+        col.setNoValueColour(NoValueColour.NONE);
+      }
+      else if (noColour == fcol.getMaxColour())
+      {
+        col.setNoValueColour(NoValueColour.MAX);
+      }
+      else
+      {
+        col.setNoValueColour(NoValueColour.MIN);
+      }
+    }
+    col.setName(featureType);
+    return col;
+  }
+
+  /**
+   * Populates an XML model of the feature filter(s) for one feature type
+   * 
+   * @param firstMatcher
+   *          the first (or only) match condition)
+   * @param filter
+   *          remaining match conditions (if any)
+   * @param and
+   *          if true, conditions are and-ed, else or-ed
+   */
+  protected static MatcherSet marshalFilter(FeatureMatcherI firstMatcher,
+          Iterator<FeatureMatcherI> filters, boolean and)
+  {
+    MatcherSet result = new MatcherSet();
+  
+    if (filters.hasNext())
+    {
+      /*
+       * compound matcher
+       */
+      CompoundMatcher compound = new CompoundMatcher();
+      compound.setAnd(and);
+      MatcherSet matcher1 = marshalFilter(firstMatcher,
+              Collections.emptyIterator(), and);
+      compound.addMatcherSet(matcher1);
+      FeatureMatcherI nextMatcher = filters.next();
+      MatcherSet matcher2 = marshalFilter(nextMatcher, filters, and);
+      compound.addMatcherSet(matcher2);
+      result.setCompoundMatcher(compound);
+    }
+    else
+    {
+      /*
+       * single condition matcher
+       */
+      MatchCondition matcherModel = new MatchCondition();
+      matcherModel.setCondition(
+              firstMatcher.getMatcher().getCondition().getStableName());
+      matcherModel.setValue(firstMatcher.getMatcher().getPattern());
+      if (firstMatcher.isByAttribute())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYATTRIBUTE);
+        matcherModel.setAttributeName(firstMatcher.getAttribute());
+      }
+      else if (firstMatcher.isByLabel())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYLABEL);
+      }
+      else if (firstMatcher.isByScore())
+      {
+        matcherModel.setBy(FeatureMatcherByType.BYSCORE);
+      }
+      result.setMatchCondition(matcherModel);
+    }
+  
+    return result;
+  }
+
+  /**
+   * Loads one XML model of a feature filter to a Jalview object
+   * 
+   * @param featureType
+   * @param matcherSetModel
+   * @return
+   */
+  protected static FeatureMatcherSetI unmarshalFilter(
+          String featureType, MatcherSet matcherSetModel)
+  {
+    FeatureMatcherSetI result = new FeatureMatcherSet();
+    try
+    {
+      unmarshalFilterConditions(result, matcherSetModel, true);
+    } catch (IllegalStateException e)
+    {
+      // mixing AND and OR conditions perhaps
+      System.err.println(
+              String.format("Error reading filter conditions for '%s': %s",
+                      featureType, e.getMessage()));
+      // return as much as was parsed up to the error
+    }
+  
+    return result;
+  }
+
+  /**
+   * Adds feature match conditions to matcherSet as unmarshalled from XML
+   * (possibly recursively for compound conditions)
+   * 
+   * @param matcherSet
+   * @param matcherSetModel
+   * @param and
+   *          if true, multiple conditions are AND-ed, else they are OR-ed
+   * @throws IllegalStateException
+   *           if AND and OR conditions are mixed
+   */
+  protected static void unmarshalFilterConditions(
+          FeatureMatcherSetI matcherSet, MatcherSet matcherSetModel,
+          boolean and)
+  {
+    MatchCondition mc = matcherSetModel.getMatchCondition();
+    if (mc != null)
+    {
+      /*
+       * single condition
+       */
+      FeatureMatcherByType filterBy = mc.getBy();
+      Condition cond = Condition.fromString(mc.getCondition());
+      String pattern = mc.getValue();
+      FeatureMatcherI matchCondition = null;
+      if (filterBy == FeatureMatcherByType.BYLABEL)
+      {
+        matchCondition = FeatureMatcher.byLabel(cond, pattern);
+      }
+      else if (filterBy == FeatureMatcherByType.BYSCORE)
+      {
+        matchCondition = FeatureMatcher.byScore(cond, pattern);
+  
+      }
+      else if (filterBy == FeatureMatcherByType.BYATTRIBUTE)
+      {
+        String[] attNames = mc.getAttributeName();
+        matchCondition = FeatureMatcher.byAttribute(cond, pattern,
+                attNames);
+      }
+  
+      /*
+       * note this throws IllegalStateException if AND-ing to a 
+       * previously OR-ed compound condition, or vice versa
+       */
+      if (and)
+      {
+        matcherSet.and(matchCondition);
+      }
+      else
+      {
+        matcherSet.or(matchCondition);
+      }
+    }
+    else
+    {
+      /*
+       * compound condition
+       */
+      MatcherSet[] matchers = matcherSetModel.getCompoundMatcher()
+              .getMatcherSet();
+      boolean anded = matcherSetModel.getCompoundMatcher().getAnd();
+      if (matchers.length == 2)
+      {
+        unmarshalFilterConditions(matcherSet, matchers[0], anded);
+        unmarshalFilterConditions(matcherSet, matchers[1], anded);
+      }
+      else
+      {
+        System.err.println("Malformed compound filter condition");
+      }
+    }
+  }
+
+  /**
+   * Loads one XML model of a feature colour to a Jalview object
+   * 
+   * @param colourModel
+   * @return
+   */
+  protected static FeatureColourI unmarshalColour(
+          jalview.schemabinding.version2.Colour colourModel)
+  {
+    FeatureColourI colour = null;
+  
+    if (colourModel.hasMax())
+    {
+      Color mincol = null;
+      Color maxcol = null;
+      Color noValueColour = null;
+  
+      try
+      {
+        mincol = new Color(Integer.parseInt(colourModel.getMinRGB(), 16));
+        maxcol = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      } catch (Exception e)
+      {
+        Cache.log.warn("Couldn't parse out graduated feature color.", e);
+      }
+  
+      NoValueColour noCol = colourModel.getNoValueColour();
+      if (noCol == NoValueColour.MIN)
+      {
+        noValueColour = mincol;
+      }
+      else if (noCol == NoValueColour.MAX)
+      {
+        noValueColour = maxcol;
+      }
+  
+      colour = new FeatureColour(mincol, maxcol, noValueColour,
+              colourModel.getMin(),
+              colourModel.getMax());
+      String[] attributes = colourModel.getAttributeName();
+      if (attributes != null && attributes.length > 0)
+      {
+        colour.setAttributeName(attributes);
+      }
+      if (colourModel.hasAutoScale())
+      {
+        colour.setAutoScaled(colourModel.getAutoScale());
+      }
+      if (colourModel.hasColourByLabel())
+      {
+        colour.setColourByLabel(colourModel.getColourByLabel());
+      }
+      if (colourModel.hasThreshold())
+      {
+        colour.setThreshold(colourModel.getThreshold());
+      }
+      ColourThreshTypeType ttyp = colourModel.getThreshType();
+      if (ttyp != null)
+      {
+        if (ttyp == ColourThreshTypeType.ABOVE)
+        {
+          colour.setAboveThreshold(true);
+        }
+        else if (ttyp == ColourThreshTypeType.BELOW)
+        {
+          colour.setBelowThreshold(true);
+        }
+      }
+    }
+    else
+    {
+      Color color = new Color(Integer.parseInt(colourModel.getRGB(), 16));
+      colour = new FeatureColour(color);
+    }
+  
+    return colour;
+  }
 }
index 05f5ffc..1d7bf3d 100644 (file)
@@ -27,8 +27,8 @@ import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
 import java.awt.event.WindowEvent;
-import java.awt.event.WindowListener;
 
 import javax.swing.JButton;
 import javax.swing.JDialog;
@@ -118,55 +118,14 @@ public abstract class JalviewDialog extends JPanel
         closeDialog();
       }
     });
-    frame.addWindowListener(new WindowListener()
+    frame.addWindowListener(new WindowAdapter()
     {
-
-      @Override
-      public void windowOpened(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowIconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeiconified(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
-      @Override
-      public void windowDeactivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
-
       @Override
       public void windowClosing(WindowEvent e)
       {
         // user has cancelled the dialog
         closeDialog();
       }
-
-      @Override
-      public void windowClosed(WindowEvent e)
-      {
-      }
-
-      @Override
-      public void windowActivated(WindowEvent e)
-      {
-        // TODO Auto-generated method stub
-
-      }
     });
   }
 
@@ -177,8 +136,8 @@ public abstract class JalviewDialog extends JPanel
   {
     try
     {
-      frame.dispose();
       raiseClosed();
+      frame.dispose();
     } catch (Exception ex)
     {
     }
index 0a765cb..4658668 100644 (file)
@@ -24,14 +24,20 @@ import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
 import java.awt.Color;
+import java.awt.Component;
 import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.Rectangle;
 import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.List;
 import java.util.Objects;
 
 import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
 import javax.swing.JButton;
+import javax.swing.JComboBox;
 import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JMenu;
@@ -39,6 +45,8 @@ import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JScrollBar;
 import javax.swing.SwingConstants;
+import javax.swing.border.Border;
+import javax.swing.border.TitledBorder;
 
 /**
  * useful functions for building Swing GUIs
@@ -304,4 +312,71 @@ public final class JvSwingUtils
     comp.setFont(JvSwingUtils.getLabelFont());
   }
 
+  /**
+   * A helper method to build a drop-down choice of values, with tooltips for
+   * the entries
+   * 
+   * @param entries
+   * @param tooltips
+   */
+  public static JComboBox<String> buildComboWithTooltips(
+          List<String> entries, List<String> tooltips)
+  {
+    JComboBox<String> combo = new JComboBox<>();
+    final ComboBoxTooltipRenderer renderer = new ComboBoxTooltipRenderer();
+    combo.setRenderer(renderer);
+    for (String attName : entries)
+    {
+      combo.addItem(attName);
+    }
+    renderer.setTooltips(tooltips);
+    final MouseAdapter mouseListener = new MouseAdapter()
+    {
+      @Override
+      public void mouseEntered(MouseEvent e)
+      {
+        int j = combo.getSelectedIndex();
+        if (j > -1)
+        {
+          combo.setToolTipText(tooltips.get(j));
+        }
+      }
+      @Override
+      public void mouseExited(MouseEvent e)
+      {
+        combo.setToolTipText(null);
+      }
+    };
+    for (Component c : combo.getComponents())
+    {
+      c.addMouseListener(mouseListener);
+    }
+    return combo;
+  }
+
+  /**
+   * Adds a titled border to the component in the default font and position (top
+   * left), optionally witht italic text
+   * 
+   * @param comp
+   * @param title
+   * @param italic
+   */
+  public static TitledBorder createTitledBorder(JComponent comp,
+          String title, boolean italic)
+  {
+    Font font = comp.getFont();
+    if (italic)
+    {
+      font = new Font(font.getName(), Font.ITALIC, font.getSize());
+    }
+    Border border = BorderFactory.createTitledBorder("");
+    TitledBorder titledBorder = BorderFactory.createTitledBorder(border,
+            title, TitledBorder.LEADING, TitledBorder.DEFAULT_POSITION,
+            font);
+    comp.setBorder(titledBorder);
+
+    return titledBorder;
+  }
+
 }
index 7371eb5..cc361a5 100644 (file)
@@ -157,11 +157,9 @@ public class OverviewCanvas extends JComponent
     {
       mg.translate(0, od.getSequencesHeight());
       or.drawGraph(mg, av.getAlignmentConservationAnnotation(),
-              av.getCharWidth(), od.getGraphHeight(),
-              od.getColumns(av.getAlignment()));
+              od.getGraphHeight(), od.getColumns(av.getAlignment()));
       mg.translate(0, -od.getSequencesHeight());
     }
-    System.gc();
 
     or.removePropertyChangeListener(progressPanel);
     or = null;
@@ -183,6 +181,8 @@ public class OverviewCanvas extends JComponent
   @Override
   public void paintComponent(Graphics g)
   {
+    super.paintComponent(g);
+
     if (restart)
     {
       if (lastMiniMe == null)
@@ -204,7 +204,8 @@ public class OverviewCanvas extends JComponent
               && ((getWidth() != od.getWidth())
                       || (getHeight() != od.getHeight())))
       {
-        // if there is annotation, scale the alignment and annotation separately
+        // if there is annotation, scale the alignment and annotation
+        // separately
         if (od.getGraphHeight() > 0)
         {
           BufferedImage topImage = lastMiniMe.getSubimage(0, 0,
@@ -235,25 +236,24 @@ public class OverviewCanvas extends JComponent
           od.setHeight(getHeight());
         }
 
-        // scale lastMiniMe to the new size
-        g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
-
         // make sure the box is in the right place
         od.setBoxPosition(av.getAlignment().getHiddenSequences(),
                 av.getAlignment().getHiddenColumns());
       }
-      else // not a resize
-      {
-        // fall back to normal behaviour
-        g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
-      }
+      // fall back to normal behaviour
+      g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
     }
-
+    else
+    {
+      g.drawImage(lastMiniMe, 0, 0, getWidth(), getHeight(), this);
+    }
+    
     // draw the box
     g.setColor(Color.red);
     od.drawBox(g);
   }
 
+
   public void dispose()
   {
     dispose = true;
index 51d7a84..02d54a8 100755 (executable)
@@ -40,8 +40,10 @@ import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseMotionAdapter;
 import java.beans.PropertyChangeEvent;
+import java.beans.PropertyVetoException;
 
 import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JInternalFrame;
 import javax.swing.JPanel;
 import javax.swing.JPopupMenu;
 import javax.swing.SwingUtilities;
@@ -168,15 +170,20 @@ public class OverviewPanel extends JPanel
       @Override
       public void mouseMoved(MouseEvent evt)
       {
-        if (od.isPositionInBox(evt.getX(), evt.getY()))
+        if (!draggingBox)
+        // don't bother changing the cursor if we're dragging the box
+        // as we can't have moved inside or out of the box in that case
         {
-          // display drag cursor at mouse position
-          setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
-        }
-        else
-        {
-          // reset cursor
-          setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+          if (od.isPositionInBox(evt.getX(), evt.getY()))
+          {
+            // display drag cursor at mouse position
+            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+          }
+          else
+          {
+            // reset cursor
+            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+          }
         }
       }
     });
@@ -201,6 +208,10 @@ public class OverviewPanel extends JPanel
           if (!od.isPositionInBox(evt.getX(), evt.getY()))
           {
             draggingBox = false;
+
+            // display drag cursor at mouse position
+            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+
             od.updateViewportFromMouse(evt.getX(), evt.getY(),
                     av.getAlignment().getHiddenSequences(),
                     av.getAlignment().getHiddenColumns());
@@ -223,6 +234,13 @@ public class OverviewPanel extends JPanel
           showPopupMenu(evt);
         }
       }
+
+      @Override
+      public void mouseReleased(MouseEvent evt)
+      {
+        draggingBox = false;
+      }
+
     });
   }
 
@@ -327,6 +345,22 @@ public class OverviewPanel extends JPanel
    * changed
    * 
    */
+  private void setBoxPositionOnly()
+  {
+    if (od != null)
+    {
+      int oldX = od.getBoxX();
+      int oldY = od.getBoxY();
+      int oldWidth = od.getBoxWidth();
+      int oldHeight = od.getBoxHeight();
+      od.setBoxPosition(av.getAlignment().getHiddenSequences(),
+              av.getAlignment().getHiddenColumns());
+      repaint(oldX - 1, oldY - 1, oldWidth + 2, oldHeight + 2);
+      repaint(od.getBoxX(), od.getBoxY(), od.getBoxWidth(),
+              od.getBoxHeight());
+    }
+  }
+
   private void setBoxPosition()
   {
     if (od != null)
@@ -340,7 +374,7 @@ public class OverviewPanel extends JPanel
   @Override
   public void propertyChange(PropertyChangeEvent evt)
   {
-    setBoxPosition();
+    setBoxPositionOnly();
   }
 
   /**
@@ -350,8 +384,22 @@ public class OverviewPanel extends JPanel
   {
     try
     {
-      av.getRanges().removePropertyChangeListener(this);
+      if (av != null)
+      {
+        av.getRanges().removePropertyChangeListener(this);
+      }
+
       oviewCanvas.dispose();
+
+      /*
+       * close the parent frame (which also removes it from the
+       * Desktop Windows menu)
+       */
+      ((JInternalFrame) SwingUtilities.getAncestorOfClass(
+              JInternalFrame.class, (this))).setClosed(true);
+    } catch (PropertyVetoException e)
+    {
+      // ignore
     } finally
     {
       progressPanel = null;
index f861a7c..7ceceee 100644 (file)
@@ -79,6 +79,8 @@ public class PCAPanel extends GPCAPanel
 
   int top = 0;
 
+  private boolean working;
+
   /**
    * Creates a new PCAPanel object using default score model and parameters
    * 
@@ -234,6 +236,7 @@ public class PCAPanel extends GPCAPanel
       message = MessageManager.getString("label.pca_calculating");
     }
     progress.setProgressBar(message, progId);
+    working = true;
     try
     {
       calcSettings.setEnabled(false);
@@ -252,6 +255,7 @@ public class PCAPanel extends GPCAPanel
     } catch (OutOfMemoryError er)
     {
       new OOMWarning("calculating PCA", er);
+      working = false;
       return;
     } finally
     {
@@ -266,6 +270,7 @@ public class PCAPanel extends GPCAPanel
               .getString("label.principal_component_analysis"), 475, 450);
       this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
     }
+    working = false;
   }
 
   @Override
@@ -459,7 +464,16 @@ public class PCAPanel extends GPCAPanel
     public void run()
     {
       PrinterJob printJob = PrinterJob.getPrinterJob();
-      PageFormat pf = printJob.pageDialog(printJob.defaultPage());
+      PageFormat defaultPage = printJob.defaultPage();
+      PageFormat pf = printJob.pageDialog(defaultPage);
+
+      if (defaultPage == pf)
+      {
+        /*
+         * user cancelled
+         */
+        return;
+      }
 
       printJob.setPrintable(this, pf);
 
@@ -788,4 +802,14 @@ public class PCAPanel extends GPCAPanel
     top = t;
     zCombobox.setSelectedIndex(2);
   }
+
+  /**
+   * Answers true if PCA calculation is in progress, else false
+   * 
+   * @return
+   */
+  public boolean isWorking()
+  {
+    return working;
+  }
 }
index d731e70..ced5544 100755 (executable)
@@ -26,9 +26,9 @@ import jalview.datamodel.SequenceI;
 import java.awt.Component;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 
 /**
  * Route datamodel/view update events for a sequence set to any display
@@ -74,26 +74,21 @@ public class PaintRefresher
    */
   public static void RemoveComponent(Component comp)
   {
-    List<String> emptied = new ArrayList<String>();
-    for (Entry<String, List<Component>> registered : components.entrySet())
+    if (components == null)
     {
-      String id = registered.getKey();
-      List<Component> comps = components.get(id);
+      return;
+    }
+
+    Iterator<String> it = components.keySet().iterator();
+    while (it.hasNext())
+    {
+      List<Component> comps = components.get(it.next());
       comps.remove(comp);
       if (comps.isEmpty())
       {
-        emptied.add(id);
+        it.remove();
       }
     }
-
-    /*
-     * Remove now empty ids after the above (to avoid
-     * ConcurrentModificationException).
-     */
-    for (String id : emptied)
-    {
-      components.remove(id);
-    }
   }
 
   public static void Refresh(Component source, String id)
index f75407c..d081794 100755 (executable)
@@ -22,7 +22,8 @@ package jalview.gui;
 
 import jalview.analysis.AlignSeq;
 import jalview.datamodel.Alignment;
-import jalview.datamodel.Sequence;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.jbgui.GPairwiseAlignPanel;
 import jalview.util.MessageManager;
@@ -40,49 +41,56 @@ import java.util.Vector;
 public class PairwiseAlignPanel extends GPairwiseAlignPanel
 {
 
+  private static final String DASHES = "---------------------\n";
+
   AlignmentViewport av;
 
-  Vector sequences;
+  Vector<SequenceI> sequences;
 
   /**
    * Creates a new PairwiseAlignPanel object.
    * 
-   * @param av
+   * @param viewport
    *          DOCUMENT ME!
    */
-  public PairwiseAlignPanel(AlignmentViewport av)
+  public PairwiseAlignPanel(AlignmentViewport viewport)
   {
     super();
-    this.av = av;
+    this.av = viewport;
 
-    sequences = new Vector();
+    sequences = new Vector<SequenceI>();
 
-    SequenceI[] seqs;
-    String[] seqStrings = av.getViewAsString(true);
+    SequenceGroup selectionGroup = viewport.getSelectionGroup();
+    boolean isSelection = selectionGroup != null
+            && selectionGroup.getSize() > 0;
+    AlignmentView view = viewport.getAlignmentView(isSelection);
+    // String[] seqStrings = viewport.getViewAsString(true);
+    String[] seqStrings = view.getSequenceStrings(viewport
+            .getGapCharacter());
 
-    if (av.getSelectionGroup() == null)
+    SequenceI[] seqs;
+    if (isSelection)
     {
-      seqs = av.getAlignment().getSequencesArray();
+      seqs = (SequenceI[]) view.getAlignmentAndHiddenColumns(viewport
+              .getGapCharacter())[0];
     }
     else
     {
-      seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment());
+      seqs = av.getAlignment().getSequencesArray();
     }
 
-    String type = (av.getAlignment().isNucleotide()) ? AlignSeq.DNA
+    String type = (viewport.getAlignment().isNucleotide()) ? AlignSeq.DNA
             : AlignSeq.PEP;
 
     float[][] scores = new float[seqs.length][seqs.length];
-    double totscore = 0;
+    double totscore = 0D;
     int count = seqs.length;
-
-    Sequence seq;
+    boolean first = true;
 
     for (int i = 1; i < count; i++)
     {
       for (int j = 0; j < i; j++)
       {
-
         AlignSeq as = new AlignSeq(seqs[i], seqStrings[i], seqs[j],
                 seqStrings[j], type);
 
@@ -94,9 +102,15 @@ public class PairwiseAlignPanel extends GPairwiseAlignPanel
         as.calcScoreMatrix();
         as.traceAlignment();
 
+        if (!first)
+        {
+          System.out.println(DASHES);
+          textarea.append(DASHES);
+        }
+        first = false;
         as.printAlignment(System.out);
-        scores[i][j] = (float) as.getMaxScore()
-                / (float) as.getASeq1().length;
+        scores[i][j] = as.getMaxScore()
+                / as.getASeq1().length;
         totscore = totscore + scores[i][j];
 
         textarea.append(as.getOutput());
@@ -107,28 +121,53 @@ public class PairwiseAlignPanel extends GPairwiseAlignPanel
 
     if (count > 2)
     {
-      System.out.println(
-              "Pairwise alignment scaled similarity score matrix\n");
+      printScoreMatrix(seqs, scores, totscore);
+    }
+  }
 
-      for (int i = 0; i < count; i++)
-      {
-        jalview.util.Format.print(System.out, "%s \n",
-                ("" + i) + " " + seqs[i].getName());
-      }
+  /**
+   * Prints a matrix of seqi-seqj pairwise alignment scores to sysout
+   * 
+   * @param seqs
+   * @param scores
+   * @param totscore
+   */
+  protected void printScoreMatrix(SequenceI[] seqs, float[][] scores,
+          double totscore)
+  {
+    System.out
+            .println("Pairwise alignment scaled similarity score matrix\n");
 
-      System.out.println("\n");
+    for (int i = 0; i < seqs.length; i++)
+    {
+      System.out.println(String.format("%3d %s", i + 1,
+              seqs[i].getDisplayId(true)));
+    }
+
+    /*
+     * table heading columns for sequences 1, 2, 3...
+     */
+    System.out.print("\n ");
+    for (int i = 0; i < seqs.length; i++)
+    {
+      System.out.print(String.format("%7d", i + 1));
+    }
+    System.out.println();
 
-      for (int i = 0; i < count; i++)
+    for (int i = 0; i < seqs.length; i++)
+    {
+      System.out.print(String.format("%3d", i + 1));
+      for (int j = 0; j < i; j++)
       {
-        for (int j = 0; j < i; j++)
-        {
-          jalview.util.Format.print(System.out, "%7.3f",
-                  scores[i][j] / totscore);
-        }
+        /*
+         * as a fraction of tot score, outputs are 0 <= score <= 1
+         */
+        System.out.print(String.format("%7.3f", scores[i][j] / totscore));
       }
-
-      System.out.println("\n");
+      System.out.println();
     }
+
+    System.out.println("\n");
   }
 
   /**
@@ -137,13 +176,14 @@ public class PairwiseAlignPanel extends GPairwiseAlignPanel
    * @param e
    *          DOCUMENT ME!
    */
+  @Override
   protected void viewInEditorButton_actionPerformed(ActionEvent e)
   {
-    Sequence[] seq = new Sequence[sequences.size()];
+    SequenceI[] seq = new SequenceI[sequences.size()];
 
     for (int i = 0; i < sequences.size(); i++)
     {
-      seq[i] = (Sequence) sequences.elementAt(i);
+      seq[i] = sequences.elementAt(i);
     }
 
     AlignFrame af = new AlignFrame(new Alignment(seq),
index 846ba64..ed3d29a 100644 (file)
@@ -34,7 +34,6 @@ import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.PDBEntry;
-import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
@@ -50,6 +49,7 @@ import jalview.schemes.PIDColourScheme;
 import jalview.util.GroupUrlLink;
 import jalview.util.GroupUrlLink.UrlStringTooLongException;
 import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 import jalview.util.UrlLink;
 
 import java.awt.Color;
@@ -176,25 +176,31 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
    * Creates a new PopupMenu object.
    * 
    * @param ap
-   *          DOCUMENT ME!
    * @param seq
-   *          DOCUMENT ME!
+   * @param features
+   *          non-positional features (for seq not null), or positional features
+   *          at residue (for seq equal to null)
    */
-  public PopupMenu(final AlignmentPanel ap, Sequence seq,
-          List<String> links)
+  public PopupMenu(final AlignmentPanel ap, SequenceI seq,
+          List<SequenceFeature> features)
   {
-    this(ap, seq, links, null);
+    this(ap, seq, features, null);
   }
 
   /**
+   * Constructor
    * 
-   * @param ap
+   * @param alignPanel
    * @param seq
-   * @param links
+   *          the sequence under the cursor if in the Id panel, null if in the
+   *          sequence panel
+   * @param features
+   *          non-positional features if in the Id panel, features at the
+   *          clicked residue if in the sequence panel
    * @param groupLinks
    */
-  public PopupMenu(final AlignmentPanel ap, final SequenceI seq,
-          List<String> links, List<String> groupLinks)
+  public PopupMenu(final AlignmentPanel alignPanel, final SequenceI seq,
+          List<SequenceFeature> features, List<String> groupLinks)
   {
     // /////////////////////////////////////////////////////////
     // If this is activated from the sequence panel, the user may want to
@@ -202,7 +208,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     //
     // If from the IDPanel, we must display the sequence menu
     // ////////////////////////////////////////////////////////
-    this.ap = ap;
+    this.ap = alignPanel;
     sequence = seq;
 
     for (String ff : FileFormats.getInstance().getWritableFormats(true))
@@ -237,9 +243,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     /*
      * And repeat for the current selection group (if there is one):
      */
-    final List<SequenceI> selectedGroup = (ap.av.getSelectionGroup() == null
+    final List<SequenceI> selectedGroup = (alignPanel.av.getSelectionGroup() == null
             ? Collections.<SequenceI> emptyList()
-            : ap.av.getSelectionGroup().getSequences());
+            : alignPanel.av.getSelectionGroup().getSequences());
     buildAnnotationTypesMenus(groupShowAnnotationsMenu,
             groupHideAnnotationsMenu, selectedGroup);
     configureReferenceAnnotationsMenu(groupAddReferenceAnnotations,
@@ -257,7 +263,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     if (seq != null)
     {
       sequenceMenu.setText(sequence.getName());
-      if (seq == ap.av.getAlignment().getSeqrep())
+      if (seq == alignPanel.av.getAlignment().getSeqrep())
       {
         makeReferenceSeq.setText(
                 MessageManager.getString("action.unmark_as_reference"));
@@ -268,7 +274,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 MessageManager.getString("action.set_as_reference"));
       }
 
-      if (!ap.av.getAlignment().isNucleotide())
+      if (!alignPanel.av.getAlignment().isNucleotide())
       {
         remove(rnaStructureMenu);
       }
@@ -279,7 +285,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
          * add menu items to 2D-render any alignment or sequence secondary
          * structure annotation
          */
-        AlignmentAnnotation[] aas = ap.av.getAlignment()
+        AlignmentAnnotation[] aas = alignPanel.av.getAlignment()
                 .getAlignmentAnnotation();
         if (aas != null)
         {
@@ -299,7 +305,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 @Override
                 public void actionPerformed(ActionEvent e)
                 {
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -328,7 +334,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
                 public void actionPerformed(ActionEvent e)
                 {
                   // TODO: VARNA does'nt print gaps in the sequence
-                  new AppVarna(seq, aa, ap);
+                  new AppVarna(seq, aa, alignPanel);
                 }
               });
               rnaStructureMenu.add(menuItem);
@@ -353,8 +359,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       });
       add(menuItem);
 
-      if (ap.av.getSelectionGroup() != null
-              && ap.av.getSelectionGroup().getSize() > 1)
+      if (alignPanel.av.getSelectionGroup() != null
+              && alignPanel.av.getSelectionGroup().getSize() > 1)
       {
         menuItem = new JMenuItem(MessageManager
                 .formatMessage("label.represent_group_with", new Object[]
@@ -370,12 +376,12 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         sequenceMenu.add(menuItem);
       }
 
-      if (ap.av.hasHiddenRows())
+      if (alignPanel.av.hasHiddenRows())
       {
-        final int index = ap.av.getAlignment().findIndex(seq);
+        final int index = alignPanel.av.getAlignment().findIndex(seq);
 
-        if (ap.av.adjustForHiddenSeqs(index)
-                - ap.av.adjustForHiddenSeqs(index - 1) > 1)
+        if (alignPanel.av.adjustForHiddenSeqs(index)
+                - alignPanel.av.adjustForHiddenSeqs(index - 1) > 1)
         {
           menuItem = new JMenuItem(
                   MessageManager.getString("action.reveal_sequences"));
@@ -384,10 +390,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
             @Override
             public void actionPerformed(ActionEvent e)
             {
-              ap.av.showSequence(index);
-              if (ap.overviewPanel != null)
+              alignPanel.av.showSequence(index);
+              if (alignPanel.overviewPanel != null)
               {
-                ap.overviewPanel.updateOverviewImage();
+                alignPanel.overviewPanel.updateOverviewImage();
               }
             }
           });
@@ -396,7 +402,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
     // for the case when no sequences are even visible
-    if (ap.av.hasHiddenRows())
+    if (alignPanel.av.hasHiddenRows())
     {
       {
         menuItem = new JMenuItem(
@@ -406,10 +412,10 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
           @Override
           public void actionPerformed(ActionEvent e)
           {
-            ap.av.showAllHiddenSeqs();
-            if (ap.overviewPanel != null)
+            alignPanel.av.showAllHiddenSeqs();
+            if (alignPanel.overviewPanel != null)
             {
-              ap.overviewPanel.updateOverviewImage();
+              alignPanel.overviewPanel.updateOverviewImage();
             }
           }
         });
@@ -418,9 +424,9 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
     }
 
-    SequenceGroup sg = ap.av.getSelectionGroup();
+    SequenceGroup sg = alignPanel.av.getSelectionGroup();
     boolean isDefinedGroup = (sg != null)
-            ? ap.av.getAlignment().getGroups().contains(sg)
+            ? alignPanel.av.getAlignment().getGroups().contains(sg)
             : false;
 
     if (sg != null && sg.getSize() > 0)
@@ -458,7 +464,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       Hashtable<String, PDBEntry> pdbe = new Hashtable<>(), reppdb = new Hashtable<>();
 
       SequenceI sqass = null;
-      for (SequenceI sq : ap.av.getSequenceSelection())
+      for (SequenceI sq : alignPanel.av.getSequenceSelection())
       {
         Vector<PDBEntry> pes = sq.getDatasetSequence().getAllPDBEntries();
         if (pes != null && pes.size() > 0)
@@ -508,24 +514,133 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       rnaStructureMenu.setVisible(false);
     }
 
-    if (links != null && links.size() > 0)
+    addLinks(seq, features);
+
+    if (seq == null)
+    {
+      addFeatureDetails(features);
+    }
+  }
+
+  /**
+   * Add a link to show feature details for each sequence feature
+   * 
+   * @param features
+   */
+  protected void addFeatureDetails(List<SequenceFeature> features)
+  {
+    if (features == null || features.isEmpty())
     {
-      addFeatureLinks(seq, links);
+      return;
     }
+    JMenu details = new JMenu(
+            MessageManager.getString("label.feature_details"));
+    add(details);
+
+    for (final SequenceFeature sf : features)
+    {
+      int start = sf.getBegin();
+      int end = sf.getEnd();
+      String desc = null;
+      if (start == end)
+      {
+        desc = String.format("%s %d", sf.getType(), start);
+      }
+      else
+      {
+        desc = String.format("%s %d-%d", sf.getType(), start, end);
+      }
+      String tooltip = desc;
+      String description = sf.getDescription();
+      if (description != null)
+      {
+        description = StringUtils.stripHtmlTags(description);
+        if (description.length() > 12)
+        {
+          desc = desc + " " + description.substring(0, 12) + "..";
+        }
+        else
+        {
+          desc = desc + " " + description;
+        }
+        tooltip = tooltip + " " + description;
+      }
+      if (sf.getFeatureGroup() != null)
+      {
+        tooltip = tooltip + (" (" + sf.getFeatureGroup() + ")");
+      }
+      JMenuItem item = new JMenuItem(desc);
+      item.setToolTipText(tooltip);
+      item.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent e)
+        {
+          showFeatureDetails(sf);
+        }
+      });
+      details.add(item);
+    }
+  }
+
+  /**
+   * Opens a panel showing a text report of feature dteails
+   * 
+   * @param sf
+   */
+  protected void showFeatureDetails(SequenceFeature sf)
+  {
+    CutAndPasteHtmlTransfer cap = new CutAndPasteHtmlTransfer();
+    // it appears Java's CSS does not support border-collaps :-(
+    cap.addStylesheetRule("table { border-collapse: collapse;}");
+    cap.addStylesheetRule("table, td, th {border: 1px solid black;}");
+    cap.setText(sf.getDetailsReport());
+
+    Desktop.addInternalFrame(cap,
+            MessageManager.getString("label.feature_details"), 500, 500);
   }
 
   /**
    * Adds a 'Link' menu item with a sub-menu item for each hyperlink provided.
+   * When seq is not null, these are links for the sequence id, which may be to
+   * external web sites for the sequence accession, and/or links embedded in
+   * non-positional features. When seq is null, only links embedded in the
+   * provided features are added.
    * 
    * @param seq
-   * @param links
+   * @param features
    */
-  void addFeatureLinks(final SequenceI seq, List<String> links)
+  void addLinks(final SequenceI seq, List<SequenceFeature> features)
   {
     JMenu linkMenu = new JMenu(MessageManager.getString("action.link"));
+
+    List<String> nlinks = null;
+    if (seq != null)
+    {
+      nlinks = Preferences.sequenceUrlLinks.getLinksForMenu();
+    }
+    else
+    {
+      nlinks = new ArrayList<>();
+    }
+
+    if (features != null)
+    {
+      for (SequenceFeature sf : features)
+      {
+        if (sf.links != null)
+        {
+          for (String link : sf.links)
+          {
+            nlinks.add(link);
+          }
+        }
+      }
+    }
+
     Map<String, List<String>> linkset = new LinkedHashMap<>();
 
-    for (String link : links)
+    for (String link : nlinks)
     {
       UrlLink urlLink = null;
       try
@@ -548,25 +663,18 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
     addshowLinks(linkMenu, linkset.values());
 
-    // disable link menu if there are no valid entries
+    // only add link menu if it has entries
     if (linkMenu.getItemCount() > 0)
     {
-      linkMenu.setEnabled(true);
-    }
-    else
-    {
-      linkMenu.setEnabled(false);
-    }
-
-    if (sequence != null)
-    {
-      sequenceMenu.add(linkMenu);
-    }
-    else
-    {
-      add(linkMenu);
+      if (sequence != null)
+      {
+        sequenceMenu.add(linkMenu);
+      }
+      else
+      {
+        add(linkMenu);
+      }
     }
-
   }
 
   /**
@@ -1453,15 +1561,8 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
 
   protected void hideInsertions_actionPerformed(ActionEvent actionEvent)
   {
-
-    HiddenColumns hidden = new HiddenColumns();
-    BitSet inserts = new BitSet(), mask = new BitSet();
-
-    // set mask to preserve existing hidden columns outside selected group
-    if (ap.av.hasHiddenColumns())
-    {
-      ap.av.getAlignment().getHiddenColumns().markHiddenRegions(mask);
-    }
+    HiddenColumns hidden = ap.av.getAlignment().getHiddenColumns();
+    BitSet inserts = new BitSet();
 
     boolean markedPopup = false;
     // mark inserts in current selection
@@ -1469,10 +1570,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
     {
       // mark just the columns in the selection group to be hidden
       inserts.set(ap.av.getSelectionGroup().getStartRes(),
-              ap.av.getSelectionGroup().getEndRes() + 1);
-
-      // and clear that part of the mask
-      mask.andNot(inserts);
+              ap.av.getSelectionGroup().getEndRes() + 1); // TODO why +1?
 
       // now clear columns without gaps
       for (SequenceI sq : ap.av.getSelectionGroup().getSequences())
@@ -1483,29 +1581,18 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
         }
         inserts.and(sq.getInsertionsAsBits());
       }
-    }
-    else
-    {
-      // initially, mark all columns to be hidden
-      inserts.set(0, ap.av.getAlignment().getWidth());
-
-      // and clear out old hidden regions completely
-      mask.clear();
+      hidden.clearAndHideColumns(inserts, ap.av.getSelectionGroup().getStartRes(),
+              ap.av.getSelectionGroup().getEndRes());
     }
 
     // now mark for sequence under popup if we haven't already done it
-    if (!markedPopup && sequence != null)
+    else if (!markedPopup && sequence != null)
     {
-      inserts.and(sequence.getInsertionsAsBits());
-    }
-
-    // finally, preserve hidden regions outside selection
-    inserts.or(mask);
+      inserts.or(sequence.getInsertionsAsBits());
 
-    // and set hidden columns accordingly
-    hidden.hideMarkedBits(inserts);
-
-    ap.av.getAlignment().setHiddenColumns(hidden);
+      // and set hidden columns accordingly
+      hidden.hideColumns(inserts);
+    }
     refresh();
   }
 
@@ -1530,10 +1617,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
               new Object[]
               { seq.getDisplayId(true) }) + "</h2></p><p>");
       new SequenceAnnotationReport(null).createSequenceAnnotationReport(
-              contents, seq, true, true,
-              (ap.getSeqPanel().seqCanvas.fr != null)
-                      ? ap.getSeqPanel().seqCanvas.fr.getMinMax()
-                      : null);
+              contents, seq, true, true, ap.getSeqPanel().seqCanvas.fr);
       contents.append("</p>");
     }
     cap.setText("<html>" + contents.toString() + "</html>");
@@ -1743,7 +1827,7 @@ public class PopupMenu extends JPopupMenu implements ColourChangeListener
       }
 
       sequence.setName(dialog.getName().replace(' ', '_'));
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
 
     sequence.setDescription(dialog.getDescription());
index 6635dbe..5aab26d 100755 (executable)
@@ -525,8 +525,10 @@ public class Preferences extends GPreferences
     autoIdWidth.setSelected(Cache.getDefault("FIGURE_AUTOIDWIDTH", false));
     userIdWidth.setEnabled(!autoIdWidth.isSelected());
     userIdWidthlabel.setEnabled(!autoIdWidth.isSelected());
-    Integer wi = Cache.getIntegerProperty("FIGURE_USERIDWIDTH");
+    Integer wi = Cache.getIntegerProperty("FIGURE_FIXEDIDWIDTH");
     userIdWidth.setText(wi == null ? "" : wi.toString());
+    // TODO: refactor to use common enum via FormatAdapter and allow extension
+    // for new flat file formats
     blcjv.setSelected(Cache.getDefault("BLC_JVSUFFIX", true));
     clustaljv.setSelected(Cache.getDefault("CLUSTAL_JVSUFFIX", true));
     fastajv.setSelected(Cache.getDefault("FASTA_JVSUFFIX", true));
@@ -782,7 +784,7 @@ public class Preferences extends GPreferences
     Cache.applicationProperties.setProperty("FIGURE_AUTOIDWIDTH",
             Boolean.toString(autoIdWidth.isSelected()));
     userIdWidth_actionPerformed();
-    Cache.applicationProperties.setProperty("FIGURE_USERIDWIDTH",
+    Cache.applicationProperties.setProperty("FIGURE_FIXEDIDWIDTH",
             userIdWidth.getText());
 
     /*
index ea341e3..011d810 100644 (file)
@@ -89,8 +89,8 @@ public class ProgressBar implements IProgressIndicator
     }
     this.statusPanel = container;
     this.statusBar = statusBar;
-    this.progressBars = new Hashtable<Long, JPanel>();
-    this.progressBarHandlers = new Hashtable<Long, IProgressIndicatorHandler>();
+    this.progressBars = new Hashtable<>();
+    this.progressBarHandlers = new Hashtable<>();
 
   }
 
@@ -119,46 +119,52 @@ public class ProgressBar implements IProgressIndicator
    * execution.
    */
   @Override
-  public void setProgressBar(String message, long id)
+  public void setProgressBar(final String message, final long id)
   {
-    Long longId = Long.valueOf(id);
-
-    JPanel progressPanel = progressBars.get(longId);
-    if (progressPanel != null)
+    SwingUtilities.invokeLater(new Runnable()
     {
-      /*
-       * Progress bar is displayed for this id - remove it now, and any handler
-       */
-      progressBars.remove(id);
-      if (message != null && statusBar != null)
-      {
-        statusBar.setText(message);
-      }
-      if (progressBarHandlers.containsKey(longId))
+      @Override
+      public void run()
       {
-        progressBarHandlers.remove(longId);
-      }
-      removeRow(progressPanel);
-    }
-    else
-    {
-      /*
-       * No progress bar for this id - add one now
-       */
-      progressPanel = new JPanel(new BorderLayout(10, 5));
+        JPanel progressPanel = progressBars.get(id);
+        if (progressPanel != null)
+        {
+          /*
+           * Progress bar is displayed for this id - remove it now, and any handler
+           */
+          progressBars.remove(id);
+          if (message != null && statusBar != null)
+          {
+            statusBar.setText(message);
+          }
+          if (progressBarHandlers.containsKey(id))
+          {
+            progressBarHandlers.remove(id);
+          }
+          removeRow(progressPanel);
+        }
+        else
+        {
+          /*
+           * No progress bar for this id - add one now
+           */
+          progressPanel = new JPanel(new BorderLayout(10, 5));
 
-      JProgressBar progressBar = new JProgressBar();
-      progressBar.setIndeterminate(true);
+          JProgressBar progressBar = new JProgressBar();
+          progressBar.setIndeterminate(true);
 
-      progressPanel.add(new JLabel(message), BorderLayout.WEST);
-      progressPanel.add(progressBar, BorderLayout.CENTER);
+          progressPanel.add(new JLabel(message), BorderLayout.WEST);
+          progressPanel.add(progressBar, BorderLayout.CENTER);
 
-      addRow(progressPanel);
+          addRow(progressPanel);
 
-      progressBars.put(longId, progressPanel);
-    }
+          progressBars.put(id, progressPanel);
+        }
+
+        refreshLayout();
+      }
+    });
 
-    refreshLayout();
   }
 
   /**
@@ -215,41 +221,50 @@ public class ProgressBar implements IProgressIndicator
   public void registerHandler(final long id,
           final IProgressIndicatorHandler handler)
   {
-    Long longId = Long.valueOf(id);
-    final JPanel progressPanel = progressBars.get(longId);
-    if (progressPanel == null)
-    {
-      System.err.println(
-              "call setProgressBar before registering the progress bar's handler.");
-      return;
-    }
-
-    /*
-     * Nothing useful to do if not a Cancel handler
-     */
-    if (!handler.canCancel())
-    {
-      return;
-    }
-
-    progressBarHandlers.put(longId, handler);
-    JButton cancel = new JButton(MessageManager.getString("action.cancel"));
     final IProgressIndicator us = this;
-    cancel.addActionListener(new ActionListener()
-    {
 
+    SwingUtilities.invokeLater(new Runnable()
+    {
       @Override
-      public void actionPerformed(ActionEvent e)
+      public void run()
       {
-        handler.cancelActivity(id);
-        us.setProgressBar(MessageManager
-                .formatMessage("label.cancelled_params", new Object[]
-                { ((JLabel) progressPanel.getComponent(0)).getText() }),
-                id);
+        final JPanel progressPanel = progressBars.get(id);
+        if (progressPanel == null)
+        {
+          System.err.println(
+                  "call setProgressBar before registering the progress bar's handler.");
+          return;
+        }
+
+        /*
+         * Nothing useful to do if not a Cancel handler
+         */
+        if (!handler.canCancel())
+        {
+          return;
+        }
+
+        progressBarHandlers.put(id, handler);
+        JButton cancel = new JButton(
+                MessageManager.getString("action.cancel"));
+        cancel.addActionListener(new ActionListener()
+        {
+
+          @Override
+          public void actionPerformed(ActionEvent e)
+          {
+            handler.cancelActivity(id);
+            us.setProgressBar(MessageManager
+                    .formatMessage("label.cancelled_params", new Object[]
+                    { ((JLabel) progressPanel.getComponent(0)).getText() }),
+                    id);
+          }
+        });
+        progressPanel.add(cancel, BorderLayout.EAST);
+        refreshLayout();
+
       }
     });
-    progressPanel.add(cancel, BorderLayout.EAST);
-    refreshLayout();
   }
 
 }
index 6261015..cb59452 100644 (file)
@@ -24,8 +24,6 @@ import jalview.bin.Cache;
 
 import java.awt.Component;
 
-import javax.swing.JOptionPane;
-
 public class PromptUserConfig implements Runnable
 {
   /**
@@ -120,6 +118,7 @@ public class PromptUserConfig implements Runnable
     this.allowCancel = allowCancel;
   }
 
+  @Override
   public void run()
   {
     if (property == null)
@@ -206,12 +205,7 @@ public class PromptUserConfig implements Runnable
               (allowCancel) ? JvOptionPane.YES_NO_CANCEL_OPTION
                       : JvOptionPane.YES_NO_OPTION,
               JvOptionPane.QUESTION_MESSAGE);
-      // now, ask the desktop to relayer any external windows that might have
-      // been obsured
-      if (Desktop.instance != null)
-      {
-        Desktop.instance.relayerWindows();
-      }
+
       // and finish parsing the result
       jalview.bin.Cache.log.debug("Got response : " + reply);
       if (reply == JvOptionPane.YES_OPTION)
index 8bf2fba..c4390c0 100755 (executable)
@@ -54,7 +54,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
 
   AlignmentPanel ap;
 
-  Stack<CommandI> historyList = new Stack<CommandI>();
+  Stack<CommandI> historyList = new Stack<>();
 
   // simpler than synching with alignFrame.
 
@@ -194,7 +194,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
     }
 
     float value = slider.getValue();
-    List<SequenceI> redundantSequences = new ArrayList<SequenceI>();
+    List<SequenceI> redundantSequences = new ArrayList<>();
     for (int i = 0; i < redundancy.length; i++)
     {
       if (value <= redundancy[i])
@@ -295,7 +295,7 @@ public class RedundancyPanel extends GSliderPanel implements Runnable
       af.updateEditMenuBar();
     }
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
 
     if (historyList.size() == 0)
     {
index 4ef18d4..02368df 100755 (executable)
@@ -128,7 +128,6 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
   boolean applyToAllViews = false;
 
-  // Controller controller;
   public RotatableCanvas(AlignmentPanel ap)
   {
     this.av = ap.av;
@@ -136,16 +135,23 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
     addMouseWheelListener(new MouseWheelListener()
     {
+      @Override
       public void mouseWheelMoved(MouseWheelEvent e)
       {
-        if (e.getWheelRotation() > 0)
+        double wheelRotation = e.getPreciseWheelRotation();
+        if (wheelRotation > 0)
         {
+          /*
+           * zoom in
+           */
           scale = (float) (scale * 1.1);
           repaint();
         }
-
-        else
+        else if (wheelRotation < 0)
         {
+          /*
+           * zoom out
+           */
           scale = (float) (scale * 0.9);
           repaint();
         }
@@ -162,6 +168,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
 
   boolean first = true;
 
+  @Override
   public void setPoints(Vector points, int npoint)
   {
     this.points = points;
@@ -327,7 +334,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
       dim = height;
     }
 
-    return (float) ((dim * scalefactor) / (2 * maxwidth));
+    return (dim * scalefactor) / (2 * maxwidth);
   }
 
   /**
@@ -352,6 +359,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Dimension getPreferredSize()
   {
     if (prefsize != null)
@@ -369,6 +377,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * 
    * @return DOCUMENT ME!
    */
+  @Override
   public Dimension getMinimumSize()
   {
     return getPreferredSize();
@@ -380,6 +389,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param g
    *          DOCUMENT ME!
    */
+  @Override
   public void paintComponent(Graphics g1)
   {
 
@@ -475,8 +485,8 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     for (int i = 0; i < npoint; i++)
     {
       SequencePoint sp = (SequencePoint) points.elementAt(i);
-      int x = (int) ((float) (sp.coord[0] - centre[0]) * scale) + halfwidth;
-      int y = (int) ((float) (sp.coord[1] - centre[1]) * scale)
+      int x = (int) ((sp.coord[0] - centre[0]) * scale) + halfwidth;
+      int y = (int) ((sp.coord[1] - centre[1]) * scale)
               + halfheight;
       float z = sp.coord[1] - centre[2];
 
@@ -547,6 +557,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void keyTyped(KeyEvent evt)
   {
   }
@@ -557,6 +568,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void keyReleased(KeyEvent evt)
   {
   }
@@ -567,6 +579,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void keyPressed(KeyEvent evt)
   {
     if (evt.getKeyCode() == KeyEvent.VK_UP)
@@ -598,6 +611,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseClicked(MouseEvent evt)
   {
   }
@@ -608,6 +622,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseEntered(MouseEvent evt)
   {
   }
@@ -618,6 +633,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseExited(MouseEvent evt)
   {
   }
@@ -628,6 +644,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseReleased(MouseEvent evt)
   {
   }
@@ -638,6 +655,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mousePressed(MouseEvent evt)
   {
     int x = evt.getX();
@@ -690,6 +708,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
   // controller.handleSequenceSelectionEvent(new
   // SequenceSelectionEvent(this,sel));
   // }
+  @Override
   public void mouseMoved(MouseEvent evt)
   {
     SequenceI found = findPoint(evt.getX(), evt.getY());
@@ -710,6 +729,7 @@ public class RotatableCanvas extends JPanel implements MouseListener,
    * @param evt
    *          DOCUMENT ME!
    */
+  @Override
   public void mouseDragged(MouseEvent evt)
   {
     mx = evt.getX();
@@ -725,8 +745,8 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     {
       rotmat.setIdentity();
 
-      rotmat.rotate((float) (my - omy), 'x');
-      rotmat.rotate((float) (mx - omx), 'y');
+      rotmat.rotate(my - omy, 'x');
+      rotmat.rotate(mx - omx, 'y');
 
       for (int i = 0; i < npoint; i++)
       {
@@ -774,9 +794,9 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     {
       SequencePoint sp = (SequencePoint) points.elementAt(i);
       int tmp1 = (int) (((sp.coord[0] - centre[0]) * scale)
-              + ((float) getWidth() / 2.0));
+              + (getWidth() / 2.0));
       int tmp2 = (int) (((sp.coord[1] - centre[1]) * scale)
-              + ((float) getHeight() / 2.0));
+              + (getHeight() / 2.0));
 
       if ((tmp1 > x1) && (tmp1 < x2) && (tmp2 > y1) && (tmp2 < y2))
       {
@@ -816,9 +836,9 @@ public class RotatableCanvas extends JPanel implements MouseListener,
     for (int i = 0; i < npoint; i++)
     {
       SequencePoint sp = (SequencePoint) points.elementAt(i);
-      int px = (int) ((float) (sp.coord[0] - centre[0]) * scale)
+      int px = (int) ((sp.coord[0] - centre[0]) * scale)
               + halfwidth;
-      int py = (int) ((float) (sp.coord[1] - centre[1]) * scale)
+      int py = (int) ((sp.coord[1] - centre[1]) * scale)
               + halfheight;
 
       if ((Math.abs(px - x) < 3) && (Math.abs(py - y) < 3))
index 1db4051..e6bba02 100755 (executable)
@@ -42,6 +42,7 @@ import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.beans.PropertyChangeEvent;
+import java.util.Iterator;
 import java.util.List;
 
 import javax.swing.JMenuItem;
@@ -112,7 +113,7 @@ public class ScalePanel extends JPanel
 
     if (av.hasHiddenColumns())
     {
-      x = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(x);
+      x = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(x);
     }
 
     if (x >= av.getAlignment().getWidth())
@@ -168,13 +169,13 @@ public class ScalePanel extends JPanel
         {
           av.showColumn(reveal[0]);
           reveal = null;
-          ap.paintAlignment(true);
+          ap.paintAlignment(true, true);
           av.sendSelection();
         }
       });
       pop.add(item);
 
-      if (av.getAlignment().getHiddenColumns().hasHiddenColumns())
+      if (av.getAlignment().getHiddenColumns().hasMultiHiddenColumnRegions())
       {
         item = new JMenuItem(MessageManager.getString("action.reveal_all"));
         item.addActionListener(new ActionListener()
@@ -184,7 +185,7 @@ public class ScalePanel extends JPanel
           {
             av.showAllHiddenColumns();
             reveal = null;
-            ap.paintAlignment(true);
+            ap.paintAlignment(true, true);
             av.sendSelection();
           }
         });
@@ -208,7 +209,7 @@ public class ScalePanel extends JPanel
             av.setSelectionGroup(null);
           }
 
-          ap.paintAlignment(true);
+          ap.paintAlignment(true, true);
           av.sendSelection();
         }
       });
@@ -260,7 +261,7 @@ public class ScalePanel extends JPanel
       sg.setEndRes(max);
     }
     av.setSelectionGroup(sg);
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -281,7 +282,7 @@ public class ScalePanel extends JPanel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     if (res >= av.getAlignment().getWidth())
@@ -297,7 +298,7 @@ public class ScalePanel extends JPanel
       }
       else
       {
-        ap.paintAlignment(false);
+        ap.paintAlignment(false, false);
       }
       return;
     }
@@ -316,7 +317,7 @@ public class ScalePanel extends JPanel
       }
     }
     stretchingGroup = false;
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -336,7 +337,7 @@ public class ScalePanel extends JPanel
     int res = (evt.getX() / av.getCharWidth())
             + av.getRanges().getStartRes();
     res = Math.max(0, res);
-    res = hidden.adjustForHiddenColumns(res);
+    res = hidden.visibleToAbsoluteColumn(res);
     res = Math.min(res, av.getAlignment().getWidth() - 1);
     min = Math.min(res, min);
     max = Math.max(res, max);
@@ -346,7 +347,7 @@ public class ScalePanel extends JPanel
     {
       stretchingGroup = true;
       cs.stretchGroup(res, sg, min, max);
-      ap.paintAlignment(false);
+      ap.paintAlignment(false, false);
     }
   }
 
@@ -392,7 +393,7 @@ public class ScalePanel extends JPanel
     reveal = av.getAlignment().getHiddenColumns()
             .getRegionWithEdgeAtRes(res);
 
-    res = av.getAlignment().getHiddenColumns().adjustForHiddenColumns(res);
+    res = av.getAlignment().getHiddenColumns().visibleToAbsoluteColumn(res);
 
     ToolTipManager.sharedInstance().registerComponent(this);
     this.setToolTipText(
@@ -409,6 +410,8 @@ public class ScalePanel extends JPanel
   @Override
   public void paintComponent(Graphics g)
   {
+    super.paintComponent(g);
+
     /*
      * shouldn't get called in wrapped mode as the scale above is
      * drawn instead by SeqCanvas.drawNorthScale
@@ -457,7 +460,7 @@ public class ScalePanel extends JPanel
         {
           if (hidden.isVisible(sel))
           {
-            sel = hidden.findColumnPosition(sel);
+            sel = hidden.absoluteToVisibleColumn(sel);
           }
           else
           {
@@ -487,23 +490,18 @@ public class ScalePanel extends JPanel
 
       if (av.getShowHiddenMarkers())
       {
-        List<Integer> positions = hidden.findHiddenRegionPositions();
-        for (int pos : positions)
+        Iterator<Integer> it = hidden.getStartRegionIterator(startx,
+                startx + widthx + 1);
+        while (it.hasNext())
         {
-          res = pos - startx;
-
-          if (res < 0 || res > widthx)
-          {
-            continue;
-          }
+          res = it.next() - startx;
 
           gg.fillPolygon(
                   new int[]
-                  { -1 + res * avCharWidth - avCharHeight / 4,
-                      -1 + res * avCharWidth + avCharHeight / 4,
-                      -1 + res * avCharWidth },
-                  new int[]
-                  { y, y, y + 2 * yOf }, 3);
+          { -1 + res * avCharWidth - avCharHeight / 4,
+              -1 + res * avCharWidth + avCharHeight / 4,
+              -1 + res * avCharWidth }, new int[]
+          { y, y, y + 2 * yOf }, 3);
         }
       }
     }
@@ -549,10 +547,16 @@ public class ScalePanel extends JPanel
     // Here we only want to fastpaint on a scroll, with resize using a normal
     // paint, so scroll events are identified as changes to the horizontal or
     // vertical start value.
-    if (evt.getPropertyName().equals(ViewportRanges.STARTRES))
+    if (evt.getPropertyName().equals(ViewportRanges.STARTRES)
+            || evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ)
+            || evt.getPropertyName().equals(ViewportRanges.MOVE_VIEWPORT))
     {
       // scroll event, repaint panel
-      repaint();
+       
+       // Call repaint on alignment panel so that repaints from other alignment
+    // panel components can be aggregated. Otherwise performance of the overview
+    // window and others may be adversely affected.
+      av.getAlignPanel().repaint();
     }
   }
 
index 4e896a0..8f315bd 100755 (executable)
@@ -25,8 +25,10 @@ import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.VisibleContigsIterator;
 import jalview.renderer.ScaleRenderer;
 import jalview.renderer.ScaleRenderer.ScaleMark;
+import jalview.util.Comparison;
 import jalview.viewmodel.ViewportListenerI;
 import jalview.viewmodel.ViewportRanges;
 
@@ -41,58 +43,62 @@ import java.awt.RenderingHints;
 import java.awt.Shape;
 import java.awt.image.BufferedImage;
 import java.beans.PropertyChangeEvent;
+import java.util.Iterator;
 import java.util.List;
 
 import javax.swing.JComponent;
 
 /**
- * DOCUMENT ME!
+ * The Swing component on which the alignment sequences, and annotations (if
+ * shown), are drawn. This includes scales above, left and right (if shown) in
+ * Wrapped mode, but not the scale above in Unwrapped mode.
  * 
- * @author $author$
- * @version $Revision$
  */
 public class SeqCanvas extends JComponent implements ViewportListenerI
 {
-  private static String ZEROS = "0000000000";
+  private static final String ZEROS = "0000000000";
 
   final FeatureRenderer fr;
 
-  final SequenceRenderer seqRdr;
-
   BufferedImage img;
 
-  Graphics2D gg;
-
   AlignViewport av;
 
-  boolean fastPaint = false;
+  int cursorX = 0;
+
+  int cursorY = 0;
 
-  int labelWidthWest;
+  private final SequenceRenderer seqRdr;
 
-  int labelWidthEast;
+  private boolean fastPaint = false;
 
-  int cursorX = 0;
+  private boolean fastpainting = false;
 
-  int cursorY = 0;
+  private AnnotationPanel annotations;
+
+  /*
+   * measurements for drawing a wrapped alignment
+   */
+  private int labelWidthEast; // label right width in pixels if shown
 
-  int charHeight = 0;
+  private int labelWidthWest; // label left width in pixels if shown
 
-  int charWidth = 0;
+  private int wrappedSpaceAboveAlignment; // gap between widths
 
-  boolean fastpainting = false;
+  private int wrappedRepeatHeightPx; // height in pixels of wrapped width
 
-  AnnotationPanel annotations;
+  private int wrappedVisibleWidths; // number of wrapped widths displayed
+
+  private Graphics2D gg;
 
   /**
    * Creates a new SeqCanvas object.
    * 
-   * @param av
-   *          DOCUMENT ME!
+   * @param ap
    */
   public SeqCanvas(AlignmentPanel ap)
   {
     this.av = ap.av;
-    updateViewport();
     fr = new FeatureRenderer(ap);
     seqRdr = new SequenceRenderer(av);
     setLayout(new BorderLayout());
@@ -112,29 +118,36 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     return fr;
   }
 
-  private void updateViewport()
-  {
-    charHeight = av.getCharHeight();
-    charWidth = av.getCharWidth();
-  }
-
   /**
-   * DOCUMENT ME!
+   * Draws the scale above a region of a wrapped alignment, consisting of a
+   * column number every major interval (10 columns).
    * 
    * @param g
-   *          DOCUMENT ME!
+   *          the graphics context to draw on, positioned at the start (bottom
+   *          left) of the line on which to draw any scale marks
    * @param startx
-   *          DOCUMENT ME!
+   *          start alignment column (0..)
    * @param endx
-   *          DOCUMENT ME!
+   *          end alignment column (0..)
    * @param ypos
-   *          DOCUMENT ME!
+   *          y offset to draw at
    */
   private void drawNorthScale(Graphics g, int startx, int endx, int ypos)
   {
-    updateViewport();
-    for (ScaleMark mark : new ScaleRenderer().calculateMarks(av, startx,
-            endx))
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+
+    /*
+     * white fill the scale space (for the fastPaint case)
+     */
+    g.setColor(Color.white);
+    g.fillRect(0, ypos - charHeight - charHeight / 2, getWidth(),
+            charHeight * 3 / 2 + 2);
+    g.setColor(Color.black);
+
+    List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, startx,
+            endx);
+    for (ScaleMark mark : marks)
     {
       int mpos = mark.column; // (i - startx - 1)
       if (mpos < 0)
@@ -149,137 +162,119 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         {
           g.drawString(mstring, mpos * charWidth, ypos - (charHeight / 2));
         }
-        g.drawLine((mpos * charWidth) + (charWidth / 2),
-                (ypos + 2) - (charHeight / 2),
-                (mpos * charWidth) + (charWidth / 2), ypos - 2);
+
+        /*
+         * draw a tick mark below the column number, centred on the column;
+         * height of tick mark is 4 pixels less than half a character
+         */
+        int xpos = (mpos * charWidth) + (charWidth / 2);
+        g.drawLine(xpos, (ypos + 2) - (charHeight / 2), xpos, ypos - 2);
       }
     }
   }
 
   /**
-   * DOCUMENT ME!
+   * Draw the scale to the left or right of a wrapped alignment
    * 
    * @param g
-   *          DOCUMENT ME!
+   *          graphics context, positioned at the start of the scale to be drawn
    * @param startx
-   *          DOCUMENT ME!
+   *          first column of wrapped width (0.. excluding any hidden columns)
    * @param endx
-   *          DOCUMENT ME!
+   *          last column of wrapped width (0.. excluding any hidden columns)
    * @param ypos
-   *          DOCUMENT ME!
+   *          vertical offset at which to begin the scale
+   * @param left
+   *          if true, scale is left of residues, if false, scale is right
    */
-  void drawWestScale(Graphics g, int startx, int endx, int ypos)
+  void drawVerticalScale(Graphics g, final int startx, final int endx,
+          final int ypos, final boolean left)
   {
-    FontMetrics fm = getFontMetrics(av.getFont());
-    ypos += charHeight;
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
 
-    if (av.hasHiddenColumns())
-    {
-      startx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(startx);
-      endx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(endx);
-    }
+    int yPos = ypos + charHeight;
+    int startX = startx;
+    int endX = endx;
 
-    int maxwidth = av.getAlignment().getWidth();
     if (av.hasHiddenColumns())
     {
-      maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth) - 1;
+      HiddenColumns hiddenColumns = av.getAlignment().getHiddenColumns();
+      startX = hiddenColumns.visibleToAbsoluteColumn(startx);
+      endX = hiddenColumns.visibleToAbsoluteColumn(endx);
     }
+    FontMetrics fm = getFontMetrics(av.getFont());
 
-    // WEST SCALE
     for (int i = 0; i < av.getAlignment().getHeight(); i++)
     {
       SequenceI seq = av.getAlignment().getSequenceAt(i);
-      int index = startx;
-      int value = -1;
 
-      while (index < endx)
+      /*
+       * find sequence position of first non-gapped position -
+       * to the right if scale left, to the left if scale right
+       */
+      int index = left ? startX : endX;
+      int value = -1;
+      while (index >= startX && index <= endX)
       {
-        if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
+        if (!Comparison.isGap(seq.getCharAt(index)))
+        {
+          value = seq.findPosition(index);
+          break;
+        }
+        if (left)
         {
           index++;
-
-          continue;
         }
-
-        value = av.getAlignment().getSequenceAt(i).findPosition(index);
-
-        break;
-      }
-
-      if (value != -1)
-      {
-        int x = labelWidthWest - fm.stringWidth(String.valueOf(value))
-                - charWidth / 2;
-        g.drawString(value + "", x,
-                (ypos + (i * charHeight)) - (charHeight / 5));
-      }
-    }
-  }
-
-  /**
-   * DOCUMENT ME!
-   * 
-   * @param g
-   *          DOCUMENT ME!
-   * @param startx
-   *          DOCUMENT ME!
-   * @param endx
-   *          DOCUMENT ME!
-   * @param ypos
-   *          DOCUMENT ME!
-   */
-  void drawEastScale(Graphics g, int startx, int endx, int ypos)
-  {
-    ypos += charHeight;
-
-    if (av.hasHiddenColumns())
-    {
-      endx = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(endx);
-    }
-
-    SequenceI seq;
-    // EAST SCALE
-    for (int i = 0; i < av.getAlignment().getHeight(); i++)
-    {
-      seq = av.getAlignment().getSequenceAt(i);
-      int index = endx;
-      int value = -1;
-
-      while (index > startx)
-      {
-        if (jalview.util.Comparison.isGap(seq.getCharAt(index)))
+        else
         {
           index--;
-
-          continue;
         }
-
-        value = seq.findPosition(index);
-
-        break;
       }
 
+      /*
+       * white fill the space for the scale
+       */
+      g.setColor(Color.white);
+      int y = (yPos + (i * charHeight)) - (charHeight / 5);
+      // fillRect origin is top left of rectangle
+      g.fillRect(0, y - charHeight, left ? labelWidthWest : labelWidthEast,
+              charHeight + 1);
+
       if (value != -1)
       {
-        g.drawString(String.valueOf(value), 0,
-                (ypos + (i * charHeight)) - (charHeight / 5));
+        /*
+         * draw scale value, right justified within its width less half a
+         * character width padding on the right
+         */
+        int labelSpace = left ? labelWidthWest : labelWidthEast;
+        labelSpace -= charWidth / 2; // leave space to the right
+        String valueAsString = String.valueOf(value);
+        int labelLength = fm.stringWidth(valueAsString);
+        int xOffset = labelSpace - labelLength;
+        g.setColor(Color.black);
+        g.drawString(valueAsString, xOffset, y);
       }
     }
   }
 
-
   /**
-   * need to make this thread safe move alignment rendering in response to
-   * slider adjustment
+   * Does a fast paint of an alignment in response to a scroll. Most of the
+   * visible region is simply copied and shifted, and then any newly visible
+   * columns or rows are drawn. The scroll may be horizontal or vertical, but
+   * not both at once. Scrolling may be the result of
+   * <ul>
+   * <li>dragging a scroll bar</li>
+   * <li>clicking in the scroll bar</li>
+   * <li>scrolling by trackpad, middle mouse button, or other device</li>
+   * <li>by moving the box in the Overview window</li>
+   * <li>programmatically to make a highlighted position visible</li>
+   * </ul>
    * 
    * @param horizontal
-   *          shift along
+   *          columns to shift right (positive) or left (negative)
    * @param vertical
-   *          shift up or down in repaint
+   *          rows to shift down (positive) or up (negative)
    */
   public void fastPaint(int horizontal, int vertical)
   {
@@ -289,80 +284,92 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     }
     fastpainting = true;
     fastPaint = true;
-    updateViewport();
 
-    ViewportRanges ranges = av.getRanges();
-    int startRes = ranges.getStartRes();
-    int endRes = ranges.getEndRes();
-    int startSeq = ranges.getStartSeq();
-    int endSeq = ranges.getEndSeq();
-    int transX = 0;
-    int transY = 0;
-
-    gg.copyArea(horizontal * charWidth, vertical * charHeight,
-            img.getWidth(), img.getHeight(), -horizontal * charWidth,
-            -vertical * charHeight);
-
-    if (horizontal > 0) // scrollbar pulled right, image to the left
-    {
-      transX = (endRes - startRes - horizontal) * charWidth;
-      startRes = endRes - horizontal;
-    }
-    else if (horizontal < 0)
-    {
-      endRes = startRes - horizontal;
-    }
-    else if (vertical > 0) // scroll down
+    try
     {
-      startSeq = endSeq - vertical;
+      int charHeight = av.getCharHeight();
+      int charWidth = av.getCharWidth();
+    
+      ViewportRanges ranges = av.getRanges();
+      int startRes = ranges.getStartRes();
+      int endRes = ranges.getEndRes();
+      int startSeq = ranges.getStartSeq();
+      int endSeq = ranges.getEndSeq();
+      int transX = 0;
+      int transY = 0;
+      
+      gg.copyArea(horizontal * charWidth, vertical * charHeight,
+              img.getWidth(), img.getHeight(), -horizontal * charWidth,
+              -vertical * charHeight);
 
-      if (startSeq < ranges.getStartSeq())
-      { // ie scrolling too fast, more than a page at a time
-        startSeq = ranges.getStartSeq();
+      if (horizontal > 0) // scrollbar pulled right, image to the left
+      {
+        transX = (endRes - startRes - horizontal) * charWidth;
+        startRes = endRes - horizontal;
       }
-      else
+      else if (horizontal < 0)
       {
-        transY = img.getHeight() - ((vertical + 1) * charHeight);
+        endRes = startRes - horizontal;
       }
-    }
-    else if (vertical < 0)
-    {
-      endSeq = startSeq - vertical;
 
-      if (endSeq > ranges.getEndSeq())
+      if (vertical > 0) // scroll down
+      {
+        startSeq = endSeq - vertical;
+
+        if (startSeq < ranges.getStartSeq())
+        { // ie scrolling too fast, more than a page at a time
+          startSeq = ranges.getStartSeq();
+        }
+        else
+        {
+          transY = img.getHeight() - ((vertical + 1) * charHeight);
+        }
+      }
+      else if (vertical < 0)
       {
-        endSeq = ranges.getEndSeq();
+        endSeq = startSeq - vertical;
+
+        if (endSeq > ranges.getEndSeq())
+        {
+          endSeq = ranges.getEndSeq();
+        }
       }
-    }
 
-    gg.translate(transX, transY);
-    drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
-    gg.translate(-transX, -transY);
+      gg.translate(transX, transY);
+      drawPanel(gg, startRes, endRes, startSeq, endSeq, 0);
+      gg.translate(-transX, -transY);
 
-    repaint();
-    fastpainting = false;
+      // Call repaint on alignment panel so that repaints from other alignment
+      // panel components can be aggregated. Otherwise performance of the
+      // overview window and others may be adversely affected.
+      av.getAlignPanel().repaint();
+    } finally
+    {
+      fastpainting = false;
+    }
   }
 
   @Override
   public void paintComponent(Graphics g)
   {
-    super.paintComponent(g);
-
-    updateViewport();
-
+    super.paintComponent(g);    
+    
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    
     ViewportRanges ranges = av.getRanges();
-
+    
     int width = getWidth();
     int height = getHeight();
-
+    
     width -= (width % charWidth);
     height -= (height % charHeight);
-
+    
     // selectImage is the selection group outline image
     BufferedImage selectImage = drawSelectionGroup(
             ranges.getStartRes(), ranges.getEndRes(),
             ranges.getStartSeq(), ranges.getEndSeq());
-
+    
     if ((img != null) && (fastPaint
             || (getVisibleRect().width != g.getClipBounds().width)
             || (getVisibleRect().height != g.getClipBounds().height)))
@@ -386,16 +393,16 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         gg = (Graphics2D) img.getGraphics();
         gg.setFont(av.getFont());
       }
-
+    
       if (av.antiAlias)
       {
         gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                 RenderingHints.VALUE_ANTIALIAS_ON);
       }
-
+    
       gg.setColor(Color.white);
       gg.fillRect(0, 0, img.getWidth(), img.getHeight());
-
+    
       if (av.getWrapAlignment())
       {
         drawWrappedPanel(gg, getWidth(), getHeight(), ranges.getStartRes());
@@ -405,13 +412,20 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
         drawPanel(gg, ranges.getStartRes(), ranges.getEndRes(),
                 ranges.getStartSeq(), ranges.getEndSeq(), 0);
       }
-
+    
       // lcimg is a local *copy* of img which we'll draw selectImage on top of
       BufferedImage lcimg = buildLocalImage(selectImage);
       g.drawImage(lcimg, 0, 0, this);
+
     }
-  }
 
+    if (av.cursorMode)
+    {
+      drawCursor(g, ranges.getStartRes(), ranges.getEndRes(),
+              ranges.getStartSeq(), ranges.getEndSeq());
+    }
+  }
+  
   /**
    * Draw an alignment panel for printing
    * 
@@ -495,8 +509,11 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   private BufferedImage buildLocalImage(BufferedImage selectImage)
   {
     // clone the cached image
-    BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
-            img.getType());
+         BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
+                   img.getType());
+
+    // BufferedImage lcimg = new BufferedImage(img.getWidth(), img.getHeight(),
+    // img.getType());
     Graphics2D g2d = lcimg.createGraphics();
     g2d.drawImage(img, 0, 0, null);
 
@@ -507,6 +524,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
               AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
       g2d.drawImage(selectImage, 0, 0, this);
     }
+
     g2d.dispose();
 
     return lcimg;
@@ -519,6 +537,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   {
     BufferedImage lcimg = null;
 
+    int charWidth = av.getCharWidth();
+    int charHeight = av.getCharHeight();
+    
     int width = getWidth();
     int height = getHeight();
 
@@ -532,8 +553,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
     try
     {
-      lcimg = new BufferedImage(width, height,
-              BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
+       lcimg = new BufferedImage(width, height,
+                BufferedImage.TYPE_INT_ARGB); // ARGB so alpha compositing works
     } catch (OutOfMemoryError er)
     {
       System.gc();
@@ -558,29 +579,30 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
    */
   public int getWrappedCanvasWidth(int canvasWidth)
   {
-    FontMetrics fm = getFontMetrics(av.getFont());
+    int charWidth = av.getCharWidth();
 
-    labelWidthEast = 0;
-    labelWidthWest = 0;
+    FontMetrics fm = getFontMetrics(av.getFont());
 
-    if (av.getScaleRightWrapped())
+    int labelWidth = 0;
+    
+    if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
     {
-      labelWidthEast = getLabelWidth(fm);
+      labelWidth = getLabelWidth(fm);
     }
 
-    if (av.getScaleLeftWrapped())
-    {
-      labelWidthWest = labelWidthEast > 0 ? labelWidthEast
-              : getLabelWidth(fm);
-    }
+    labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
+
+    labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
 
     return (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
   }
 
   /**
-   * Returns a pixel width suitable for showing the largest sequence coordinate
-   * (end position) in the alignment. Returns 2 plus the number of decimal
-   * digits to be shown (3 for 1-10, 4 for 11-99 etc).
+   * Returns a pixel width sufficient to show the largest sequence coordinate
+   * (end position) in the alignment, calculated as the FontMetrics width of
+   * zeroes "0000000" limited to the number of decimal digits to be shown (3 for
+   * 1-10, 4 for 11-99 etc). One character width is added to this, to allow for
+   * half a character width space on either side.
    * 
    * @param fm
    * @return
@@ -598,160 +620,313 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       maxWidth = Math.max(maxWidth, alignment.getSequenceAt(i).getEnd());
     }
 
-    int length = 2;
+    int length = 0;
     for (int i = maxWidth; i > 0; i /= 10)
     {
       length++;
     }
 
-    return fm.stringWidth(ZEROS.substring(0, length));
+    return fm.stringWidth(ZEROS.substring(0, length)) + av.getCharWidth();
   }
 
   /**
-   * DOCUMENT ME!
+   * Draws as many widths of a wrapped alignment as can fit in the visible
+   * window
    * 
    * @param g
-   *          DOCUMENT ME!
    * @param canvasWidth
-   *          DOCUMENT ME!
+   *          available width in pixels
    * @param canvasHeight
-   *          DOCUMENT ME!
-   * @param startRes
-   *          DOCUMENT ME!
+   *          available height in pixels
+   * @param startColumn
+   *          the first column (0...) of the alignment to draw
    */
-  private void drawWrappedPanel(Graphics g, int canvasWidth,
-          int canvasHeight, int startRes)
+  public void drawWrappedPanel(Graphics g, int canvasWidth,
+          int canvasHeight, final int startColumn)
   {
-    updateViewport();
-    AlignmentI al = av.getAlignment();
+    int wrappedWidthInResidues = calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
 
-    int labelWidth = 0;
-    if (av.getScaleRightWrapped() || av.getScaleLeftWrapped())
+    av.setWrappedWidth(wrappedWidthInResidues);
+
+    ViewportRanges ranges = av.getRanges();
+    ranges.setViewportStartAndWidth(startColumn, wrappedWidthInResidues);
+
+    // we need to call this again to make sure the startColumn +
+    // wrappedWidthInResidues values are used to calculate wrappedVisibleWidths
+    // correctly.
+    calculateWrappedGeometry(canvasWidth, canvasHeight);
+
+    /*
+     * draw one width at a time (including any scales or annotation shown),
+     * until we have run out of either alignment or vertical space available
+     */
+    int ypos = wrappedSpaceAboveAlignment;
+    int maxWidth = ranges.getVisibleAlignmentWidth();
+
+    int start = startColumn;
+    int currentWidth = 0;
+    while ((currentWidth < wrappedVisibleWidths) && (start < maxWidth))
     {
-      FontMetrics fm = getFontMetrics(av.getFont());
-      labelWidth = getLabelWidth(fm);
+      int endColumn = Math
+              .min(maxWidth, start + wrappedWidthInResidues - 1);
+      drawWrappedWidth(g, ypos, start, endColumn, canvasHeight);
+      ypos += wrappedRepeatHeightPx;
+      start += wrappedWidthInResidues;
+      currentWidth++;
     }
 
-    labelWidthEast = av.getScaleRightWrapped() ? labelWidth : 0;
-    labelWidthWest = av.getScaleLeftWrapped() ? labelWidth : 0;
+    drawWrappedDecorators(g, startColumn);
+  }
 
-    int hgap = charHeight;
-    if (av.getScaleAboveWrapped())
+  /**
+   * Calculates and saves values needed when rendering a wrapped alignment.
+   * These depend on many factors, including
+   * <ul>
+   * <li>canvas width and height</li>
+   * <li>number of visible sequences, and height of annotations if shown</li>
+   * <li>font and character width</li>
+   * <li>whether scales are shown left, right or above the alignment</li>
+   * </ul>
+   * 
+   * @param canvasWidth
+   * @param canvasHeight
+   * @return the number of residue columns in each width
+   */
+  protected int calculateWrappedGeometry(int canvasWidth, int canvasHeight)
+  {
+    int charHeight = av.getCharHeight();
+
+    /*
+     * vertical space in pixels between wrapped widths of alignment
+     * - one character height, or two if scale above is drawn
+     */
+    wrappedSpaceAboveAlignment = charHeight
+            * (av.getScaleAboveWrapped() ? 2 : 1);
+
+    /*
+     * height in pixels of the wrapped widths
+     */
+    wrappedRepeatHeightPx = wrappedSpaceAboveAlignment;
+    // add sequences
+    wrappedRepeatHeightPx += av.getRanges().getViewportHeight()
+            * charHeight;
+    // add annotations panel height if shown
+    wrappedRepeatHeightPx += getAnnotationHeight();
+
+    /*
+     * number of visible widths (the last one may be part height),
+     * ensuring a part height includes at least one sequence
+     */
+    ViewportRanges ranges = av.getRanges();
+    wrappedVisibleWidths = canvasHeight / wrappedRepeatHeightPx;
+    int remainder = canvasHeight % wrappedRepeatHeightPx;
+    if (remainder >= (wrappedSpaceAboveAlignment + charHeight))
     {
-      hgap += charHeight;
+      wrappedVisibleWidths++;
     }
 
-    int cWidth = (canvasWidth - labelWidthEast - labelWidthWest) / charWidth;
-    int cHeight = av.getAlignment().getHeight() * charHeight;
+    /*
+     * compute width in residues; this also sets East and West label widths
+     */
+    int wrappedWidthInResidues = getWrappedCanvasWidth(canvasWidth);
 
-    av.setWrappedWidth(cWidth);
+    /*
+     *  limit visibleWidths to not exceed width of alignment
+     */
+    int xMax = ranges.getVisibleAlignmentWidth();
+    int startToEnd = xMax - ranges.getStartRes();
+    int maxWidths = startToEnd / wrappedWidthInResidues;
+    if (startToEnd % wrappedWidthInResidues > 0)
+    {
+      maxWidths++;
+    }
+    wrappedVisibleWidths = Math.min(wrappedVisibleWidths, maxWidths);
 
-    av.getRanges().setViewportStartAndWidth(startRes, cWidth);
+    return wrappedWidthInResidues;
+  }
 
-    int endx;
-    int ypos = hgap;
-    int maxwidth = av.getAlignment().getWidth();
+  /**
+   * Draws one width of a wrapped alignment, including sequences and
+   * annnotations, if shown, but not scales or hidden column markers
+   * 
+   * @param g
+   * @param ypos
+   * @param startColumn
+   * @param endColumn
+   * @param canvasHeight
+   */
+  protected void drawWrappedWidth(Graphics g, int ypos, int startColumn,
+          int endColumn, int canvasHeight)
+  {
+    ViewportRanges ranges = av.getRanges();
+    int viewportWidth = ranges.getViewportWidth();
 
-    if (av.hasHiddenColumns())
+    int endx = Math.min(startColumn + viewportWidth - 1, endColumn);
+
+    /*
+     * move right before drawing by the width of the scale left (if any)
+     * plus column offset from left margin (usually zero, but may be non-zero
+     * when fast painting is drawing just a few columns)
+     */
+    int charWidth = av.getCharWidth();
+    int xOffset = labelWidthWest
+            + ((startColumn - ranges.getStartRes()) % viewportWidth)
+            * charWidth;
+    g.translate(xOffset, 0);
+
+    // When printing we have an extra clipped region,
+    // the Printable page which we need to account for here
+    Shape clip = g.getClip();
+
+    if (clip == null)
     {
-      maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth);
+      g.setClip(0, 0, viewportWidth * charWidth, canvasHeight);
     }
+    else
+    {
+      g.setClip(0, (int) clip.getBounds().getY(),
+              viewportWidth * charWidth, (int) clip.getBounds().getHeight());
+    }
+
+    /*
+     * white fill the region to be drawn (so incremental fast paint doesn't
+     * scribble over an existing image)
+     */
+    g.setColor(Color.white);
+    g.fillRect(0, ypos, (endx - startColumn + 1) * charWidth,
+            wrappedRepeatHeightPx);
 
-    int annotationHeight = getAnnotationHeight();
+    drawPanel(g, startColumn, endx, 0, av.getAlignment().getHeight() - 1,
+            ypos);
 
-    while ((ypos <= canvasHeight) && (startRes < maxwidth))
-    {
-      endx = startRes + cWidth - 1;
+    int cHeight = av.getAlignment().getHeight() * av.getCharHeight();
 
-      if (endx > maxwidth)
+    if (av.isShowAnnotation())
+    {
+      g.translate(0, cHeight + ypos + 3);
+      if (annotations == null)
       {
-        endx = maxwidth;
+        annotations = new AnnotationPanel(av);
       }
 
-      g.setFont(av.getFont());
-      g.setColor(Color.black);
+      annotations.renderer.drawComponent(annotations, av, g, -1,
+              startColumn, endx + 1);
+      g.translate(0, -cHeight - ypos - 3);
+    }
+    g.setClip(clip);
+    g.translate(-xOffset, 0);
+  }
+
+  /**
+   * Draws scales left, right and above (if shown), and any hidden column
+   * markers, on all widths of the wrapped alignment
+   * 
+   * @param g
+   * @param startColumn
+   */
+  protected void drawWrappedDecorators(Graphics g, final int startColumn)
+  {
+    int charWidth = av.getCharWidth();
+
+    g.setFont(av.getFont());
+    g.setColor(Color.black);
+
+    int ypos = wrappedSpaceAboveAlignment;
+    ViewportRanges ranges = av.getRanges();
+    int viewportWidth = ranges.getViewportWidth();
+    int maxWidth = ranges.getVisibleAlignmentWidth();
+    int widthsDrawn = 0;
+    int startCol = startColumn;
+
+    while (widthsDrawn < wrappedVisibleWidths)
+    {
+      int endColumn = Math.min(maxWidth, startCol + viewportWidth - 1);
 
       if (av.getScaleLeftWrapped())
       {
-        drawWestScale(g, startRes, endx, ypos);
+        drawVerticalScale(g, startCol, endColumn - 1, ypos, true);
       }
 
       if (av.getScaleRightWrapped())
       {
-        g.translate(canvasWidth - labelWidthEast, 0);
-        drawEastScale(g, startRes, endx, ypos);
-        g.translate(-(canvasWidth - labelWidthEast), 0);
+        int x = labelWidthWest + viewportWidth * charWidth;
+        g.translate(x, 0);
+        drawVerticalScale(g, startCol, endColumn, ypos, false);
+        g.translate(-x, 0);
       }
 
+      /*
+       * white fill region of scale above and hidden column markers
+       * (to support incremental fast paint of image)
+       */
+      g.translate(labelWidthWest, 0);
+      g.setColor(Color.white);
+      g.fillRect(0, ypos - wrappedSpaceAboveAlignment, viewportWidth
+              * charWidth + labelWidthWest, wrappedSpaceAboveAlignment);
+      g.setColor(Color.black);
+      g.translate(-labelWidthWest, 0);
+
       g.translate(labelWidthWest, 0);
 
       if (av.getScaleAboveWrapped())
       {
-        drawNorthScale(g, startRes, endx, ypos);
+        drawNorthScale(g, startCol, endColumn, ypos);
       }
 
       if (av.hasHiddenColumns() && av.getShowHiddenMarkers())
       {
-        g.setColor(Color.blue);
-        int res;
-        HiddenColumns hidden = av.getAlignment().getHiddenColumns();
-        List<Integer> positions = hidden.findHiddenRegionPositions();
-        for (int pos : positions)
-        {
-          res = pos - startRes;
-
-          if (res < 0 || res > endx - startRes)
-          {
-            continue;
-          }
+        drawHiddenColumnMarkers(g, ypos, startCol, endColumn);
+      }
 
-          gg.fillPolygon(
-                  new int[]
-                  { res * charWidth - charHeight / 4,
-                      res * charWidth + charHeight / 4, res * charWidth },
-                  new int[]
-                  { ypos - (charHeight / 2), ypos - (charHeight / 2),
-                      ypos - (charHeight / 2) + 8 },
-                  3);
+      g.translate(-labelWidthWest, 0);
 
-        }
-      }
+      ypos += wrappedRepeatHeightPx;
+      startCol += viewportWidth;
+      widthsDrawn++;
+    }
+  }
 
-      // When printing we have an extra clipped region,
-      // the Printable page which we need to account for here
-      Shape clip = g.getClip();
+  /**
+   * Draws markers (triangles) above hidden column positions between startColumn
+   * and endColumn.
+   * 
+   * @param g
+   * @param ypos
+   * @param startColumn
+   * @param endColumn
+   */
+  protected void drawHiddenColumnMarkers(Graphics g, int ypos,
+          int startColumn, int endColumn)
+  {
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
 
-      if (clip == null)
-      {
-        g.setClip(0, 0, cWidth * charWidth, canvasHeight);
-      }
-      else
-      {
-        g.setClip(0, (int) clip.getBounds().getY(), cWidth * charWidth,
-                (int) clip.getBounds().getHeight());
-      }
+    g.setColor(Color.blue);
+    int res;
+    HiddenColumns hidden = av.getAlignment().getHiddenColumns();
 
-      drawPanel(g, startRes, endx, 0, al.getHeight() - 1, ypos);
+    Iterator<Integer> it = hidden.getStartRegionIterator(startColumn,
+            endColumn);
+    while (it.hasNext())
+    {
+      res = it.next() - startColumn;
 
-      if (av.isShowAnnotation())
+      if (res < 0 || res > endColumn - startColumn + 1)
       {
-        g.translate(0, cHeight + ypos + 3);
-        if (annotations == null)
-        {
-          annotations = new AnnotationPanel(av);
-        }
-
-        annotations.renderer.drawComponent(annotations, av, g, -1, startRes,
-                endx + 1);
-        g.translate(0, -cHeight - ypos - 3);
+        continue;
       }
-      g.setClip(clip);
-      g.translate(-labelWidthWest, 0);
 
-      ypos += cHeight + annotationHeight + hgap;
-
-      startRes += cWidth;
+      /*
+       * draw a downward-pointing triangle at the hidden columns location
+       * (before the following visible column)
+       */
+      int xMiddle = res * charWidth;
+      int[] xPoints = new int[] { xMiddle - charHeight / 4,
+          xMiddle + charHeight / 4, xMiddle };
+      int yTop = ypos - (charHeight / 2);
+      int[] yPoints = new int[] { yTop, yTop, yTop + 8 };
+      g.fillPolygon(xPoints, yPoints, 3);
     }
   }
 
@@ -762,6 +937,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
           int canvasWidth,
           int canvasHeight, int startRes)
   {
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+      
     // height gap above each panel
     int hgap = charHeight;
     if (av.getScaleAboveWrapped())
@@ -781,7 +959,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       maxwidth = av.getAlignment().getHiddenColumns()
-              .findColumnPosition(maxwidth);
+              .absoluteToVisibleColumn(maxwidth);
     }
 
     // chop the wrapped alignment extent up into panel-sized blocks and treat
@@ -834,22 +1012,24 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
    * marker.
    * 
    * @param g1
-   *          Graphics object to draw with
+   *          the graphics context, positioned at the first residue to be drawn
    * @param startRes
-   *          offset of the first column in the visible region (0..)
+   *          offset of the first column to draw (0..)
    * @param endRes
-   *          offset of the last column in the visible region (0..)
+   *          offset of the last column to draw (0..)
    * @param startSeq
-   *          offset of the first sequence in the visible region (0..)
+   *          offset of the first sequence to draw (0..)
    * @param endSeq
-   *          offset of the last sequence in the visible region (0..)
+   *          offset of the last sequence to draw (0..)
    * @param yOffset
    *          vertical offset at which to draw (for wrapped alignments)
    */
   public void drawPanel(Graphics g1, final int startRes, final int endRes,
           final int startSeq, final int endSeq, final int yOffset)
   {
-    updateViewport();
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+
     if (!av.hasHiddenColumns())
     {
       draw(g1, startRes, endRes, startSeq, endSeq, yOffset);
@@ -857,29 +1037,23 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     else
     {
       int screenY = 0;
-      final int screenYMax = endRes - startRes;
-      int blockStart = startRes;
-      int blockEnd = endRes;
+      int blockStart;
+      int blockEnd;
 
-      for (int[] region : av.getAlignment().getHiddenColumns()
-              .getHiddenColumnsCopy())
-      {
-        int hideStart = region[0];
-        int hideEnd = region[1];
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      VisibleContigsIterator regions = hidden
+              .getVisContigsIterator(startRes, endRes + 1, true);
 
-        if (hideStart <= blockStart)
-        {
-          blockStart += (hideEnd - hideStart) + 1;
-          continue;
-        }
+      while (regions.hasNext())
+      {
+        int[] region = regions.next();
+        blockEnd = region[1];
+        blockStart = region[0];
 
         /*
          * draw up to just before the next hidden region, or the end of
          * the visible region, whichever comes first
          */
-        blockEnd = Math.min(hideStart - 1, blockStart + screenYMax
-                - screenY);
-
         g1.translate(screenY * charWidth, 0);
 
         draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
@@ -888,7 +1062,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
          * draw the downline of the hidden column marker (ScalePanel draws the
          * triangle on top) if we reached it
          */
-        if (av.getShowHiddenMarkers() && blockEnd == hideStart - 1)
+        if (av.getShowHiddenMarkers()
+                && (regions.hasNext() || regions.endsAtHidden()))
         {
           g1.setColor(Color.blue);
 
@@ -899,23 +1074,6 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
         g1.translate(-screenY * charWidth, 0);
         screenY += blockEnd - blockStart + 1;
-        blockStart = hideEnd + 1;
-
-        if (screenY > screenYMax)
-        {
-          // already rendered last block
-          return;
-        }
-      }
-
-      if (screenY <= screenYMax)
-      {
-        // remaining visible region to render
-        blockEnd = blockStart + screenYMax - screenY;
-        g1.translate(screenY * charWidth, 0);
-        draw(g1, blockStart, blockEnd, startSeq, endSeq, yOffset);
-
-        g1.translate(-screenY * charWidth, 0);
       }
     }
 
@@ -939,6 +1097,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   private void draw(Graphics g, int startRes, int endRes, int startSeq,
           int endSeq, int offset)
   {
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+
     g.setFont(av.getFont());
     seqRdr.prepare(g, av.isRenderGaps());
 
@@ -970,26 +1131,19 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       if (av.hasSearchResults())
       {
         SearchResultsI searchResults = av.getSearchResults();
-        int[] visibleResults = searchResults.getResults(nextSeq,
-                startRes, endRes);
+        int[] visibleResults = searchResults.getResults(nextSeq, startRes,
+                endRes);
         if (visibleResults != null)
         {
           for (int r = 0; r < visibleResults.length; r += 2)
           {
             seqRdr.drawHighlightedText(nextSeq, visibleResults[r],
-                    visibleResults[r + 1], (visibleResults[r] - startRes)
-                            * charWidth, offset
-                            + ((i - startSeq) * charHeight));
+                    visibleResults[r + 1],
+                    (visibleResults[r] - startRes) * charWidth,
+                    offset + ((i - startSeq) * charHeight));
           }
         }
       }
-
-      if (av.cursorMode && cursorY == i && cursorX >= startRes
-              && cursorX <= endRes)
-      {
-        seqRdr.drawCursor(nextSeq, cursorX, (cursorX - startRes) * charWidth,
-                offset + ((i - startSeq) * charHeight));
-      }
     }
 
     if (av.getSelectionGroup() != null
@@ -1087,6 +1241,94 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     return selectionImage;
   }
 
+  /**
+   * Draw the cursor as a separate image and overlay
+   * 
+   * @param startRes
+   *          start residue of area to draw cursor in
+   * @param endRes
+   *          end residue of area to draw cursor in
+   * @param startSeq
+   *          start sequence of area to draw cursor in
+   * @param endSeq
+   *          end sequence of are to draw cursor in
+   * @return a transparent image of the same size as the sequence canvas, with
+   *         the cursor drawn on it, if any
+   */
+  private void drawCursor(Graphics g, int startRes, int endRes,
+          int startSeq,
+          int endSeq)
+  {
+    // convert the cursorY into a position on the visible alignment
+    int cursor_ypos = cursorY;
+
+    // don't do work unless we have to
+    if (cursor_ypos >= startSeq && cursor_ypos <= endSeq)
+    {
+      int yoffset = 0;
+      int xoffset = 0;
+      int startx = startRes;
+      int endx = endRes;
+
+      // convert the cursorX into a position on the visible alignment
+      int cursor_xpos = av.getAlignment().getHiddenColumns()
+              .absoluteToVisibleColumn(cursorX);
+
+      if (av.getAlignment().getHiddenColumns().isVisible(cursorX))
+      {
+
+        if (av.getWrapAlignment())
+        {
+          // work out the correct offsets for the cursor
+          int charHeight = av.getCharHeight();
+          int charWidth = av.getCharWidth();
+          int canvasWidth = getWidth();
+          int canvasHeight = getHeight();
+
+          // height gap above each panel
+          int hgap = charHeight;
+          if (av.getScaleAboveWrapped())
+          {
+            hgap += charHeight;
+          }
+
+          int cWidth = (canvasWidth - labelWidthEast - labelWidthWest)
+                  / charWidth;
+          int cHeight = av.getAlignment().getHeight() * charHeight;
+
+          endx = startx + cWidth - 1;
+          int ypos = hgap; // vertical offset
+
+          // iterate down the wrapped panels
+          while ((ypos <= canvasHeight) && (endx < cursor_xpos))
+          {
+            // update vertical offset
+            ypos += cHeight + getAnnotationHeight() + hgap;
+
+            // update horizontal offset
+            startx += cWidth;
+            endx = startx + cWidth - 1;
+          }
+          yoffset = ypos;
+          xoffset = labelWidthWest;
+        }
+
+        // now check if cursor is within range for x values
+        if (cursor_xpos >= startx && cursor_xpos <= endx)
+        {
+          // get the character the cursor is drawn at
+          SequenceI seq = av.getAlignment().getSequenceAt(cursorY);
+          char s = seq.getCharAt(cursorX);
+
+          seqRdr.drawCursor(g, s,
+                  xoffset + (cursor_xpos - startx) * av.getCharWidth(),
+                  yoffset + (cursor_ypos - startSeq) * av.getCharHeight());
+        }
+      }
+    }
+  }
+
+
   /*
    * Set up graphics for selection group
    */
@@ -1118,6 +1360,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   private void drawUnwrappedSelection(Graphics2D g, SequenceGroup group,
           int startRes, int endRes, int startSeq, int endSeq, int offset)
   {
+    int charWidth = av.getCharWidth();
+          
     if (!av.hasHiddenColumns())
     {
       drawPartialGroupOutline(g, group, startRes, endRes, startSeq, endSeq,
@@ -1127,22 +1371,17 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     {
       // package into blocks of visible columns
       int screenY = 0;
-      int blockStart = startRes;
-      int blockEnd = endRes;
+      int blockStart;
+      int blockEnd;
 
-      for (int[] region : av.getAlignment().getHiddenColumns()
-              .getHiddenColumnsCopy())
+      HiddenColumns hidden = av.getAlignment().getHiddenColumns();
+      VisibleContigsIterator regions = hidden
+              .getVisContigsIterator(startRes, endRes + 1, true);
+      while (regions.hasNext())
       {
-        int hideStart = region[0];
-        int hideEnd = region[1];
-
-        if (hideStart <= blockStart)
-        {
-          blockStart += (hideEnd - hideStart) + 1;
-          continue;
-        }
-
-        blockEnd = hideStart - 1;
+        int[] region = regions.next();
+        blockEnd = region[1];
+        blockStart = region[0];
 
         g.translate(screenY * charWidth, 0);
         drawPartialGroupOutline(g, group,
@@ -1150,24 +1389,6 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
 
         g.translate(-screenY * charWidth, 0);
         screenY += blockEnd - blockStart + 1;
-        blockStart = hideEnd + 1;
-
-        if (screenY > (endRes - startRes))
-        {
-          // already rendered last block
-          break;
-        }
-      }
-
-      if (screenY <= (endRes - startRes))
-      {
-        // remaining visible region to render
-        blockEnd = blockStart + (endRes - startRes) - screenY;
-        g.translate(screenY * charWidth, 0);
-        drawPartialGroupOutline(g, group,
-                blockStart, blockEnd, startSeq, endSeq, offset);
-        
-        g.translate(-screenY * charWidth, 0);
       }
     }
   }
@@ -1179,6 +1400,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
           int startRes, int endRes, int startSeq, int endSeq,
           int verticalOffset)
   {
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
     int visWidth = (endRes - startRes + 1) * charWidth;
 
     int oldY = -1;
@@ -1186,140 +1409,141 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     boolean inGroup = false;
     int top = -1;
     int bottom = -1;
-
-    int sx = -1;
     int sy = -1;
-    int xwidth = -1;
 
-    for (i = startSeq; i <= endSeq; i++)
-    {
-      // position of start residue of group relative to startRes, in pixels
-      sx = (group.getStartRes() - startRes) * charWidth;
-
-      // width of group in pixels
-      xwidth = (((group.getEndRes() + 1) - group.getStartRes()) * charWidth)
-              - 1;
+    List<SequenceI> seqs = group.getSequences(null);
 
-      sy = verticalOffset + (i - startSeq) * charHeight;
+    // position of start residue of group relative to startRes, in pixels
+    int sx = (group.getStartRes() - startRes) * charWidth;
 
-      if (sx + xwidth < 0 || sx > visWidth)
-      {
-        continue;
-      }
+    // width of group in pixels
+    int xwidth = (((group.getEndRes() + 1) - group.getStartRes())
+            * charWidth) - 1;
 
-      if ((sx <= (endRes - startRes) * charWidth)
-              && group.getSequences(null)
-                      .contains(av.getAlignment().getSequenceAt(i)))
+    if (!(sx + xwidth < 0 || sx > visWidth))
+    {
+      for (i = startSeq; i <= endSeq; i++)
       {
-        if ((bottom == -1) && !group.getSequences(null)
-                .contains(av.getAlignment().getSequenceAt(i + 1)))
-        {
-          bottom = sy + charHeight;
-        }
-
-        if (!inGroup)
-        {
-          if (((top == -1) && (i == 0)) || !group.getSequences(null)
-                  .contains(av.getAlignment().getSequenceAt(i - 1)))
-          {
-            top = sy;
-          }
+        sy = verticalOffset + (i - startSeq) * charHeight;
 
-          oldY = sy;
-          inGroup = true;
-        }
-      }
-      else
-      {
-        if (inGroup)
+        if ((sx <= (endRes - startRes) * charWidth)
+                && seqs.contains(av.getAlignment().getSequenceAt(i)))
         {
-          // if start position is visible, draw vertical line to left of
-          // group
-          if (sx >= 0 && sx < visWidth)
-          {
-            g.drawLine(sx, oldY, sx, sy);
-          }
-
-          // if end position is visible, draw vertical line to right of
-          // group
-          if (sx + xwidth < visWidth)
+          if ((bottom == -1)
+                  && !seqs.contains(av.getAlignment().getSequenceAt(i + 1)))
           {
-            g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
+            bottom = sy + charHeight;
           }
 
-          if (sx < 0)
+          if (!inGroup)
           {
-            xwidth += sx;
-            sx = 0;
-          }
-
-          // don't let width extend beyond current block, or group extent
-          // fixes JAL-2672
-          if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
-          {
-            xwidth = (endRes - startRes + 1) * charWidth - sx;
-          }
-          
-          // draw horizontal line at top of group
-          if (top != -1)
-          {
-            g.drawLine(sx, top, sx + xwidth, top);
-            top = -1;
-          }
+            if (((top == -1) && (i == 0)) || !seqs
+                    .contains(av.getAlignment().getSequenceAt(i - 1)))
+            {
+              top = sy;
+            }
 
-          // draw horizontal line at bottom of group
-          if (bottom != -1)
-          {
-            g.drawLine(sx, bottom, sx + xwidth, bottom);
-            bottom = -1;
+            oldY = sy;
+            inGroup = true;
           }
+        }
+        else if (inGroup)
+        {
+          drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
+          drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
 
+          // reset top and bottom
+          top = -1;
+          bottom = -1;
           inGroup = false;
         }
       }
-    }
-
-    if (inGroup)
-    {
-      sy = verticalOffset + ((i - startSeq) * charHeight);
-      if (sx >= 0 && sx < visWidth)
+      if (inGroup)
       {
-        g.drawLine(sx, oldY, sx, sy);
+        sy = verticalOffset + ((i - startSeq) * charHeight);
+        drawVerticals(g, sx, xwidth, visWidth, oldY, sy);
+        drawHorizontals(g, sx, xwidth, visWidth, top, bottom);
       }
+    }
+  }
 
-      if (sx + xwidth < visWidth)
-      {
-        g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
-      }
+  /**
+   * Draw horizontal selection group boundaries at top and bottom positions
+   * 
+   * @param g
+   *          graphics object to draw on
+   * @param sx
+   *          start x position
+   * @param xwidth
+   *          width of gap
+   * @param visWidth
+   *          visWidth maximum available width
+   * @param top
+   *          position to draw top of group at
+   * @param bottom
+   *          position to draw bottom of group at
+   */
+  private void drawHorizontals(Graphics2D g, int sx, int xwidth,
+          int visWidth, int top, int bottom)
+  {
+    int width = xwidth;
+    int startx = sx;
+    if (startx < 0)
+    {
+      width += startx;
+      startx = 0;
+    }
 
-      if (sx < 0)
-      {
-        xwidth += sx;
-        sx = 0;
-      }
+    // don't let width extend beyond current block, or group extent
+    // fixes JAL-2672
+    if (startx + width >= visWidth)
+    {
+      width = visWidth - startx;
+    }
 
-      if (sx + xwidth > visWidth)
-      {
-        xwidth = visWidth;
-      }
-      else if (sx + xwidth >= (endRes - startRes + 1) * charWidth)
-      {
-        xwidth = (endRes - startRes + 1) * charWidth;
-      }
+    if (top != -1)
+    {
+      g.drawLine(startx, top, startx + width, top);
+    }
 
-      if (top != -1)
-      {
-        g.drawLine(sx, top, sx + xwidth, top);
-        top = -1;
-      }
+    if (bottom != -1)
+    {
+      g.drawLine(startx, bottom - 1, startx + width, bottom - 1);
+    }
+  }
 
-      if (bottom != -1)
-      {
-        g.drawLine(sx, bottom - 1, sx + xwidth, bottom - 1);
-        bottom = -1;
-      }
+  /**
+   * Draw vertical lines at sx and sx+xwidth providing they lie within
+   * [0,visWidth)
+   * 
+   * @param g
+   *          graphics object to draw on
+   * @param sx
+   *          start x position
+   * @param xwidth
+   *          width of gap
+   * @param visWidth
+   *          visWidth maximum available width
+   * @param oldY
+   *          top y value
+   * @param sy
+   *          bottom y value
+   */
+  private void drawVerticals(Graphics2D g, int sx, int xwidth, int visWidth,
+          int oldY, int sy)
+  {
+    // if start position is visible, draw vertical line to left of
+    // group
+    if (sx >= 0 && sx < visWidth)
+    {
+      g.drawLine(sx, oldY, sx, sy);
+    }
 
-      inGroup = false;
+    // if end position is visible, draw vertical line to right of
+    // group
+    if (sx + xwidth < visWidth)
+    {
+      g.drawLine(sx + xwidth, oldY, sx + xwidth, sy);
     }
   }
   
@@ -1347,14 +1571,11 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       return false;
     }
     boolean wrapped = av.getWrapAlignment();
-
     try
     {
       fastPaint = !noFastPaint;
       fastpainting = fastPaint;
 
-      updateViewport();
-
       /*
        * to avoid redrawing the whole visible region, we instead
        * redraw just the minimal regions to remove previous highlights
@@ -1409,7 +1630,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
    */
   protected boolean drawMappedPositions(SearchResultsI results)
   {
-    if (results == null)
+    if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
     {
       return false;
     }
@@ -1431,9 +1652,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       firstVisibleColumn = alignment.getHiddenColumns()
-              .adjustForHiddenColumns(firstVisibleColumn);
+              .visibleToAbsoluteColumn(firstVisibleColumn);
       lastVisibleColumn = alignment.getHiddenColumns()
-              .adjustForHiddenColumns(lastVisibleColumn);
+              .visibleToAbsoluteColumn(lastVisibleColumn);
     }
 
     for (int seqNo = ranges.getStartSeq(); seqNo <= ranges
@@ -1476,8 +1697,8 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       if (av.hasHiddenColumns())
       {
         firstCol = alignment.getHiddenColumns()
-                .findColumnPosition(firstCol);
-        lastCol = alignment.getHiddenColumns().findColumnPosition(lastCol);
+                .absoluteToVisibleColumn(firstCol);
+        lastCol = alignment.getHiddenColumns().absoluteToVisibleColumn(lastCol);
       }
       int transX = (firstCol - ranges.getStartRes()) * av.getCharWidth();
       int transY = (firstSeq - ranges.getStartSeq()) * av.getCharHeight();
@@ -1498,34 +1719,42 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     {
       fastPaint = true;
       repaint();
+      return;
     }
-    else if (av.getWrapAlignment())
+    else if (eventName.equals(ViewportRanges.MOVE_VIEWPORT))
     {
-      if (eventName.equals(ViewportRanges.STARTRES))
-      {
-        repaint();
-      }
+      fastPaint = false;
+      repaint();
+      return;
     }
-    else
+
+    int scrollX = 0;
+    if (eventName.equals(ViewportRanges.STARTRES)
+            || eventName.equals(ViewportRanges.STARTRESANDSEQ))
     {
-      int scrollX = 0;
+      // Make sure we're not trying to draw a panel
+      // larger than the visible window
       if (eventName.equals(ViewportRanges.STARTRES))
       {
-        // Make sure we're not trying to draw a panel
-        // larger than the visible window
-        ViewportRanges vpRanges = av.getRanges();
         scrollX = (int) evt.getNewValue() - (int) evt.getOldValue();
-        int range = vpRanges.getEndRes() - vpRanges.getStartRes();
-        if (scrollX > range)
-        {
-          scrollX = range;
-        }
-        else if (scrollX < -range)
-        {
-          scrollX = -range;
-        }
       }
+      else
+      {
+        scrollX = ((int[]) evt.getNewValue())[0]
+                - ((int[]) evt.getOldValue())[0];
+      }
+      ViewportRanges vpRanges = av.getRanges();
 
+      int range = vpRanges.getEndRes() - vpRanges.getStartRes();
+      if (scrollX > range)
+      {
+        scrollX = range;
+      }
+      else if (scrollX < -range)
+      {
+        scrollX = -range;
+      }
+    }
       // Both scrolling and resizing change viewport ranges: scrolling changes
       // both start and end points, but resize only changes end values.
       // Here we only want to fastpaint on a scroll, with resize using a normal
@@ -1533,18 +1762,297 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
       // vertical start value.
       if (eventName.equals(ViewportRanges.STARTRES))
       {
-        // scroll - startres and endres both change
-        fastPaint(scrollX, 0);
+         if (av.getWrapAlignment())
+          {
+            fastPaintWrapped(scrollX);
+          }
+          else
+          {
+            fastPaint(scrollX, 0);
+          }
       }
       else if (eventName.equals(ViewportRanges.STARTSEQ))
       {
         // scroll
         fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
       }
+      else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+      {
+        if (av.getWrapAlignment())
+        {
+          fastPaintWrapped(scrollX);
+        }
+        else
+        {
+          fastPaint(scrollX, 0);
+        }
+    }
+    else if (eventName.equals(ViewportRanges.STARTSEQ))
+    {
+      // scroll
+      fastPaint(0, (int) evt.getNewValue() - (int) evt.getOldValue());
+    }
+    else if (eventName.equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      if (av.getWrapAlignment())
+      {
+        fastPaintWrapped(scrollX);
+      }
+    }
+  }
+
+  /**
+   * Does a minimal update of the image for a scroll movement. This method
+   * handles scroll movements of up to one width of the wrapped alignment (one
+   * click in the vertical scrollbar). Larger movements (for example after a
+   * scroll to highlight a mapped position) trigger a full redraw instead.
+   * 
+   * @param scrollX
+   *          number of positions scrolled (right if positive, left if negative)
+   */
+  protected void fastPaintWrapped(int scrollX)
+  {
+    ViewportRanges ranges = av.getRanges();
+
+    if (Math.abs(scrollX) > ranges.getViewportWidth())
+    {
+      /*
+       * shift of more than one view width is 
+       * overcomplicated to handle in this method
+       */
+      fastPaint = false;
+      repaint();
+      return;
+    }
+
+    if (fastpainting || gg == null)
+    {
+      return;
+    }
+
+    fastPaint = true;
+    fastpainting = true;
+
+    try
+    {
+      calculateWrappedGeometry(getWidth(), getHeight());
+
+      /*
+       * relocate the regions of the alignment that are still visible
+       */
+      shiftWrappedAlignment(-scrollX);
+
+      /*
+       * add new columns (sequence, annotation)
+       * - at top left if scrollX < 0 
+       * - at right of last two widths if scrollX > 0
+       */
+      if (scrollX < 0)
+      {
+        int startRes = ranges.getStartRes();
+        drawWrappedWidth(gg, wrappedSpaceAboveAlignment, startRes, startRes
+                - scrollX - 1, getHeight());
+      }
+      else
+      {
+        fastPaintWrappedAddRight(scrollX);
+      }
+
+      /*
+       * draw all scales (if  shown) and hidden column markers
+       */
+      drawWrappedDecorators(gg, ranges.getStartRes());
+
+      repaint();
+    } finally
+    {
+      fastpainting = false;
     }
   }
 
   /**
+   * Draws the specified number of columns at the 'end' (bottom right) of a
+   * wrapped alignment view, including sequences and annotations if shown, but
+   * not scales. Also draws the same number of columns at the right hand end of
+   * the second last width shown, if the last width is not full height (so
+   * cannot simply be copied from the graphics image).
+   * 
+   * @param columns
+   */
+  protected void fastPaintWrappedAddRight(int columns)
+  {
+    if (columns == 0)
+    {
+      return;
+    }
+
+    ViewportRanges ranges = av.getRanges();
+    int viewportWidth = ranges.getViewportWidth();
+    int charWidth = av.getCharWidth();
+
+    /**
+     * draw full height alignment in the second last row, last columns, if the
+     * last row was not full height
+     */
+    int visibleWidths = wrappedVisibleWidths;
+    int canvasHeight = getHeight();
+    boolean lastWidthPartHeight = (wrappedVisibleWidths * wrappedRepeatHeightPx) > canvasHeight;
+
+    if (lastWidthPartHeight)
+    {
+      int widthsAbove = Math.max(0, visibleWidths - 2);
+      int ypos = wrappedRepeatHeightPx * widthsAbove
+              + wrappedSpaceAboveAlignment;
+      int endRes = ranges.getEndRes();
+      endRes += widthsAbove * viewportWidth;
+      int startRes = endRes - columns;
+      int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+              * charWidth;
+
+      /*
+       * white fill first to erase annotations
+       */
+      gg.translate(xOffset, 0);
+      gg.setColor(Color.white);
+      gg.fillRect(labelWidthWest, ypos,
+              (endRes - startRes + 1) * charWidth, wrappedRepeatHeightPx);
+      gg.translate(-xOffset, 0);
+
+      drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+    }
+
+    /*
+     * draw newly visible columns in last wrapped width (none if we
+     * have reached the end of the alignment)
+     * y-offset for drawing last width is height of widths above,
+     * plus one gap row
+     */
+    int widthsAbove = visibleWidths - 1;
+    int ypos = wrappedRepeatHeightPx * widthsAbove
+            + wrappedSpaceAboveAlignment;
+    int endRes = ranges.getEndRes();
+    endRes += widthsAbove * viewportWidth;
+    int startRes = endRes - columns + 1;
+
+    /*
+     * white fill first to erase annotations
+     */
+    int xOffset = ((startRes - ranges.getStartRes()) % viewportWidth)
+            * charWidth;
+    gg.translate(xOffset, 0);
+    gg.setColor(Color.white);
+    int width = viewportWidth * charWidth - xOffset;
+    gg.fillRect(labelWidthWest, ypos, width, wrappedRepeatHeightPx);
+    gg.translate(-xOffset, 0);
+
+    gg.setFont(av.getFont());
+    gg.setColor(Color.black);
+
+    if (startRes < ranges.getVisibleAlignmentWidth())
+    {
+      drawWrappedWidth(gg, ypos, startRes, endRes, canvasHeight);
+    }
+
+    /*
+     * and finally, white fill any space below the visible alignment
+     */
+    int heightBelow = canvasHeight - visibleWidths * wrappedRepeatHeightPx;
+    if (heightBelow > 0)
+    {
+      gg.setColor(Color.white);
+      gg.fillRect(0, canvasHeight - heightBelow, getWidth(), heightBelow);
+    }
+  }
+
+  /**
+   * Shifts the visible alignment by the specified number of columns - left if
+   * negative, right if positive. Copies and moves sequences and annotations (if
+   * shown). Scales, hidden column markers and any newly visible columns must be
+   * drawn separately.
+   * 
+   * @param positions
+   */
+  protected void shiftWrappedAlignment(int positions)
+  {
+    if (positions == 0)
+    {
+      return;
+    }
+    int charWidth = av.getCharWidth();
+
+    int canvasHeight = getHeight();
+    ViewportRanges ranges = av.getRanges();
+    int viewportWidth = ranges.getViewportWidth();
+    int widthToCopy = (ranges.getViewportWidth() - Math.abs(positions))
+            * charWidth;
+    int heightToCopy = wrappedRepeatHeightPx - wrappedSpaceAboveAlignment;
+    int xMax = ranges.getVisibleAlignmentWidth();
+
+    if (positions > 0)
+    {
+      /*
+       * shift right (after scroll left)
+       * for each wrapped width (starting with the last), copy (width-positions) 
+       * columns from the left margin to the right margin, and copy positions 
+       * columns from the right margin of the row above (if any) to the 
+       * left margin of the current row
+       */
+
+      /*
+       * get y-offset of last wrapped width, first row of sequences
+       */
+      int y = canvasHeight / wrappedRepeatHeightPx * wrappedRepeatHeightPx;
+      y += wrappedSpaceAboveAlignment;
+      int copyFromLeftStart = labelWidthWest;
+      int copyFromRightStart = copyFromLeftStart + widthToCopy;
+
+      while (y >= 0)
+      {
+        gg.copyArea(copyFromLeftStart, y, widthToCopy, heightToCopy,
+                positions * charWidth, 0);
+        if (y > 0)
+        {
+          gg.copyArea(copyFromRightStart, y - wrappedRepeatHeightPx,
+                  positions * charWidth, heightToCopy, -widthToCopy,
+                  wrappedRepeatHeightPx);
+        }
+
+        y -= wrappedRepeatHeightPx;
+      }
+    }
+    else
+    {
+      /*
+       * shift left (after scroll right)
+       * for each wrapped width (starting with the first), copy (width-positions) 
+       * columns from the right margin to the left margin, and copy positions 
+       * columns from the left margin of the row below (if any) to the 
+       * right margin of the current row
+       */
+      int xpos = av.getRanges().getStartRes();
+      int y = wrappedSpaceAboveAlignment;
+      int copyFromRightStart = labelWidthWest - positions * charWidth;
+
+      while (y < canvasHeight)
+      {
+        gg.copyArea(copyFromRightStart, y, widthToCopy, heightToCopy,
+                positions * charWidth, 0);
+        if (y + wrappedRepeatHeightPx < canvasHeight - wrappedRepeatHeightPx
+                && (xpos + viewportWidth <= xMax))
+        {
+          gg.copyArea(labelWidthWest, y + wrappedRepeatHeightPx, -positions
+                  * charWidth, heightToCopy, widthToCopy,
+                  -wrappedRepeatHeightPx);
+        }
+
+        y += wrappedRepeatHeightPx;
+        xpos += viewportWidth;
+      }
+    }
+  }
+
+  
+  /**
    * Redraws any positions in the search results in the visible region of a
    * wrapped alignment. Any highlights are drawn depending on the search results
    * set on the Viewport, not the <code>results</code> argument. This allows
@@ -1556,15 +2064,17 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
    */
   protected boolean drawMappedPositionsWrapped(SearchResultsI results)
   {
-    if (results == null)
+    if ((results == null) || (gg == null)) // JAL-2784 check gg is not null
     {
       return false;
     }
-  
+    int charHeight = av.getCharHeight();
+
     boolean matchFound = false;
 
+    calculateWrappedGeometry(getWidth(), getHeight());
     int wrappedWidth = av.getWrappedWidth();
-    int wrappedHeight = getRepeatHeightWrapped();
+    int wrappedHeight = wrappedRepeatHeightPx;
 
     ViewportRanges ranges = av.getRanges();
     int canvasHeight = getHeight();
@@ -1582,9 +2092,9 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
     if (av.hasHiddenColumns())
     {
       firstVisibleColumn = alignment.getHiddenColumns()
-              .adjustForHiddenColumns(firstVisibleColumn);
+              .visibleToAbsoluteColumn(firstVisibleColumn);
       lastVisibleColumn = alignment.getHiddenColumns()
-              .adjustForHiddenColumns(lastVisibleColumn);
+              .visibleToAbsoluteColumn(lastVisibleColumn);
     }
 
     int gapHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
@@ -1623,7 +2133,7 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
               if (av.hasHiddenColumns())
               {
                 displayColumn = alignment.getHiddenColumns()
-                        .findColumnPosition(displayColumn);
+                        .absoluteToVisibleColumn(displayColumn);
               }
 
               /*
@@ -1663,23 +2173,12 @@ public class SeqCanvas extends JComponent implements ViewportListenerI
   }
 
   /**
-   * Answers the height in pixels of a repeating section of the wrapped
-   * alignment, including space above, scale above if shown, sequences, and
-   * annotation panel if shown
+   * Answers the width in pixels of the left scale labels (0 if not shown)
    * 
    * @return
    */
-  protected int getRepeatHeightWrapped()
+  int getLabelWidthWest()
   {
-    // gap (and maybe scale) above
-    int repeatHeight = charHeight * (av.getScaleAboveWrapped() ? 2 : 1);
-
-    // add sequences
-    repeatHeight += av.getRanges().getViewportHeight() * charHeight;
-
-    // add annotations panel height if shown
-    repeatHeight += getAnnotationHeight();
-
-    return repeatHeight;
+    return labelWidthWest;
   }
 }
index e99e577..8b2e7bc 100644 (file)
@@ -59,7 +59,6 @@ import java.awt.event.MouseListener;
 import java.awt.event.MouseMotionListener;
 import java.awt.event.MouseWheelEvent;
 import java.awt.event.MouseWheelListener;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -76,12 +75,11 @@ import javax.swing.ToolTipManager;
 public class SeqPanel extends JPanel
         implements MouseListener, MouseMotionListener, MouseWheelListener,
         SequenceListener, SelectionListener
-
 {
-  /** DOCUMENT ME!! */
+  private static final int MAX_TOOLTIP_LENGTH = 300;
+
   public SeqCanvas seqCanvas;
 
-  /** DOCUMENT ME!! */
   public AlignmentPanel ap;
 
   /*
@@ -148,35 +146,33 @@ public class SeqPanel extends JPanel
   SearchResultsI lastSearchResults;
 
   /**
-   * Creates a new SeqPanel object.
+   * Creates a new SeqPanel object
    * 
-   * @param avp
-   *          DOCUMENT ME!
-   * @param p
-   *          DOCUMENT ME!
+   * @param viewport
+   * @param alignPanel
    */
-  public SeqPanel(AlignViewport av, AlignmentPanel ap)
+  public SeqPanel(AlignViewport viewport, AlignmentPanel alignPanel)
   {
     linkImageURL = getClass().getResource("/images/link.gif");
     seqARep = new SequenceAnnotationReport(linkImageURL.toString());
     ToolTipManager.sharedInstance().registerComponent(this);
     ToolTipManager.sharedInstance().setInitialDelay(0);
     ToolTipManager.sharedInstance().setDismissDelay(10000);
-    this.av = av;
+    this.av = viewport;
     setBackground(Color.white);
 
-    seqCanvas = new SeqCanvas(ap);
+    seqCanvas = new SeqCanvas(alignPanel);
     setLayout(new BorderLayout());
     add(seqCanvas, BorderLayout.CENTER);
 
-    this.ap = ap;
+    this.ap = alignPanel;
 
-    if (!av.isDataset())
+    if (!viewport.isDataset())
     {
       addMouseMotionListener(this);
       addMouseListener(this);
       addMouseWheelListener(this);
-      ssm = av.getStructureSelectionManager();
+      ssm = viewport.getStructureSelectionManager();
       ssm.addStructureViewerListener(this);
       ssm.addSelectionListener(this);
     }
@@ -215,8 +211,8 @@ public class SeqPanel extends JPanel
               + hgap + seqCanvas.getAnnotationHeight();
 
       int y = evt.getY();
-      y -= hgap;
-      x = Math.max(0, x - seqCanvas.labelWidthWest);
+      y = Math.max(0, y - hgap);
+      x = Math.max(0, x - seqCanvas.getLabelWidthWest());
 
       int cwidth = seqCanvas.getWrappedCanvasWidth(this.getWidth());
       if (cwidth < 1)
@@ -250,7 +246,7 @@ public class SeqPanel extends JPanel
     if (av.hasHiddenColumns())
     {
       res = av.getAlignment().getHiddenColumns()
-              .adjustForHiddenColumns(res);
+              .visibleToAbsoluteColumn(res);
     }
 
     return res;
@@ -320,13 +316,13 @@ public class SeqPanel extends JPanel
   void setCursorRow()
   {
     seqCanvas.cursorY = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorColumn()
   {
     seqCanvas.cursorX = getKeyboardNo1() - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void setCursorRowAndColumn()
@@ -339,7 +335,7 @@ public class SeqPanel extends JPanel
     {
       seqCanvas.cursorX = getKeyboardNo1() - 1;
       seqCanvas.cursorY = getKeyboardNo2() - 1;
-      scrollToVisible();
+      scrollToVisible(true);
     }
   }
 
@@ -348,7 +344,7 @@ public class SeqPanel extends JPanel
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
 
     seqCanvas.cursorX = sequence.findIndex(getKeyboardNo1()) - 1;
-    scrollToVisible();
+    scrollToVisible(true);
   }
 
   void moveCursor(int dx, int dy)
@@ -363,10 +359,25 @@ public class SeqPanel extends JPanel
       int original = seqCanvas.cursorX - dx;
       int maxWidth = av.getAlignment().getWidth();
 
-      while (!hidden.isVisible(seqCanvas.cursorX)
-              && seqCanvas.cursorX < maxWidth && seqCanvas.cursorX > 0)
+      if (!hidden.isVisible(seqCanvas.cursorX))
       {
-        seqCanvas.cursorX += dx;
+        int visx = hidden.absoluteToVisibleColumn(seqCanvas.cursorX - dx);
+        int[] region = hidden.getRegionWithEdgeAtRes(visx);
+
+        if (region != null) // just in case
+        {
+          if (dx == 1)
+          {
+            // moving right
+            seqCanvas.cursorX = region[1] + 1;
+          }
+          else if (dx == -1)
+          {
+            // moving left
+            seqCanvas.cursorX = region[0] - 1;
+          }
+        }
+        seqCanvas.cursorX = (seqCanvas.cursorX < 0) ? 0 : seqCanvas.cursorX;
       }
 
       if (seqCanvas.cursorX >= maxWidth
@@ -376,10 +387,16 @@ public class SeqPanel extends JPanel
       }
     }
 
-    scrollToVisible();
+    scrollToVisible(false);
   }
 
-  void scrollToVisible()
+  /**
+   * Scroll to make the cursor visible in the viewport.
+   * 
+   * @param jump
+   *          just jump to the location rather than scrolling
+   */
+  void scrollToVisible(boolean jump)
   {
     if (seqCanvas.cursorX < 0)
     {
@@ -400,20 +417,44 @@ public class SeqPanel extends JPanel
     }
 
     endEditing();
-    if (av.getWrapAlignment())
+
+    boolean repaintNeeded = true;
+    if (jump)
     {
-      av.getRanges().scrollToWrappedVisible(seqCanvas.cursorX);
+      // only need to repaint if the viewport did not move, as otherwise it will
+      // get a repaint
+      repaintNeeded = !av.getRanges().setViewportLocation(seqCanvas.cursorX,
+              seqCanvas.cursorY);
     }
     else
     {
-      av.getRanges().scrollToVisible(seqCanvas.cursorX, seqCanvas.cursorY);
+      if (av.getWrapAlignment())
+      {
+        // scrollToWrappedVisible expects x-value to have hidden cols subtracted
+        int x = av.getAlignment().getHiddenColumns()
+                .absoluteToVisibleColumn(seqCanvas.cursorX);
+        av.getRanges().scrollToWrappedVisible(x);
+      }
+      else
+      {
+        av.getRanges().scrollToVisible(seqCanvas.cursorX,
+                seqCanvas.cursorY);
+      }
     }
-    setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
+
+    if (av.getAlignment().getHiddenColumns().isVisible(seqCanvas.cursorX))
+    {
+      setStatusMessage(av.getAlignment().getSequenceAt(seqCanvas.cursorY),
             seqCanvas.cursorX, seqCanvas.cursorY);
+    }
 
-    seqCanvas.repaint();
+    if (repaintNeeded)
+    {
+      seqCanvas.repaint();
+    }
   }
 
+
   void setSelectionAreaAtCursor(boolean topLeft)
   {
     SequenceI sequence = av.getAlignment().getSequenceAt(seqCanvas.cursorY);
@@ -484,7 +525,7 @@ public class SeqPanel extends JPanel
       av.setSelectionGroup(sg);
     }
 
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     av.sendSelection();
   }
 
@@ -719,10 +760,12 @@ public class SeqPanel extends JPanel
   }
 
   /**
-   * DOCUMENT ME!
+   * Action on mouse movement is to update the status bar to show the current
+   * sequence position, and (if features are shown) to show any features at the
+   * position in a tooltip. Does nothing if the mouse move does not change
+   * residue position.
    * 
    * @param evt
-   *          DOCUMENT ME!
    */
   @Override
   public void mouseMoved(MouseEvent evt)
@@ -735,7 +778,8 @@ public class SeqPanel extends JPanel
     }
 
     final int column = findColumn(evt);
-    int seq = findSeq(evt);
+    final int seq = findSeq(evt);
+
     if (column < 0 || seq < 0 || seq >= av.getAlignment().getHeight())
     {
       lastMouseSeq = -1;
@@ -802,7 +846,7 @@ public class SeqPanel extends JPanel
       List<SequenceFeature> features = ap.getFeatureRenderer()
               .findFeaturesAtColumn(sequence, column + 1);
       seqARep.appendFeatures(tooltipText, pos, features,
-              this.ap.getSeqPanel().seqCanvas.fr.getMinMax());
+              this.ap.getSeqPanel().seqCanvas.fr);
     }
     if (tooltipText.length() == 6) // <html>
     {
@@ -811,6 +855,11 @@ public class SeqPanel extends JPanel
     }
     else
     {
+      if (tooltipText.length() > MAX_TOOLTIP_LENGTH) // constant
+      {
+        tooltipText.setLength(MAX_TOOLTIP_LENGTH);
+        tooltipText.append("...");
+      }
       String textString = tooltipText.toString();
       if (lastTooltip == null || !lastTooltip.equals(textString))
       {
@@ -852,11 +901,12 @@ public class SeqPanel extends JPanel
 
   /**
    * set when the current UI interaction has resulted in a change that requires
-   * overview shading to be recalculated. this could be changed to something
-   * more expressive that indicates what actually has changed, so selective
-   * redraws can be applied
+   * shading in overviews and structures to be recalculated. this could be
+   * changed to a something more expressive that indicates what actually has
+   * changed, so selective redraws can be applied (ie. only structures, only
+   * overview, etc)
    */
-  private boolean needOverviewUpdate = false; // TODO: refactor to avcontroller
+  private boolean updateOverviewAndStructs = false; // TODO: refactor to avcontroller
 
   /**
    * set if av.getSelectionGroup() refers to a group that is defined on the
@@ -1057,7 +1107,7 @@ public class SeqPanel extends JPanel
         }
         if (newWidth > 0)
         {
-          ap.paintAlignment(false);
+          ap.paintAlignment(false, false);
           if (copyChanges)
           {
             /*
@@ -1222,9 +1272,9 @@ public class SeqPanel extends JPanel
     {
       fixedColumns = true;
       int y1 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryLeft(startres);
+              .getNextHiddenBoundary(true, startres);
       int y2 = av.getAlignment().getHiddenColumns()
-              .getHiddenBoundaryRight(startres);
+              .getNextHiddenBoundary(false, startres);
 
       if ((insertGap && startres > y1 && lastres < y1)
               || (!insertGap && startres < y2 && lastres > y2))
@@ -1300,7 +1350,8 @@ public class SeqPanel extends JPanel
           if (sg.getSize() == av.getAlignment().getHeight())
           {
             if ((av.hasHiddenColumns() && startres < av.getAlignment()
-                    .getHiddenColumns().getHiddenBoundaryRight(startres)))
+                    .getHiddenColumns()
+                    .getNextHiddenBoundary(false, startres)))
             {
               endEditing();
               return;
@@ -1619,30 +1670,37 @@ public class SeqPanel extends JPanel
   public void mouseWheelMoved(MouseWheelEvent e)
   {
     e.consume();
-    if (e.getWheelRotation() > 0)
+    double wheelRotation = e.getPreciseWheelRotation();
+    if (wheelRotation > 0)
     {
       if (e.isShiftDown())
       {
         av.getRanges().scrollRight(true);
 
       }
-      else if (!av.getWrapAlignment())
+      else
       {
         av.getRanges().scrollUp(false);
       }
     }
-    else
+    else if (wheelRotation < 0)
     {
       if (e.isShiftDown())
       {
         av.getRanges().scrollRight(false);
       }
-      else if (!av.getWrapAlignment())
+      else
       {
         av.getRanges().scrollUp(true);
       }
     }
-    // TODO Update tooltip for new position.
+
+    /*
+     * update status bar and tooltip for new position
+     * (need to synthesize a mouse movement to refresh tooltip)
+     */
+    mouseMoved(e);
+    ToolTipManager.sharedInstance().mouseMoved(e);
   }
 
   /**
@@ -1656,7 +1714,7 @@ public class SeqPanel extends JPanel
     final int res = findColumn(evt);
     final int seq = findSeq(evt);
     oldSeq = seq;
-    needOverviewUpdate = false;
+    updateOverviewAndStructs = false;
 
     startWrapBlock = wrappedBlock;
 
@@ -1782,21 +1840,10 @@ public class SeqPanel extends JPanel
     final int column = findColumn(evt);
     final int seq = findSeq(evt);
     SequenceI sequence = av.getAlignment().getSequenceAt(seq);
-    List<SequenceFeature> allFeatures = ap.getFeatureRenderer()
+    List<SequenceFeature> features = ap.getFeatureRenderer()
             .findFeaturesAtColumn(sequence, column + 1);
-    List<String> links = new ArrayList<>();
-    for (SequenceFeature sf : allFeatures)
-    {
-      if (sf.links != null)
-      {
-        for (String link : sf.links)
-        {
-          links.add(link);
-        }
-      }
-    }
 
-    PopupMenu pop = new PopupMenu(ap, null, links);
+    PopupMenu pop = new PopupMenu(ap, null, features);
     pop.show(this, evt.getX(), evt.getY());
   }
 
@@ -1821,7 +1868,7 @@ public class SeqPanel extends JPanel
     // always do this - annotation has own state
     // but defer colourscheme update until hidden sequences are passed in
     boolean vischange = stretchGroup.recalcConservation(true);
-    needOverviewUpdate |= vischange && av.isSelectionDefinedGroup()
+    updateOverviewAndStructs |= vischange && av.isSelectionDefinedGroup()
             && afterDrag;
     if (stretchGroup.cs != null)
     {
@@ -1841,8 +1888,10 @@ public class SeqPanel extends JPanel
       }
     }
     PaintRefresher.Refresh(this, av.getSequenceSetId());
-    ap.paintAlignment(needOverviewUpdate);
-    needOverviewUpdate = false;
+    // TODO: structure colours only need updating if stretchGroup used to or now
+    // does contain sequences with structure views
+    ap.paintAlignment(updateOverviewAndStructs, updateOverviewAndStructs);
+    updateOverviewAndStructs = false;
     changeEndRes = false;
     changeStartRes = false;
     stretchGroup = null;
@@ -1896,7 +1945,7 @@ public class SeqPanel extends JPanel
       if (res > (stretchGroup.getStartRes() - 1))
       {
         stretchGroup.setEndRes(res);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
     else if (changeStartRes)
@@ -1904,7 +1953,7 @@ public class SeqPanel extends JPanel
       if (res < (stretchGroup.getEndRes() + 1))
       {
         stretchGroup.setStartRes(res);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -1938,7 +1987,7 @@ public class SeqPanel extends JPanel
       if (stretchGroup.getSequences(null).contains(nextSeq))
       {
         stretchGroup.deleteSequence(seq, false);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
       else
       {
@@ -1948,7 +1997,7 @@ public class SeqPanel extends JPanel
         }
 
         stretchGroup.addSequence(nextSeq, false);
-        needOverviewUpdate |= av.isSelectionDefinedGroup();
+        updateOverviewAndStructs |= av.isSelectionDefinedGroup();
       }
     }
 
@@ -2243,4 +2292,13 @@ public class SeqPanel extends JPanel
 
     return true;
   }
+
+  /**
+   * 
+   * @return null or last search results handled by this panel
+   */
+  public SearchResultsI getLastSearchResults()
+  {
+    return lastSearchResults;
+  }
 }
index e05230b..f545e70 100755 (executable)
@@ -26,6 +26,7 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.fts.core.GFTSPanel;
 import jalview.fts.service.pdb.PDBFTSPanel;
 import jalview.fts.service.uniprot.UniprotFTSPanel;
 import jalview.io.FileFormatI;
@@ -78,6 +79,8 @@ public class SequenceFetcher extends JPanel implements Runnable
 
   JButton close = new JButton();
 
+  JButton back = new JButton();
+
   JPanel jPanel1 = new JPanel();
 
   JTextArea textArea = new JTextArea();
@@ -270,7 +273,7 @@ public class SequenceFetcher extends JPanel implements Runnable
         return Collections.emptyList();
       }
     }
-    sf.newAlframes = new ArrayList<AlignFrame>();
+    sf.newAlframes = new ArrayList<>();
     sf.run();
     return sf.newAlframes;
   }
@@ -383,6 +386,15 @@ public class SequenceFetcher extends JPanel implements Runnable
                     .getString("label.additional_sequence_fetcher"));
   }
 
+  GFTSPanel parentFTSframe = null;
+  /**
+   * change the buttons so they fit with the FTS panel.
+   */
+  public void embedWithFTSPanel(GFTSPanel toClose)
+  {
+    back.setVisible(true);
+    parentFTSframe = toClose;
+  }
   private void jbInit() throws Exception
   {
     this.setLayout(borderLayout2);
@@ -427,7 +439,7 @@ public class SequenceFetcher extends JPanel implements Runnable
         example_actionPerformed();
       }
     });
-    close.setText(MessageManager.getString("action.close"));
+    close.setText(MessageManager.getString("action.cancel"));
     close.addActionListener(new ActionListener()
     {
       @Override
@@ -436,6 +448,17 @@ public class SequenceFetcher extends JPanel implements Runnable
         close_actionPerformed(e);
       }
     });
+    back.setText(MessageManager.getString("action.back"));
+    back.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        parentFTSframe.btn_back_ActionPerformed();
+      }
+    });
+    // back not visible unless embedded
+    back.setVisible(false);
     textArea.setFont(JvSwingUtils.getLabelFont());
     textArea.setLineWrap(true);
     textArea.addKeyListener(new KeyAdapter()
@@ -451,9 +474,10 @@ public class SequenceFetcher extends JPanel implements Runnable
     });
     jPanel3.setLayout(borderLayout1);
     borderLayout1.setVgap(5);
-    jPanel1.add(ok);
+    jPanel1.add(back);
     jPanel1.add(example);
     jPanel1.add(clear);
+    jPanel1.add(ok);
     jPanel1.add(close);
     jPanel2.setLayout(borderLayout3);
     databaseButt = /*database.getDatabaseSelectorButton();
@@ -582,6 +606,10 @@ public class SequenceFetcher extends JPanel implements Runnable
     try
     {
       frame.setClosed(true);
+      if (parentFTSframe!=null)
+      {
+        parentFTSframe.btn_cancel_ActionPerformed();
+      }
     } catch (Exception ex)
     {
     }
@@ -594,7 +622,7 @@ public class SequenceFetcher extends JPanel implements Runnable
     textArea.setEnabled(false);
     ok.setEnabled(false);
     close.setEnabled(false);
-
+    back.setEnabled(false);
     Thread worker = new Thread(this);
     worker.start();
   }
@@ -606,6 +634,7 @@ public class SequenceFetcher extends JPanel implements Runnable
     textArea.setEnabled(true);
     ok.setEnabled(true);
     close.setEnabled(true);
+    back.setEnabled(parentFTSframe != null);
   }
 
   @Override
@@ -645,10 +674,10 @@ public class SequenceFetcher extends JPanel implements Runnable
     // TODO: Refactor to GUI independent code and write tests.
     // indicate if successive sources should be merged into one alignment.
     boolean addToLast = false;
-    List<String> aresultq = new ArrayList<String>();
-    List<String> presultTitle = new ArrayList<String>();
-    List<AlignmentI> presult = new ArrayList<AlignmentI>();
-    List<AlignmentI> aresult = new ArrayList<AlignmentI>();
+    List<String> aresultq = new ArrayList<>();
+    List<String> presultTitle = new ArrayList<>();
+    List<AlignmentI> presult = new ArrayList<>();
+    List<AlignmentI> aresult = new ArrayList<>();
     Iterator<DbSourceProxy> proxies = database.getSelectedSources()
             .iterator();
     String[] qries;
@@ -666,7 +695,7 @@ public class SequenceFetcher extends JPanel implements Runnable
         nqueries = nextFetch.size();
         // save the remaining queries in the original array
         qries = nextFetch.toArray(new String[nqueries]);
-        nextFetch = new ArrayList<String>();
+        nextFetch = new ArrayList<>();
       }
 
       DbSourceProxy proxy = proxies.next();
@@ -832,7 +861,7 @@ public class SequenceFetcher extends JPanel implements Runnable
           List<AlignmentI> aresult, List<String> nextFetch) throws Exception
   {
     StringBuilder multiacc = new StringBuilder();
-    List<String> tosend = new ArrayList<String>();
+    List<String> tosend = new ArrayList<>();
     while (accessions.hasNext())
     {
       String nel = accessions.next();
@@ -1087,4 +1116,9 @@ public class SequenceFetcher extends JPanel implements Runnable
   {
     frame.setVisible(false);
   }
+
+  public void setDatabaseChooserVisible(boolean b)
+  {
+    databaseButt.setVisible(b);
+  }
 }
index 0a1e8ef..81b394b 100755 (executable)
@@ -481,21 +481,30 @@ public class SequenceRenderer implements jalview.api.SequenceRenderer
     }
   }
 
-  public void drawCursor(SequenceI seq, int res, int x1, int y1)
+  /**
+   * Draw a sequence canvas cursor
+   * 
+   * @param g
+   *          graphics context to draw on
+   * @param s
+   *          character to draw at cursor
+   * @param x1
+   *          x position of cursor in graphics context
+   * @param y1
+   *          y position of cursor in graphics context
+   */
+  public void drawCursor(Graphics g, char s, int x1, int y1)
   {
     int pady = av.getCharHeight() / 5;
     int charOffset = 0;
-    graphics.setColor(Color.black);
-    graphics.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
+    g.setColor(Color.black);
+    g.fillRect(x1, y1, av.getCharWidth(), av.getCharHeight());
 
     if (av.isValidCharWidth())
     {
-      graphics.setColor(Color.white);
-
-      char s = seq.getCharAt(res);
-
+      g.setColor(Color.white);
       charOffset = (av.getCharWidth() - fm.charWidth(s)) / 2;
-      graphics.drawString(String.valueOf(s), charOffset + x1,
+      g.drawString(String.valueOf(s), charOffset + x1,
               (y1 + av.getCharHeight()) - pady);
     }
 
index e6ec822..93a2457 100755 (executable)
@@ -127,7 +127,7 @@ public class SliderPanel extends GSliderPanel
       @Override
       public void mouseReleased(MouseEvent evt)
       {
-        ap.paintAlignment(true);
+        ap.paintAlignment(true, true);
       }
     });
 
index beb2d62..5bff407 100644 (file)
@@ -194,15 +194,29 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
   }
 
   /**
-   * Adjust the divider for a sensible split of the real estate (for example,
+   * Adjusts the divider for a sensible split of the real estate (for example,
    * when many transcripts are shown with a single protein). This should only be
    * called after the split pane has been laid out (made visible) so it has a
-   * height.
+   * height. The aim is to avoid unnecessary vertical scroll bars, while
+   * ensuring that at least 2 sequences are visible in each panel.
+   * <p>
+   * Once laid out, the user may choose to customise as they wish, so this
+   * method is not called again after the initial layout.
    */
-  protected void adjustDivider()
+  protected void adjustInitialLayout()
   {
-    final AlignViewport topViewport = ((AlignFrame) getTopFrame()).viewport;
-    final AlignViewport bottomViewport = ((AlignFrame) getBottomFrame()).viewport;
+    AlignFrame topFrame = (AlignFrame) getTopFrame();
+    AlignFrame bottomFrame = (AlignFrame) getBottomFrame();
+
+    /*
+     * recompute layout of top and bottom panels to reflect their
+     * actual (rather than requested) height
+     */
+    topFrame.alignPanel.adjustAnnotationHeight();
+    bottomFrame.alignPanel.adjustAnnotationHeight();
+
+    final AlignViewport topViewport = topFrame.viewport;
+    final AlignViewport bottomViewport = bottomFrame.viewport;
     final AlignmentI topAlignment = topViewport.getAlignment();
     final AlignmentI bottomAlignment = bottomViewport.getAlignment();
     boolean topAnnotations = topViewport.isShowAnnotation();
@@ -214,6 +228,29 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
     int bottomCharHeight = bottomViewport.getViewStyle().getCharHeight();
 
     /*
+     * calculate the minimum ratio that leaves at least the height 
+     * of two sequences (after rounding) visible in the top panel
+     */
+    int topPanelHeight = topFrame.getHeight();
+    int bottomPanelHeight = bottomFrame.getHeight();
+    int topSequencesHeight = topFrame.alignPanel.getSeqPanel().seqCanvas
+            .getHeight();
+    int topPanelMinHeight = topPanelHeight
+            - Math.max(0, topSequencesHeight - 3 * topCharHeight);
+    double totalHeight = (double) topPanelHeight + bottomPanelHeight;
+    double minRatio = topPanelMinHeight / totalHeight;
+
+    /*
+     * calculate the maximum ratio that leaves at least the height 
+     * of two sequences (after rounding) visible in the bottom panel
+     */
+    int bottomSequencesHeight = bottomFrame.alignPanel.getSeqPanel().seqCanvas
+            .getHeight();
+    int bottomPanelMinHeight = bottomPanelHeight
+            - Math.max(0, bottomSequencesHeight - 3 * bottomCharHeight);
+    double maxRatio = (totalHeight - bottomPanelMinHeight) / totalHeight;
+
+    /*
      * estimate ratio of (topFrameContent / bottomFrameContent)
      */
     int insets = Platform.isAMac() ? MAC_INSETS_HEIGHT
@@ -223,13 +260,14 @@ public class SplitFrame extends GSplitFrame implements SplitContainerI
             + (topAnnotations ? topViewport.calcPanelHeight() : 0);
     int bottomHeight = insets + (3 + bottomCount) * bottomCharHeight
             + (bottomAnnotations ? bottomViewport.calcPanelHeight() : 0);
-    double ratio = ((double) topHeight) / (topHeight + bottomHeight);
+    double ratio = ((double) topHeight)
+            / (double) (topHeight + bottomHeight);
 
     /*
-     * limit to 0.2 <= ratio <= 0.8 to avoid concealing all sequences
+     * limit ratio to avoid concealing all sequences
      */
-    ratio = Math.min(ratio, 0.8d);
-    ratio = Math.max(ratio, 0.2d);
+    ratio = Math.min(ratio, maxRatio);
+    ratio = Math.max(ratio, minRatio);
     setRelativeDividerLocation(ratio);
   }
 
index da10e3f..e18d6af 100644 (file)
@@ -21,6 +21,8 @@
 
 package jalview.gui;
 
+import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
@@ -53,6 +55,8 @@ import java.util.Vector;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JLabel;
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
 import javax.swing.table.AbstractTableModel;
 
 /**
@@ -65,6 +69,8 @@ import javax.swing.table.AbstractTableModel;
 public class StructureChooser extends GStructureChooser
         implements IProgressIndicator
 {
+  private static final String AUTOSUPERIMPOSE = "AUTOSUPERIMPOSE";
+
   private static int MAX_QLENGTH = 7820;
 
   private SequenceI selectedSequence;
@@ -85,6 +91,8 @@ public class StructureChooser extends GStructureChooser
 
   private boolean cachedPDBExists;
 
+  private static StructureViewer lastTargetedView = null;
+
   public StructureChooser(SequenceI[] selectedSeqs, SequenceI selectedSeq,
           AlignmentPanel ap)
   {
@@ -98,13 +106,15 @@ public class StructureChooser extends GStructureChooser
   /**
    * Initializes parameters used by the Structure Chooser Panel
    */
-  public void init()
+  protected void init()
   {
     if (!Jalview.isHeadlessMode())
     {
       progressBar = new ProgressBar(this.statusPanel, this.statusBar);
     }
 
+    chk_superpose.setSelected(Cache.getDefault(AUTOSUPERIMPOSE, true));
+
     // ensure a filter option is in force for search
     populateFilterComboBox(true, cachedPDBExists);
     Thread discoverPDBStructuresThread = new Thread(new Runnable()
@@ -122,6 +132,7 @@ public class StructureChooser extends GStructureChooser
         fetchStructuresMetaData();
         // revise filter options if no results were found
         populateFilterComboBox(isStructuresDiscovered(), cachedPDBExists);
+        discoverStructureViews();
         updateProgressIndicator(null, startTime);
         mainFrame.setVisible(true);
         updateCurrentView();
@@ -131,6 +142,59 @@ public class StructureChooser extends GStructureChooser
   }
 
   /**
+   * Builds a drop-down choice list of existing structure viewers to which new
+   * structures may be added. If this list is empty then it, and the 'Add'
+   * button, are hidden.
+   */
+  private void discoverStructureViews()
+  {
+    if (Desktop.instance != null)
+    {
+      targetView.removeAllItems();
+      if (lastTargetedView != null && !lastTargetedView.isVisible())
+      {
+        lastTargetedView = null;
+      }
+      int linkedViewsAt = 0;
+      for (StructureViewerBase view : Desktop.instance
+              .getStructureViewers(null, null))
+      {
+        StructureViewer viewHandler = (lastTargetedView != null
+                && lastTargetedView.sview == view) ? lastTargetedView
+                        : StructureViewer.reconfigure(view);
+
+        if (view.isLinkedWith(ap))
+        {
+          targetView.insertItemAt(viewHandler,
+                  linkedViewsAt++);
+        }
+        else
+        {
+          targetView.addItem(viewHandler);
+        }
+      }
+
+      /*
+       * show option to Add to viewer if at least 1 viewer found
+       */
+      targetView.setVisible(false);
+      if (targetView.getItemCount() > 0)
+      {
+        targetView.setVisible(true);
+        if (lastTargetedView != null)
+        {
+          targetView.setSelectedItem(lastTargetedView);
+        }
+        else
+        {
+          targetView.setSelectedIndex(0);
+        }
+      }
+      btn_add.setVisible(targetView.isVisible());
+    }
+  }
+
+  /**
    * Updates the progress indicator with the specified message
    * 
    * @param message
@@ -138,7 +202,7 @@ public class StructureChooser extends GStructureChooser
    * @param id
    *          unique handle for this indicator
    */
-  public void updateProgressIndicator(String message, long id)
+  protected void updateProgressIndicator(String message, long id)
   {
     if (progressIndicator != null)
     {
@@ -150,15 +214,15 @@ public class StructureChooser extends GStructureChooser
    * Retrieve meta-data for all the structure(s) for a given sequence(s) in a
    * selection group
    */
-  public void fetchStructuresMetaData()
+  void fetchStructuresMetaData()
   {
     long startTime = System.currentTimeMillis();
     pdbRestCleint = PDBFTSRestClient.getInstance();
     Collection<FTSDataColumnI> wantedFields = pdbDocFieldPrefs
             .getStructureSummaryFields();
 
-    discoveredStructuresSet = new LinkedHashSet<FTSData>();
-    HashSet<String> errors = new HashSet<String>();
+    discoveredStructuresSet = new LinkedHashSet<>();
+    HashSet<String> errors = new HashSet<>();
     for (SequenceI seq : selectedSequences)
     {
       FTSRestRequest pdbRequest = new FTSRestRequest();
@@ -221,9 +285,9 @@ public class StructureChooser extends GStructureChooser
     }
   }
 
-  public void loadLocalCachedPDBEntries()
+  protected void loadLocalCachedPDBEntries()
   {
-    ArrayList<CachedPDB> entries = new ArrayList<CachedPDB>();
+    ArrayList<CachedPDB> entries = new ArrayList<>();
     for (SequenceI seq : selectedSequences)
     {
       if (seq.getDatasetSequence() != null
@@ -252,12 +316,12 @@ public class StructureChooser extends GStructureChooser
    * @return the built query string
    */
 
-  public static String buildQuery(SequenceI seq)
+  static String buildQuery(SequenceI seq)
   {
     boolean isPDBRefsFound = false;
     boolean isUniProtRefsFound = false;
     StringBuilder queryBuilder = new StringBuilder();
-    Set<String> seqRefs = new LinkedHashSet<String>();
+    Set<String> seqRefs = new LinkedHashSet<>();
 
     if (seq.getAllPDBEntries() != null
             && queryBuilder.length() < MAX_QLENGTH)
@@ -354,7 +418,7 @@ public class StructureChooser extends GStructureChooser
    * @param seqName
    * @return
    */
-  public static boolean isValidSeqName(String seqName)
+  static boolean isValidSeqName(String seqName)
   {
     // System.out.println("seqName : " + seqName);
     String ignoreList = "pdb,uniprot,swiss-prot";
@@ -377,7 +441,7 @@ public class StructureChooser extends GStructureChooser
     return true;
   }
 
-  public static String getDBRefId(DBRefEntry dbRef)
+  static String getDBRefId(DBRefEntry dbRef)
   {
     String ref = dbRef.getAccessionId().replaceAll("GO:", "");
     return ref;
@@ -389,7 +453,7 @@ public class StructureChooser extends GStructureChooser
    * @param fieldToFilterBy
    *          the field to filter by
    */
-  public void filterResultSet(final String fieldToFilterBy)
+  void filterResultSet(final String fieldToFilterBy)
   {
     Thread filterThread = new Thread(new Runnable()
     {
@@ -401,8 +465,8 @@ public class StructureChooser extends GStructureChooser
         lbl_loading.setVisible(true);
         Collection<FTSDataColumnI> wantedFields = pdbDocFieldPrefs
                 .getStructureSummaryFields();
-        Collection<FTSData> filteredResponse = new HashSet<FTSData>();
-        HashSet<String> errors = new HashSet<String>();
+        Collection<FTSData> filteredResponse = new HashSet<>();
+        HashSet<String> errors = new HashSet<>();
 
         for (SequenceI seq : selectedSequences)
         {
@@ -453,7 +517,7 @@ public class StructureChooser extends GStructureChooser
         if (!filteredResponse.isEmpty())
         {
           final int filterResponseCount = filteredResponse.size();
-          Collection<FTSData> reorderedStructuresSet = new LinkedHashSet<FTSData>();
+          Collection<FTSData> reorderedStructuresSet = new LinkedHashSet<>();
           reorderedStructuresSet.addAll(filteredResponse);
           reorderedStructuresSet.addAll(discoveredStructuresSet);
           getResultTable().setModel(FTSRestResponse
@@ -499,7 +563,7 @@ public class StructureChooser extends GStructureChooser
    * Handles action event for btn_pdbFromFile
    */
   @Override
-  public void pdbFromFile_actionPerformed()
+  protected void pdbFromFile_actionPerformed()
   {
     jalview.io.JalviewFileChooser chooser = new jalview.io.JalviewFileChooser(
             jalview.bin.Cache.getProperty("LAST_DIRECTORY"));
@@ -525,7 +589,7 @@ public class StructureChooser extends GStructureChooser
    * structures
    */
   protected void populateFilterComboBox(boolean haveData,
-          boolean cachedPDBExists)
+          boolean cachedPDBExist)
   {
     /*
      * temporarily suspend the change listener behaviour
@@ -535,25 +599,33 @@ public class StructureChooser extends GStructureChooser
     cmb_filterOption.removeAllItems();
     if (haveData)
     {
-      cmb_filterOption.addItem(new FilterOption("Best Quality",
+      cmb_filterOption.addItem(new FilterOption(
+              MessageManager.getString("label.best_quality"),
               "overall_quality", VIEWS_FILTER, false));
-      cmb_filterOption.addItem(new FilterOption("Best Resolution",
+      cmb_filterOption.addItem(new FilterOption(
+              MessageManager.getString("label.best_resolution"),
               "resolution", VIEWS_FILTER, false));
-      cmb_filterOption.addItem(new FilterOption("Most Protein Chain",
+      cmb_filterOption.addItem(new FilterOption(
+              MessageManager.getString("label.most_protein_chain"),
               "number_of_protein_chains", VIEWS_FILTER, false));
-      cmb_filterOption.addItem(new FilterOption("Most Bound Molecules",
+      cmb_filterOption.addItem(new FilterOption(
+              MessageManager.getString("label.most_bound_molecules"),
               "number_of_bound_molecules", VIEWS_FILTER, false));
-      cmb_filterOption.addItem(new FilterOption("Most Polymer Residues",
+      cmb_filterOption.addItem(new FilterOption(
+              MessageManager.getString("label.most_polymer_residues"),
               "number_of_polymer_residues", VIEWS_FILTER, true));
     }
     cmb_filterOption.addItem(
-            new FilterOption("Enter PDB Id", "-", VIEWS_ENTER_ID, false));
+            new FilterOption(MessageManager.getString("label.enter_pdb_id"),
+                    "-", VIEWS_ENTER_ID, false));
     cmb_filterOption.addItem(
-            new FilterOption("From File", "-", VIEWS_FROM_FILE, false));
+            new FilterOption(MessageManager.getString("label.from_file"),
+                    "-", VIEWS_FROM_FILE, false));
 
-    if (cachedPDBExists)
+    if (cachedPDBExist)
     {
-      FilterOption cachedOption = new FilterOption("Cached PDB Entries",
+      FilterOption cachedOption = new FilterOption(
+              MessageManager.getString("label.cached_structures"),
               "-", VIEWS_LOCAL_PDB, false);
       cmb_filterOption.addItem(cachedOption);
       cmb_filterOption.setSelectedItem(cachedOption);
@@ -592,28 +664,37 @@ public class StructureChooser extends GStructureChooser
   }
 
   /**
-   * Validates user selection and activates the view button if all parameters
-   * are correct
+   * Validates user selection and enables the 'Add' and 'New View' buttons if
+   * all parameters are correct (the Add button will only be visible if there is
+   * at least one existing structure viewer open). This basically means at least
+   * one structure selected and no error messages.
+   * <p>
+   * The 'Superpose Structures' option is enabled if either more than one
+   * structure is selected, or the 'Add' to existing view option is enabled, and
+   * disabled if the only option is to open a new view of a single structure.
    */
   @Override
-  public void validateSelections()
+  protected void validateSelections()
   {
     FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
             .getSelectedItem());
-    btn_view.setEnabled(false);
+    btn_add.setEnabled(false);
     String currentView = selectedFilterOpt.getView();
+    int selectedCount = 0;
     if (currentView == VIEWS_FILTER)
     {
-      if (getResultTable().getSelectedRows().length > 0)
+      selectedCount = getResultTable().getSelectedRows().length;
+      if (selectedCount > 0)
       {
-        btn_view.setEnabled(true);
+        btn_add.setEnabled(true);
       }
     }
     else if (currentView == VIEWS_LOCAL_PDB)
     {
-      if (tbl_local_pdb.getSelectedRows().length > 0)
+      selectedCount = tbl_local_pdb.getSelectedRows().length;
+      if (selectedCount > 0)
       {
-        btn_view.setEnabled(true);
+        btn_add.setEnabled(true);
       }
     }
     else if (currentView == VIEWS_ENTER_ID)
@@ -624,12 +705,21 @@ public class StructureChooser extends GStructureChooser
     {
       validateAssociationFromFile();
     }
+
+    btn_newView.setEnabled(btn_add.isEnabled());
+
+    /*
+     * enable 'Superpose' option if more than one structure is selected,
+     * or there are view(s) available to add structure(s) to
+     */
+    chk_superpose
+            .setEnabled(selectedCount > 1 || targetView.getItemCount() > 0);
   }
 
   /**
    * Validates inputs from the Manual PDB entry panel
    */
-  public void validateAssociationEnterPdb()
+  protected void validateAssociationEnterPdb()
   {
     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) idInputAssSeqPanel
             .getCmb_assSeq().getSelectedItem();
@@ -655,7 +745,7 @@ public class StructureChooser extends GStructureChooser
       txt_search.setEnabled(true);
       if (isValidPBDEntry)
       {
-        btn_view.setEnabled(true);
+        btn_add.setEnabled(true);
         lbl_pdbManualFetchStatus.setToolTipText("");
         lbl_pdbManualFetchStatus.setIcon(goodImage);
       }
@@ -670,7 +760,7 @@ public class StructureChooser extends GStructureChooser
   /**
    * Validates inputs for the manual PDB file selection options
    */
-  public void validateAssociationFromFile()
+  protected void validateAssociationFromFile()
   {
     AssociateSeqOptions assSeqOpt = (AssociateSeqOptions) fileChooserAssSeqPanel
             .getCmb_assSeq().getSelectedItem();
@@ -681,7 +771,7 @@ public class StructureChooser extends GStructureChooser
       btn_pdbFromFile.setEnabled(true);
       if (selectedPdbFileName != null && selectedPdbFileName.length() > 0)
       {
-        btn_view.setEnabled(true);
+        btn_add.setEnabled(true);
         lbl_fromFileStatus.setIcon(goodImage);
       }
     }
@@ -693,7 +783,7 @@ public class StructureChooser extends GStructureChooser
   }
 
   @Override
-  public void cmbAssSeqStateChanged()
+  protected void cmbAssSeqStateChanged()
   {
     validateSelections();
   }
@@ -720,17 +810,76 @@ public class StructureChooser extends GStructureChooser
   }
 
   /**
-   * Handles action event for btn_ok
+   * select structures for viewing by their PDB IDs
+   * 
+   * @param pdbids
+   * @return true if structures were found and marked as selected
+   */
+  public boolean selectStructure(String... pdbids)
+  {
+    boolean found = false;
+
+    FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
+            .getSelectedItem());
+    String currentView = selectedFilterOpt.getView();
+    JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
+            : (currentView == VIEWS_LOCAL_PDB) ? tbl_local_pdb : null;
+
+    if (restable == null)
+    {
+      // can't select (enter PDB ID, or load file - need to also select which
+      // sequence to associate with)
+      return false;
+    }
+
+    int pdbIdColIndex = restable.getColumn("PDB Id").getModelIndex();
+    for (int r = 0; r < restable.getRowCount(); r++)
+    {
+      for (int p = 0; p < pdbids.length; p++)
+      {
+        if (String.valueOf(restable.getValueAt(r, pdbIdColIndex))
+                .equalsIgnoreCase(pdbids[p]))
+        {
+          restable.setRowSelectionInterval(r, r);
+          found = true;
+        }
+      }
+    }
+    return found;
+  }
+  
+  /**
+   * Handles the 'New View' action
+   */
+  @Override
+  protected void newView_ActionPerformed()
+  {
+    targetView.setSelectedItem(null);
+    showStructures(false);
+  }
+
+  /**
+   * Handles the 'Add to existing viewer' action
    */
   @Override
-  public void ok_ActionPerformed()
+  protected void add_ActionPerformed()
+  {
+    showStructures(false);
+  }
+
+  /**
+   * structure viewer opened by this dialog, or null
+   */
+  private StructureViewer sViewer = null;
+
+  public void showStructures(boolean waitUntilFinished)
   {
-    final long progressSessionId = System.currentTimeMillis();
+
     final StructureSelectionManager ssm = ap.getStructureSelectionManager();
+
     final int preferredHeight = pnl_filter.getHeight();
-    ssm.setProgressIndicator(this);
-    ssm.setProgressSessionId(progressSessionId);
-    new Thread(new Runnable()
+
+    Runnable viewStruc = new Runnable()
     {
       @Override
       public void run()
@@ -738,21 +887,24 @@ public class StructureChooser extends GStructureChooser
         FilterOption selectedFilterOpt = ((FilterOption) cmb_filterOption
                 .getSelectedItem());
         String currentView = selectedFilterOpt.getView();
+        JTable restable = (currentView == VIEWS_FILTER) ? getResultTable()
+                : tbl_local_pdb;
+
         if (currentView == VIEWS_FILTER)
         {
-          int pdbIdColIndex = getResultTable().getColumn("PDB Id")
+          int pdbIdColIndex = restable.getColumn("PDB Id")
                   .getModelIndex();
-          int refSeqColIndex = getResultTable().getColumn("Ref Sequence")
+          int refSeqColIndex = restable.getColumn("Ref Sequence")
                   .getModelIndex();
-          int[] selectedRows = getResultTable().getSelectedRows();
+          int[] selectedRows = restable.getSelectedRows();
           PDBEntry[] pdbEntriesToView = new PDBEntry[selectedRows.length];
           int count = 0;
-          List<SequenceI> selectedSeqsToView = new ArrayList<SequenceI>();
+          List<SequenceI> selectedSeqsToView = new ArrayList<>();
           for (int row : selectedRows)
           {
-            String pdbIdStr = getResultTable()
+            String pdbIdStr = restable
                     .getValueAt(row, pdbIdColIndex).toString();
-            SequenceI selectedSeq = (SequenceI) getResultTable()
+            SequenceI selectedSeq = (SequenceI) restable
                     .getValueAt(row, refSeqColIndex);
             selectedSeqsToView.add(selectedSeq);
             PDBEntry pdbEntry = selectedSeq.getPDBEntry(pdbIdStr);
@@ -761,6 +913,7 @@ public class StructureChooser extends GStructureChooser
               pdbEntry = getFindEntry(pdbIdStr,
                       selectedSeq.getAllPDBEntries());
             }
+
             if (pdbEntry == null)
             {
               pdbEntry = new PDBEntry();
@@ -772,7 +925,8 @@ public class StructureChooser extends GStructureChooser
           }
           SequenceI[] selectedSeqs = selectedSeqsToView
                   .toArray(new SequenceI[selectedSeqsToView.size()]);
-          launchStructureViewer(ssm, pdbEntriesToView, ap, selectedSeqs);
+          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
+                  selectedSeqs);
         }
         else if (currentView == VIEWS_LOCAL_PDB)
         {
@@ -783,7 +937,7 @@ public class StructureChooser extends GStructureChooser
                   .getModelIndex();
           int refSeqColIndex = tbl_local_pdb.getColumn("Ref Sequence")
                   .getModelIndex();
-          List<SequenceI> selectedSeqsToView = new ArrayList<SequenceI>();
+          List<SequenceI> selectedSeqsToView = new ArrayList<>();
           for (int row : selectedRows)
           {
             PDBEntry pdbEntry = (PDBEntry) tbl_local_pdb.getValueAt(row,
@@ -795,7 +949,8 @@ public class StructureChooser extends GStructureChooser
           }
           SequenceI[] selectedSeqs = selectedSeqsToView
                   .toArray(new SequenceI[selectedSeqsToView.size()]);
-          launchStructureViewer(ssm, pdbEntriesToView, ap, selectedSeqs);
+          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
+                  selectedSeqs);
         }
         else if (currentView == VIEWS_ENTER_ID)
         {
@@ -805,7 +960,6 @@ public class StructureChooser extends GStructureChooser
           {
             selectedSequence = userSelectedSeq;
           }
-
           String pdbIdStr = txt_search.getText();
           PDBEntry pdbEntry = selectedSequence.getPDBEntry(pdbIdStr);
           if (pdbEntry == null)
@@ -825,7 +979,7 @@ public class StructureChooser extends GStructureChooser
           }
 
           PDBEntry[] pdbEntriesToView = new PDBEntry[] { pdbEntry };
-          launchStructureViewer(ssm, pdbEntriesToView, ap,
+          sViewer = launchStructureViewer(ssm, pdbEntriesToView, ap,
                   new SequenceI[]
                   { selectedSequence });
         }
@@ -842,13 +996,40 @@ public class StructureChooser extends GStructureChooser
                           DataSourceType.FILE, selectedSequence, true,
                           Desktop.instance);
 
-          launchStructureViewer(ssm, new PDBEntry[] { fileEntry }, ap,
+          sViewer = launchStructureViewer(
+                  ssm, new PDBEntry[]
+                  { fileEntry }, ap,
                   new SequenceI[]
                   { selectedSequence });
         }
-        closeAction(preferredHeight);
+        SwingUtilities.invokeLater(new Runnable()
+        {
+          @Override
+          public void run()
+          {
+            closeAction(preferredHeight);
+            mainFrame.dispose();
+          }
+        });
       }
-    }).start();
+    };
+    Thread runner = new Thread(viewStruc);
+    runner.start();
+    if (waitUntilFinished)
+    {
+      while (sViewer == null ? runner.isAlive()
+              : (sViewer.sview == null ? true
+                      : !sViewer.sview.hasMapping()))
+      {
+        try
+        {
+          Thread.sleep(300);
+        } catch (InterruptedException ie)
+        {
+
+        }
+      }
+    }
   }
 
   private PDBEntry getFindEntry(String id, Vector<PDBEntry> pdbEntries)
@@ -866,17 +1047,52 @@ public class StructureChooser extends GStructureChooser
     return foundEntry;
   }
 
-  private void launchStructureViewer(StructureSelectionManager ssm,
+  /**
+   * Answers a structure viewer (new or existing) configured to superimpose
+   * added structures or not according to the user's choice
+   * 
+   * @param ssm
+   * @return
+   */
+  StructureViewer getTargetedStructureViewer(
+          StructureSelectionManager ssm)
+  {
+    Object sv = targetView.getSelectedItem();
+
+    return sv == null ? new StructureViewer(ssm) : (StructureViewer) sv;
+  }
+
+  /**
+   * 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)
   {
-    ssm.setProgressBar(MessageManager
-            .getString("status.launching_3d_structure_viewer"));
-    final StructureViewer sViewer = new StructureViewer(ssm);
+    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<SequenceI>();
+      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
@@ -898,7 +1114,7 @@ public class StructureChooser extends GStructureChooser
             }
           }
         }
-        if (seq.getPrimaryDBRefs().size() == 0)
+        if (seq.getPrimaryDBRefs().isEmpty())
         {
           seqsWithoutSourceDBRef.add(seq);
           continue;
@@ -907,41 +1123,35 @@ public class StructureChooser extends GStructureChooser
       if (!seqsWithoutSourceDBRef.isEmpty())
       {
         int y = seqsWithoutSourceDBRef.size();
-        ssm.setProgressBar(null);
-        ssm.setProgressBar(MessageManager.formatMessage(
+        setProgressBar(MessageManager.formatMessage(
                 "status.fetching_dbrefs_for_sequences_without_valid_refs",
-                y));
-        SequenceI[] seqWithoutSrcDBRef = new SequenceI[y];
-        int x = 0;
-        for (SequenceI fSeq : seqsWithoutSourceDBRef)
-        {
-          seqWithoutSrcDBRef[x++] = fSeq;
-        }
+                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)
     {
-      ArrayList<SequenceI[]> seqsMap = new ArrayList<SequenceI[]>();
-      for (SequenceI seq : sequences)
-      {
-        seqsMap.add(new SequenceI[] { seq });
-      }
-      SequenceI[][] collatedSeqs = seqsMap.toArray(new SequenceI[0][0]);
-      ssm.setProgressBar(null);
-      ssm.setProgressBar(MessageManager.getString(
-              "status.fetching_3d_structures_for_selected_entries"));
-      sViewer.viewStructures(pdbEntriesToView, collatedSeqs, alignPanel);
+      setProgressBar(MessageManager.getString(
+              "status.fetching_3d_structures_for_selected_entries"),
+              progressId);
+      theViewer.viewStructures(pdbEntriesToView, sequences, alignPanel);
     }
     else
     {
-      ssm.setProgressBar(null);
-      ssm.setProgressBar(MessageManager.formatMessage(
+      setProgressBar(MessageManager.formatMessage(
               "status.fetching_3d_structures_for",
-              pdbEntriesToView[0].getId()));
-      sViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
+              pdbEntriesToView[0].getId()),progressId);
+      theViewer.viewStructures(pdbEntriesToView[0], sequences, alignPanel);
     }
+    setProgressBar(null, progressId);
+    // remember the last viewer we used...
+    lastTargetedView = theViewer;
+    return theViewer;
   }
 
   /**
@@ -949,7 +1159,7 @@ public class StructureChooser extends GStructureChooser
    * a unique sequence when more than one sequence selection is made.
    */
   @Override
-  public void populateCmbAssociateSeqOptions(
+  protected void populateCmbAssociateSeqOptions(
           JComboBox<AssociateSeqOptions> cmb_assSeq,
           JLabel lbl_associateSeq)
   {
@@ -974,17 +1184,12 @@ public class StructureChooser extends GStructureChooser
     }
   }
 
-  public boolean isStructuresDiscovered()
+  protected boolean isStructuresDiscovered()
   {
     return discoveredStructuresSet != null
             && !discoveredStructuresSet.isEmpty();
   }
 
-  public Collection<FTSData> getDiscoveredStructuresSet()
-  {
-    return discoveredStructuresSet;
-  }
-
   @Override
   protected void txt_search_ActionPerformed()
   {
@@ -1000,7 +1205,7 @@ public class StructureChooser extends GStructureChooser
           String searchTerm = txt_search.getText().toLowerCase();
           searchTerm = searchTerm.split(":")[0];
           // System.out.println(">>>>> search term : " + searchTerm);
-          List<FTSDataColumnI> wantedFields = new ArrayList<FTSDataColumnI>();
+          List<FTSDataColumnI> wantedFields = new ArrayList<>();
           FTSRestRequest pdbRequest = new FTSRestRequest();
           pdbRequest.setAllowEmptySeq(false);
           pdbRequest.setResponseSize(1);
@@ -1034,7 +1239,7 @@ public class StructureChooser extends GStructureChooser
   }
 
   @Override
-  public void tabRefresh()
+  protected void tabRefresh()
   {
     if (selectedSequences != null)
     {
@@ -1062,7 +1267,7 @@ public class StructureChooser extends GStructureChooser
 
     public PDBEntryTableModel(List<CachedPDB> pdbEntries)
     {
-      this.pdbEntries = new ArrayList<CachedPDB>(pdbEntries);
+      this.pdbEntries = new ArrayList<>(pdbEntries);
     }
 
     @Override
@@ -1172,4 +1377,9 @@ public class StructureChooser extends GStructureChooser
   {
     return progressBar.operationInProgress();
   }
+
+  public JalviewStructureDisplayI getOpenedStructureViewer()
+  {
+    return sViewer == null ? null : sViewer.sview;
+  }
 }
index e58b378..0c8354b 100644 (file)
@@ -29,26 +29,67 @@ import jalview.structure.StructureSelectionManager;
 
 import java.awt.Rectangle;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
- * proxy for handling structure viewers.
- * 
- * this allows new views to be created with the currently configured viewer, the
- * preferred viewer to be set/read and existing views created previously with a
- * particular viewer to be recovered
+ * A proxy for handling structure viewers, that orchestrates adding selected
+ * structures, associated with sequences in Jalview, to an existing viewer, or
+ * opening a new one. Currently supports either Jmol or Chimera as the structure
+ * viewer.
  * 
  * @author jprocter
  */
 public class StructureViewer
 {
+  private static final String UNKNOWN_VIEWER_TYPE = "Unknown structure viewer type ";
+
   StructureSelectionManager ssm;
 
+  /**
+   * decide if new structures are aligned to existing ones
+   */
+  private boolean superposeAdded = true;
+
   public enum ViewerType
   {
     JMOL, CHIMERA
   };
 
+  /**
+   * Constructor
+   * 
+   * @param structureSelectionManager
+   */
+  public StructureViewer(StructureSelectionManager structureSelectionManager)
+  {
+    ssm = structureSelectionManager;
+  }
+
+  /**
+   * Factory to create a proxy for modifying existing structure viewer
+   * 
+   */
+  public static StructureViewer reconfigure(
+          JalviewStructureDisplayI display)
+  {
+    StructureViewer sv = new StructureViewer(display.getBinding().getSsm());
+    sv.sview = display;
+    return sv;
+  }
+
+  @Override
+  public String toString()
+  {
+    if (sview != null)
+    {
+      return sview.toString();
+    }
+    return "New View";
+  }
   public ViewerType getViewerType()
   {
     String viewType = Cache.getDefault(Preferences.STRUCTURE_DISPLAY,
@@ -61,29 +102,142 @@ public class StructureViewer
     Cache.setProperty(Preferences.STRUCTURE_DISPLAY, type.name());
   }
 
-  public StructureViewer(
-          StructureSelectionManager structureSelectionManager)
-  {
-    ssm = structureSelectionManager;
-  }
-
   /**
    * View multiple PDB entries, each with associated sequences
    * 
    * @param pdbs
-   * @param seqsForPdbs
+   * @param seqs
    * @param ap
    * @return
    */
   public JalviewStructureDisplayI viewStructures(PDBEntry[] pdbs,
-          SequenceI[][] seqsForPdbs, AlignmentPanel ap)
+          SequenceI[] seqs, AlignmentPanel ap)
   {
-    JalviewStructureDisplayI viewer = onlyOnePdb(pdbs, seqsForPdbs, ap);
+    JalviewStructureDisplayI viewer = onlyOnePdb(pdbs, seqs, ap);
     if (viewer != null)
     {
+      /*
+       * user added structure to an existing viewer - all done
+       */
       return viewer;
     }
-    return viewStructures(getViewerType(), pdbs, seqsForPdbs, ap);
+
+    ViewerType viewerType = getViewerType();
+
+    Map<PDBEntry, SequenceI[]> seqsForPdbs = getSequencesForPdbs(pdbs,
+            seqs);
+    PDBEntry[] pdbsForFile = seqsForPdbs.keySet().toArray(
+            new PDBEntry[seqsForPdbs.size()]);
+    SequenceI[][] theSeqs = seqsForPdbs.values().toArray(
+            new SequenceI[seqsForPdbs.size()][]);
+    if (sview != null)
+    {
+      sview.setAlignAddedStructures(superposeAdded);
+      new Thread(new Runnable()
+      {
+        @Override
+        public void run()
+        {
+
+          for (int pdbep = 0; pdbep < pdbsForFile.length; pdbep++)
+          {
+            PDBEntry pdb = pdbsForFile[pdbep];
+            if (!sview.addAlreadyLoadedFile(theSeqs[pdbep], null, ap,
+                    pdb.getId()))
+            {
+              sview.addToExistingViewer(pdb, theSeqs[pdbep], null, ap,
+                      pdb.getId());
+            }
+          }
+
+          sview.updateTitleAndMenus();
+        }
+      }).start();
+      return sview;
+    }
+
+    if (viewerType.equals(ViewerType.JMOL))
+    {
+      sview = new AppJmol(ap, superposeAdded, pdbsForFile, theSeqs);
+    }
+    else if (viewerType.equals(ViewerType.CHIMERA))
+    {
+      sview = new ChimeraViewFrame(pdbsForFile, superposeAdded, theSeqs,
+              ap);
+    }
+    else
+    {
+      Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
+    }
+    return sview;
+  }
+
+  /**
+   * Converts the list of selected PDB entries (possibly including duplicates
+   * for multiple chains), and corresponding sequences, into a map of sequences
+   * for each distinct PDB file. Returns null if either argument is null, or
+   * their lengths do not match.
+   * 
+   * @param pdbs
+   * @param seqs
+   * @return
+   */
+  Map<PDBEntry, SequenceI[]> getSequencesForPdbs(PDBEntry[] pdbs,
+          SequenceI[] seqs)
+  {
+    if (pdbs == null || seqs == null || pdbs.length != seqs.length)
+    {
+      return null;
+    }
+
+    /*
+     * we want only one 'representative' PDBEntry per distinct file name
+     * (there may be entries for distinct chains)
+     */
+    Map<String, PDBEntry> pdbsSeen = new HashMap<>();
+
+    /*
+     * LinkedHashMap preserves order of PDB entries (significant if they
+     * will get superimposed to the first structure)
+     */
+    Map<PDBEntry, List<SequenceI>> pdbSeqs = new LinkedHashMap<>();
+    for (int i = 0; i < pdbs.length; i++)
+    {
+      PDBEntry pdb = pdbs[i];
+      SequenceI seq = seqs[i];
+      String pdbFile = pdb.getFile();
+      if (pdbFile == null || pdbFile.length() == 0)
+      {
+        pdbFile = pdb.getId();
+      }
+      if (!pdbsSeen.containsKey(pdbFile))
+      {
+        pdbsSeen.put(pdbFile, pdb);
+        pdbSeqs.put(pdb, new ArrayList<SequenceI>());
+      }
+      else
+      {
+        pdb = pdbsSeen.get(pdbFile);
+      }
+      List<SequenceI> seqsForPdb = pdbSeqs.get(pdb);
+      if (!seqsForPdb.contains(seq))
+      {
+        seqsForPdb.add(seq);
+      }
+    }
+
+    /*
+     * convert to Map<PDBEntry, SequenceI[]>
+     */
+    Map<PDBEntry, SequenceI[]> result = new LinkedHashMap<>();
+    for (Entry<PDBEntry, List<SequenceI>> entry : pdbSeqs.entrySet())
+    {
+      List<SequenceI> theSeqs = entry.getValue();
+      result.put(entry.getKey(),
+              theSeqs.toArray(new SequenceI[theSeqs.size()]));
+    }
+
+    return result;
   }
 
   /**
@@ -99,9 +253,9 @@ public class StructureViewer
    * @return
    */
   private JalviewStructureDisplayI onlyOnePdb(PDBEntry[] pdbs,
-          SequenceI[][] seqsForPdbs, AlignmentPanel ap)
+          SequenceI[] seqsForPdbs, AlignmentPanel ap)
   {
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceI> seqs = new ArrayList<>();
     if (pdbs == null || pdbs.length == 0)
     {
       return null;
@@ -115,82 +269,34 @@ public class StructureViewer
       {
         return null;
       }
-      SequenceI[] pdbseqs = seqsForPdbs[i++];
-      if (pdbseqs != null)
+      SequenceI pdbseq = seqsForPdbs[i++];
+      if (pdbseq != null)
       {
-        for (SequenceI sq : pdbseqs)
-        {
-          seqs.add(sq);
-        }
+        seqs.add(pdbseq);
       }
     }
     return viewStructures(pdbs[0], seqs.toArray(new SequenceI[seqs.size()]),
             ap);
   }
 
+  JalviewStructureDisplayI sview = null;
+
   public JalviewStructureDisplayI viewStructures(PDBEntry pdb,
           SequenceI[] seqsForPdb, AlignmentPanel ap)
   {
-    return viewStructures(getViewerType(), pdb, seqsForPdb, ap);
-  }
-
-  protected JalviewStructureDisplayI viewStructures(ViewerType viewerType,
-          PDBEntry[] pdbs, SequenceI[][] seqsForPdbs, AlignmentPanel ap)
-  {
-    PDBEntry[] pdbsForFile = getUniquePdbFiles(pdbs);
-    JalviewStructureDisplayI sview = null;
-    if (viewerType.equals(ViewerType.JMOL))
-    {
-      sview = new AppJmol(ap, pdbsForFile,
-              ap.av.collateForPDB(pdbsForFile));
-    }
-    else if (viewerType.equals(ViewerType.CHIMERA))
-    {
-      sview = new ChimeraViewFrame(pdbsForFile,
-              ap.av.collateForPDB(pdbsForFile), ap);
-    }
-    else
-    {
-      Cache.log.error("Unknown structure viewer type "
-              + getViewerType().toString());
-    }
-    return sview;
-  }
-
-  /**
-   * Convert the array of PDBEntry into an array with no filename repeated
-   * 
-   * @param pdbs
-   * @return
-   */
-  static PDBEntry[] getUniquePdbFiles(PDBEntry[] pdbs)
-  {
-    if (pdbs == null)
-    {
-      return null;
-    }
-    List<PDBEntry> uniques = new ArrayList<PDBEntry>();
-    List<String> filesSeen = new ArrayList<String>();
-    for (PDBEntry entry : pdbs)
+    if (sview != null)
     {
-      String file = entry.getFile();
-      if (file == null)
-      {
-        uniques.add(entry);
-      }
-      else if (!filesSeen.contains(file))
+      sview.setAlignAddedStructures(superposeAdded);
+      String pdbId = pdb.getId();
+      if (!sview.addAlreadyLoadedFile(seqsForPdb, null, ap, pdbId))
       {
-        uniques.add(entry);
-        filesSeen.add(file);
+        sview.addToExistingViewer(pdb, seqsForPdb, null, ap, pdbId);
       }
+      sview.updateTitleAndMenus();
+      sview.raiseViewer();
+      return sview;
     }
-    return uniques.toArray(new PDBEntry[uniques.size()]);
-  }
-
-  protected JalviewStructureDisplayI viewStructures(ViewerType viewerType,
-          PDBEntry pdb, SequenceI[] seqsForPdb, AlignmentPanel ap)
-  {
-    JalviewStructureDisplayI sview = null;
+    ViewerType viewerType = getViewerType();
     if (viewerType.equals(ViewerType.JMOL))
     {
       sview = new AppJmol(pdb, seqsForPdb, null, ap);
@@ -201,8 +307,7 @@ public class StructureViewer
     }
     else
     {
-      Cache.log.error("Unknown structure viewer type "
-              + getViewerType().toString());
+      Cache.log.error(UNKNOWN_VIEWER_TYPE + getViewerType().toString());
     }
     return sview;
   }
@@ -230,7 +335,6 @@ public class StructureViewer
     final boolean usetoColourbyseq = viewerData.isColourWithAlignPanel();
     final boolean viewerColouring = viewerData.isColourByViewer();
 
-    JalviewStructureDisplayI sview = null;
     switch (type)
     {
     case JMOL:
@@ -242,9 +346,46 @@ public class StructureViewer
               "Unsupported structure viewer type " + type.toString());
       break;
     default:
-      Cache.log.error("Unknown structure viewer type " + type.toString());
+      Cache.log.error(UNKNOWN_VIEWER_TYPE + type.toString());
     }
     return sview;
   }
 
+  public boolean isBusy()
+  {
+    if (sview != null)
+    {
+      if (!sview.hasMapping())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * 
+   * @param pDBid
+   * @return true if view is already showing PDBid
+   */
+  public boolean hasPdbId(String pDBid)
+  {
+    if (sview == null)
+    {
+      return false;
+    }
+
+    return sview.getBinding().hasPdbId(pDBid);
+  }
+
+  public boolean isVisible()
+  {
+    return sview != null && sview.isVisible();
+  }
+
+  public void setSuperpose(boolean alignAddedStructures)
+  {
+    superposeAdded = alignAddedStructures;
+  }
+
 }
index 3ba9947..72b0bcc 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.gui;
 
+import jalview.api.AlignmentViewPanel;
 import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
@@ -34,6 +35,7 @@ import jalview.io.JalviewFileView;
 import jalview.jbgui.GStructureViewer;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemes;
+import jalview.structure.StructureMapping;
 import jalview.structures.models.AAStructureBindingModel;
 import jalview.util.MessageManager;
 
@@ -83,18 +85,18 @@ public abstract class StructureViewerBase extends GStructureViewer
   /**
    * list of sequenceSet ids associated with the view
    */
-  protected List<String> _aps = new ArrayList<String>();
+  protected List<String> _aps = new ArrayList<>();
 
   /**
    * list of alignment panels to use for superposition
    */
-  protected Vector<AlignmentPanel> _alignwith = new Vector<AlignmentPanel>();
+  protected Vector<AlignmentPanel> _alignwith = new Vector<>();
 
   /**
    * list of alignment panels that are used for colouring structures by aligned
    * sequences
    */
-  protected Vector<AlignmentPanel> _colourwith = new Vector<AlignmentPanel>();
+  protected Vector<AlignmentPanel> _colourwith = new Vector<>();
 
   private String viewId = null;
 
@@ -102,9 +104,9 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   protected boolean alignAddedStructures = false;
 
-  protected boolean _started = false;
+  protected volatile boolean _started = false;
 
-  protected boolean addingStructures = false;
+  protected volatile boolean addingStructures = false;
 
   protected Thread worker = null;
 
@@ -113,6 +115,13 @@ public abstract class StructureViewerBase extends GStructureViewer
   protected JMenu viewSelectionMenu;
 
   /**
+   * set after sequence colouring has been applied for this structure viewer.
+   * used to determine if the final sequence/structure mapping has been
+   * determined
+   */
+  protected volatile boolean seqColoursApplied = false;
+
+  /**
    * Default constructor
    */
   public StructureViewerBase()
@@ -121,6 +130,26 @@ public abstract class StructureViewerBase extends GStructureViewer
   }
 
   /**
+   * @return true if added structures should be aligned to existing one(s)
+   */
+  @Override
+  public boolean isAlignAddedStructures()
+  {
+    return alignAddedStructures;
+  }
+
+  /**
+   * 
+   * @param true
+   *          if added structures should be aligned to existing one(s)
+   */
+  @Override
+  public void setAlignAddedStructures(boolean alignAdded)
+  {
+    alignAddedStructures = alignAdded;
+  }
+
+  /**
    * 
    * @param ap2
    * @return true if this Jmol instance is linked with the given alignPanel
@@ -170,7 +199,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   {
     if (_alignwith == null)
     {
-      _alignwith = new Vector<AlignmentPanel>();
+      _alignwith = new Vector<>();
     }
     if (_alignwith.size() == 0 && ap != null)
     {
@@ -310,6 +339,8 @@ public abstract class StructureViewerBase extends GStructureViewer
 
   public abstract ViewerType getViewerType();
 
+  protected abstract IProgressIndicator getIProgressIndicator();
+
   /**
    * add a new structure (with associated sequences and chains) to this viewer,
    * retrieving it if necessary first.
@@ -324,7 +355,7 @@ public abstract class StructureViewerBase extends GStructureViewer
    */
   protected void addStructure(final PDBEntry pdbentry,
           final SequenceI[] seqs, final String[] chains,
-          final boolean align, final IProgressIndicator alignFrame)
+          final IProgressIndicator alignFrame)
   {
     if (pdbentry.getFile() == null)
     {
@@ -348,7 +379,7 @@ public abstract class StructureViewerBase extends GStructureViewer
               }
             }
             // and call ourselves again.
-            addStructure(pdbentry, seqs, chains, align, alignFrame);
+            addStructure(pdbentry, seqs, chains, alignFrame);
           }
         }).start();
         return;
@@ -360,87 +391,42 @@ public abstract class StructureViewerBase extends GStructureViewer
             { seqs }, new String[][] { chains });
     addingStructures = true;
     _started = false;
-    alignAddedStructures = align;
     worker = new Thread(this);
     worker.start();
     return;
   }
 
-  /**
-   * Presents a dialog with the option to add an align a structure to an
-   * existing structure view
-   * 
-   * @param pdbId
-   * @param view
-   * @return YES, NO or CANCEL JvOptionPane code
-   */
-  protected int chooseAlignStructureToViewer(String pdbId,
-          StructureViewerBase view)
-  {
-    int option = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-            MessageManager.formatMessage("label.add_pdbentry_to_view",
-                    new Object[]
-                    { pdbId, view.getTitle() }),
-            MessageManager
-                    .getString("label.align_to_existing_structure_view"),
-            JvOptionPane.YES_NO_CANCEL_OPTION);
-    return option;
-  }
-
   protected boolean hasPdbId(String pdbId)
   {
     return getBinding().hasPdbId(pdbId);
   }
 
-  protected abstract List<StructureViewerBase> getViewersFor(
-          AlignmentPanel alp);
-
   /**
-   * Check for any existing views involving this alignment and give user the
-   * option to add and align this molecule to one of them
-   * 
-   * @param pdbentry
-   * @param seq
-   * @param chains
-   * @param apanel
-   * @param pdbId
-   * @return true if user adds to a view, or cancels entirely, else false
+   * Returns a list of any viewer of the instantiated type. The list is
+   * restricted to those linked to the given alignment panel if it is not null.
    */
-  protected boolean addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
-          String[] chains, final AlignmentPanel apanel, String pdbId)
+  protected List<StructureViewerBase> getViewersFor(AlignmentPanel alp)
   {
-    for (StructureViewerBase view : getViewersFor(apanel))
-    {
-      // TODO: highlight the view somehow
-      /*
-       * JAL-1742 exclude view with this structure already mapped (don't offer
-       * to align chain B to chain A of the same structure)
-       */
-      if (view.hasPdbId(pdbId))
-      {
-        continue;
-      }
-      int option = chooseAlignStructureToViewer(pdbId, view);
-      if (option == JvOptionPane.CANCEL_OPTION)
-      {
-        return true;
-      }
-      else if (option == JvOptionPane.YES_OPTION)
-      {
-        view.useAlignmentPanelForSuperposition(apanel);
-        view.addStructure(pdbentry, seq, chains, true, apanel.alignFrame);
-        return true;
-      }
-      else
-      {
-        // NO_OPTION - offer the next viewer if any
-      }
-    }
+    return Desktop.instance.getStructureViewers(alp, this.getClass());
+  }
 
+  @Override
+  public void addToExistingViewer(PDBEntry pdbentry, SequenceI[] seq,
+          String[] chains, final AlignmentViewPanel apanel, String pdbId)
+  {
     /*
-     * nothing offered and selected
+     * JAL-1742 exclude view with this structure already mapped (don't offer
+     * to align chain B to chain A of the same structure); code may defend
+     * against this possibility before we reach here
      */
-    return false;
+    if (hasPdbId(pdbId))
+    {
+      return;
+    }
+    AlignmentPanel alignPanel = (AlignmentPanel) apanel; // Implementation error if this
+                                                 // cast fails
+    useAlignmentPanelForSuperposition(alignPanel);
+    addStructure(pdbentry, seq, chains, alignPanel.alignFrame);
   }
 
   /**
@@ -452,15 +438,18 @@ public abstract class StructureViewerBase extends GStructureViewer
    * @param apanel
    * @param pdbFilename
    */
-  protected void addSequenceMappingsToStructure(SequenceI[] seq,
-          String[] chains, final AlignmentPanel apanel, String pdbFilename)
+  public void addSequenceMappingsToStructure(SequenceI[] seq,
+          String[] chains, final AlignmentViewPanel alpanel,
+          String pdbFilename)
   {
+    AlignmentPanel apanel = (AlignmentPanel) alpanel;
+
     // TODO : Fix multiple seq to one chain issue here.
     /*
      * create the mappings
      */
     apanel.getStructureSelectionManager().setMapping(seq, chains,
-            pdbFilename, DataSourceType.FILE);
+            pdbFilename, DataSourceType.FILE, getIProgressIndicator());
 
     /*
      * alert the FeatureRenderer to show new (PDB RESNUM) features
@@ -468,7 +457,9 @@ public abstract class StructureViewerBase extends GStructureViewer
     if (apanel.getSeqPanel().seqCanvas.fr != null)
     {
       apanel.getSeqPanel().seqCanvas.fr.featuresAdded();
-      apanel.paintAlignment(true);
+      // note - we don't do a refresh for structure here because we do it
+      // explicitly for all panels later on
+      apanel.paintAlignment(true, false);
     }
 
     /*
@@ -499,47 +490,20 @@ public abstract class StructureViewerBase extends GStructureViewer
     }
   }
 
-  /**
-   * Check if the PDB file is already loaded, if so offer to add it to the
-   * existing viewer
-   * 
-   * @param seq
-   * @param chains
-   * @param apanel
-   * @param pdbId
-   * @return true if the user chooses to add to a viewer, or to cancel entirely
-   */
-  protected boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
-          final AlignmentPanel apanel, String pdbId)
+  @Override
+  public boolean addAlreadyLoadedFile(SequenceI[] seq, String[] chains,
+          final AlignmentViewPanel apanel, String pdbId)
   {
-    boolean finished = false;
     String alreadyMapped = apanel.getStructureSelectionManager()
             .alreadyMappedToFile(pdbId);
 
-    if (alreadyMapped != null)
+    if (alreadyMapped == null)
     {
-      /*
-       * the PDB file is already loaded
-       */
-      int option = JvOptionPane.showInternalConfirmDialog(Desktop.desktop,
-              MessageManager.formatMessage(
-                      "label.pdb_entry_is_already_displayed", new Object[]
-                      { pdbId }),
-              MessageManager.formatMessage(
-                      "label.map_sequences_to_visible_window", new Object[]
-                      { pdbId }),
-              JvOptionPane.YES_NO_CANCEL_OPTION);
-      if (option == JvOptionPane.CANCEL_OPTION)
-      {
-        finished = true;
-      }
-      else if (option == JvOptionPane.YES_OPTION)
-      {
-        addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
-        finished = true;
-      }
+      return false;
     }
-    return finished;
+
+    addSequenceMappingsToStructure(seq, chains, apanel, alreadyMapped);
+    return true;
   }
 
   void setChainMenuItems(List<String> chainNames)
@@ -717,11 +681,11 @@ public abstract class StructureViewerBase extends GStructureViewer
 
     if (_colourwith == null)
     {
-      _colourwith = new Vector<AlignmentPanel>();
+      _colourwith = new Vector<>();
     }
     if (_alignwith == null)
     {
-      _alignwith = new Vector<AlignmentPanel>();
+      _alignwith = new Vector<>();
     }
 
     ViewSelectionMenu seqColourBy = new ViewSelectionMenu(
@@ -819,11 +783,11 @@ public abstract class StructureViewerBase extends GStructureViewer
       int[] alm = new int[_alignwith.size()];
       int a = 0;
 
-      for (AlignmentPanel ap : _alignwith)
+      for (AlignmentPanel alignPanel : _alignwith)
       {
-        als[a] = ap.av.getAlignment();
+        als[a] = alignPanel.av.getAlignment();
         alm[a] = -1;
-        alc[a++] = ap.av.getAlignment().getHiddenColumns();
+        alc[a++] = alignPanel.av.getAlignment().getHiddenColumns();
       }
       reply = getBinding().superposeStructures(als, alm, alc);
       if (reply != null)
@@ -835,9 +799,9 @@ public abstract class StructureViewerBase extends GStructureViewer
     } catch (Exception e)
     {
       StringBuffer sp = new StringBuffer();
-      for (AlignmentPanel ap : _alignwith)
+      for (AlignmentPanel alignPanel : _alignwith)
       {
-        sp.append("'" + ap.alignFrame.getTitle() + "' ");
+        sp.append("'" + alignPanel.alignFrame.getTitle() + "' ");
       }
       Cache.log.info("Couldn't align structures with the " + sp.toString()
               + "associated alignment panels.", e);
@@ -888,7 +852,7 @@ public abstract class StructureViewerBase extends GStructureViewer
     binding.setColourBySequence(seqColour.isSelected());
     if (_colourwith == null)
     {
-      _colourwith = new Vector<AlignmentPanel>();
+      _colourwith = new Vector<>();
     }
     if (binding.isColourBySequence())
     {
@@ -901,10 +865,11 @@ public abstract class StructureViewerBase extends GStructureViewer
         }
       }
       // Set the colour using the current view for the associated alignframe
-      for (AlignmentPanel ap : _colourwith)
+      for (AlignmentPanel alignPanel : _colourwith)
       {
-        binding.colourBySequence(ap);
+        binding.colourBySequence(alignPanel);
       }
+      seqColoursApplied = true;
     }
   }
 
@@ -984,6 +949,7 @@ public abstract class StructureViewerBase extends GStructureViewer
   /**
    * Configures the title and menu items of the viewer panel.
    */
+  @Override
   public void updateTitleAndMenus()
   {
     AAStructureBindingModel binding = getBinding();
@@ -1024,4 +990,54 @@ public abstract class StructureViewerBase extends GStructureViewer
       seqColour_actionPerformed(null);
     }
   }
+
+  @Override
+  public String toString()
+  {
+    return getTitle();
+  }
+
+  @Override
+  public boolean hasMapping()
+  {
+    if (worker != null && (addingStructures || _started))
+    {
+      return false;
+    }
+    if (getBinding() == null)
+    {
+      if (_aps == null || _aps.size() == 0)
+      {
+        // viewer has been closed, but we did at some point run.
+        return true;
+      }
+      return false;
+    }
+    String[] pdbids = getBinding().getStructureFiles();
+    if (pdbids == null)
+    {
+      return false;
+    }
+    int p=0;
+    for (String pdbid:pdbids) {
+      StructureMapping sm[] = getBinding().getSsm().getMapping(pdbid);
+      if (sm!=null && sm.length>0 && sm[0]!=null) {
+        p++;
+      }
+    }
+    // only return true if there is a mapping for every structure file we have loaded
+    if (p == 0 || p != pdbids.length)
+    {
+      return false;
+    }
+    // and that coloring has been applied
+    return seqColoursApplied;
+  }
+
+  @Override
+  public void raiseViewer()
+  {
+    toFront();
+  }
+
 }
index 3986561..53e2dee 100644 (file)
@@ -184,9 +184,9 @@ public class TextColourChooser
    */
   protected void saveInitialSettings()
   {
-    groupColour1 = new HashMap<SequenceGroup, Color>();
-    groupColour2 = new HashMap<SequenceGroup, Color>();
-    groupThreshold = new HashMap<SequenceGroup, Integer>();
+    groupColour1 = new HashMap<>();
+    groupColour2 = new HashMap<>();
+    groupThreshold = new HashMap<>();
 
     if (sg == null)
     {
@@ -237,7 +237,7 @@ public class TextColourChooser
       sg.textColour = col;
     }
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(false, false);
   }
 
   void colour2Changed(Color col)
@@ -255,7 +255,7 @@ public class TextColourChooser
       sg.textColour2 = col;
     }
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(false, false);
   }
 
   void thresholdChanged(int value)
@@ -273,7 +273,7 @@ public class TextColourChooser
       sg.thresholdTextColour = value;
     }
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(false, false);
   }
 
   void setGroupTextColour()
index 5e14fce..2727db1 100755 (executable)
@@ -55,6 +55,7 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.image.BufferedImage;
 import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -63,6 +64,8 @@ import javax.imageio.ImageIO;
 import javax.swing.ButtonGroup;
 import javax.swing.JMenuItem;
 import javax.swing.JRadioButtonMenuItem;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
 
 import org.jibble.epsgraphics.EpsGraphics2D;
 
@@ -141,7 +144,35 @@ public class TreePanel extends GTreePanel
 
     buildAssociatedViewMenu();
 
-    av.addPropertyChangeListener(new java.beans.PropertyChangeListener()
+    final PropertyChangeListener listener = addAlignmentListener();
+
+    /*
+     * remove listener when window is closed, so that this
+     * panel can be garbage collected
+     */
+    addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(InternalFrameEvent evt)
+      {
+        if (av != null)
+        {
+          av.removePropertyChangeListener(listener);
+        }
+      }
+    });
+
+    TreeLoader tl = new TreeLoader(newTree, inputData);
+    tl.start();
+
+  }
+
+  /**
+   * @return
+   */
+  protected PropertyChangeListener addAlignmentListener()
+  {
+    final PropertyChangeListener listener = new PropertyChangeListener()
     {
       @Override
       public void propertyChange(PropertyChangeEvent evt)
@@ -168,11 +199,9 @@ public class TreePanel extends GTreePanel
           repaint();
         }
       }
-    });
-
-    TreeLoader tl = new TreeLoader(newTree, inputData);
-    tl.start();
-
+    };
+    av.addPropertyChangeListener(listener);
+    return listener;
   }
 
   @Override
@@ -493,7 +522,7 @@ public class TreePanel extends GTreePanel
 
     if (treeCanvas.applyToAllViews)
     {
-      final ArrayList<CommandI> commands = new ArrayList<CommandI>();
+      final ArrayList<CommandI> commands = new ArrayList<>();
       for (AlignmentPanel ap : PaintRefresher
               .getAssociatedPanels(av.getSequenceSetId()))
       {
@@ -550,13 +579,14 @@ public class TreePanel extends GTreePanel
 
   public CommandI sortAlignmentIn(AlignmentPanel ap)
   {
+    // TODO: move to alignment view controller
     AlignmentViewport viewport = ap.av;
     SequenceI[] oldOrder = viewport.getAlignment().getSequencesArray();
     AlignmentSorter.sortByTree(viewport.getAlignment(), tree);
     CommandI undo;
     undo = new OrderCommand("Tree Sort", oldOrder, viewport.getAlignment());
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, false);
     return undo;
   }
 
index 8b45c40..3290500 100755 (executable)
@@ -136,7 +136,7 @@ public class UserDefinedColours extends GUserDefinedColours
   UserDefinedColours()
   {
     super();
-    selectedButtons = new ArrayList<JButton>();
+    selectedButtons = new ArrayList<>();
   }
 
   void showFrame()
@@ -163,7 +163,7 @@ public class UserDefinedColours extends GUserDefinedColours
 
     if (upperCaseButtons == null)
     {
-      upperCaseButtons = new ArrayList<JButton>();
+      upperCaseButtons = new ArrayList<>();
     }
 
     for (int i = 0; i < 20; i++)
@@ -194,7 +194,7 @@ public class UserDefinedColours extends GUserDefinedColours
 
       if (lowerCaseButtons == null)
       {
-        lowerCaseButtons = new ArrayList<JButton>();
+        lowerCaseButtons = new ArrayList<>();
       }
 
       for (int i = 0; i < 20; i++)
@@ -631,8 +631,8 @@ public class UserDefinedColours extends GUserDefinedColours
   @Override
   protected void loadbutton_actionPerformed()
   {
-    upperCaseButtons = new ArrayList<JButton>();
-    lowerCaseButtons = new ArrayList<JButton>();
+    upperCaseButtons = new ArrayList<>();
+    lowerCaseButtons = new ArrayList<>();
 
     JalviewFileChooser chooser = new JalviewFileChooser("jc",
             "Jalview User Colours");
@@ -876,7 +876,7 @@ public class UserDefinedColours extends GUserDefinedColours
   protected void cancelButton_actionPerformed()
   {
     ap.alignFrame.changeColour(oldColourScheme);
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
 
     try
     {
index d2086e0..973cfe8 100644 (file)
@@ -1074,16 +1074,16 @@ public class VamsasApplication implements SelectionSource, VamsasSource
                   }
                   else
                   {
-                    // int[] intervals = colsel.getVisibleContigs(
-                    // seqsel.getStartRes(), seqsel.getEndRes() + 1);
-                    int[] intervals = hidden.getVisibleContigs(
-                            seqsel.getStartRes(), seqsel.getEndRes() + 1);
-                    for (int iv = 0; iv < intervals.length; iv += 2)
+                    Iterator<int[]> intervals = hidden
+                            .getVisContigsIterator(seqsel.getStartRes(),
+                                    seqsel.getEndRes() + 1, false);
+                    while (intervals.hasNext())
                     {
+                      int[] region = intervals.next();
                       Seg s = new Seg();
-                      s.setStart(intervals[iv] + 1); // vamsas indices begin at
-                      // 1, not zero.
-                      s.setEnd(intervals[iv + 1] + 1);
+                      s.setStart(region[0] + 1); // vamsas indices begin at 1,
+                                                 // not zero.
+                      s.setEnd(region[1] + 1);
                       s.setInclusive(true);
                       range.addSeg(s);
                     }
index cdbb4fa..2a7743a 100644 (file)
@@ -60,15 +60,6 @@ public class ViewSelectionMenu extends JMenu
 
   private ItemListener _handler;
 
-  @Override
-  protected void finalize() throws Throwable
-  {
-    _selectedviews = null;
-    _handler = null;
-    _allviews = null;
-    super.finalize();
-  }
-
   /**
    * create a new view selection menu. This menu has some standard entries
    * (select all, invert selection), and a checkbox for every view. Mousing over
index a603cca..497f0a5 100755 (executable)
@@ -72,7 +72,20 @@ public abstract class AlignFile extends FileParse
 
   long end;
 
-  private boolean parseCalled;
+  /**
+   * true if parse() has been called
+   */
+  private boolean parseCalled = false;
+
+  private boolean parseImmediately = true;
+
+  /**
+   * @return if doParse() was called at construction time
+   */
+  protected boolean isParseImmediately()
+  {
+    return parseImmediately;
+  }
 
   /**
    * Creates a new AlignFile object.
@@ -153,6 +166,11 @@ public abstract class AlignFile extends FileParse
   {
     super(source);
     initData();
+
+    // stash flag in case parse needs to know if it has to autoconfigure or was
+    // configured after construction
+    this.parseImmediately = parseImmediately;
+
     if (parseImmediately)
     {
       doParse();
@@ -174,11 +192,6 @@ public abstract class AlignFile extends FileParse
     }
     parseCalled = true;
     parse();
-    // sets the index of each sequence in the alignment
-    for (int i = 0, c = seqs.size(); i < c; i++)
-    {
-      seqs.get(i).setIndex(i);
-    }
   }
 
   /**
index 00476d6..e578a45 100755 (executable)
@@ -968,7 +968,7 @@ public class AnnotationFile
             else
             {
               // consider deferring this till after the file has been parsed ?
-              hidden.hideInsertionsFor(sr);
+              hidden.hideList(sr.getInsertions());
             }
           }
           modified = true;
index c21b02c..afb2009 100755 (executable)
@@ -26,7 +26,8 @@ import jalview.datamodel.SequenceI;
 import jalview.util.Format;
 
 import java.io.IOException;
-import java.util.Hashtable;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.Vector;
 
@@ -59,12 +60,11 @@ public class ClustalFile extends AlignFile
   {
     int i = 0;
     boolean flag = false;
-    boolean rna = false;
     boolean top = false;
-    StringBuffer pssecstr = new StringBuffer(),
-            consstr = new StringBuffer();
-    Vector headers = new Vector();
-    Hashtable seqhash = new Hashtable();
+    StringBuffer pssecstr = new StringBuffer();
+    StringBuffer consstr = new StringBuffer();
+    Vector<String> headers = new Vector<>();
+    Map<String, StringBuffer> seqhash = new HashMap<>();
     StringBuffer tempseq;
     String line, id;
     StringTokenizer str;
@@ -77,9 +77,11 @@ public class ClustalFile extends AlignFile
         {
           top = true;
         }
-        if (line.indexOf(" ") != 0)
+        boolean isConservation = line.startsWith(SPACE)
+                || line.startsWith(TAB);
+        if (!isConservation)
         {
-          str = new StringTokenizer(line, " ");
+          str = new StringTokenizer(line);
 
           if (str.hasMoreTokens())
           {
@@ -95,7 +97,7 @@ public class ClustalFile extends AlignFile
               {
                 if (seqhash.containsKey(id))
                 {
-                  tempseq = (StringBuffer) seqhash.get(id);
+                  tempseq = seqhash.get(id);
                 }
                 else
                 {
@@ -173,7 +175,7 @@ public class ClustalFile extends AlignFile
       AlignmentAnnotation lastssa = null;
       if (pssecstr.length() == maxLength)
       {
-        Vector ss = new Vector();
+        Vector<AlignmentAnnotation> ss = new Vector<>();
         AlignmentAnnotation ssa = lastssa = StockholmFile
                 .parseAnnotationRow(ss, "secondary structure",
                         pssecstr.toString());
@@ -182,7 +184,7 @@ public class ClustalFile extends AlignFile
       }
       if (consstr.length() == maxLength)
       {
-        Vector ss = new Vector();
+        Vector<AlignmentAnnotation> ss = new Vector<>();
         AlignmentAnnotation ssa = StockholmFile.parseAnnotationRow(ss,
                 "secondary structure", consstr.toString());
         ssa.label = "Consensus Secondary Structure";
@@ -238,19 +240,19 @@ public class ClustalFile extends AlignFile
         out.append(new Format("%-" + maxid + "s")
                 .form(printId(s[j], jvsuffix) + " "));
 
-        int start = i * len;
-        int end = start + len;
+        int chunkStart = i * len;
+        int chunkEnd = chunkStart + len;
 
         int length = s[j].getLength();
-        if ((end < length) && (start < length))
+        if ((chunkEnd < length) && (chunkStart < length))
         {
-          out.append(s[j].getSequenceAsString(start, end));
+          out.append(s[j].getSequenceAsString(chunkStart, chunkEnd));
         }
         else
         {
-          if (start < length)
+          if (chunkStart < length)
           {
-            out.append(s[j].getSequenceAsString().substring(start));
+            out.append(s[j].getSequenceAsString().substring(chunkStart));
           }
         }
 
index d2282b1..169da5a 100755 (executable)
@@ -31,6 +31,8 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.io.gff.GffHelperBase;
 import jalview.io.gff.GffHelperFactory;
 import jalview.io.gff.GffHelperI;
@@ -68,12 +70,20 @@ import java.util.Map.Entry;
  */
 public class FeaturesFile extends AlignFile implements FeaturesSourceI
 {
+  private static final String TAB_REGEX = "\\t";
+
+  private static final String STARTGROUP = "STARTGROUP";
+
+  private static final String ENDGROUP = "ENDGROUP";
+
+  private static final String STARTFILTERS = "STARTFILTERS";
+
+  private static final String ENDFILTERS = "ENDFILTERS";
+
   private static final String ID_NOT_SPECIFIED = "ID_NOT_SPECIFIED";
 
   private static final String NOTE = "Note";
 
-  protected static final String TAB = "\t";
-
   protected static final String GFF_VERSION = "##gff-version";
 
   private AlignmentI lastmatchedAl = null;
@@ -169,7 +179,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    * @param align
    *          - alignment/dataset containing sequences that are to be annotated
    * @param colours
-   *          - hashtable to store feature colour definitions
+   *          - map to store feature colour definitions
    * @param removeHTML
    *          - process html strings into plain text
    * @param relaxedIdmatching
@@ -180,11 +190,34 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           Map<String, FeatureColourI> colours, boolean removeHTML,
           boolean relaxedIdmatching)
   {
-    Map<String, String> gffProps = new HashMap<String, String>();
+    return parse(align, colours, null, removeHTML, relaxedIdmatching);
+  }
+
+  /**
+   * Parse GFF or Jalview format sequence features file
+   * 
+   * @param align
+   *          - alignment/dataset containing sequences that are to be annotated
+   * @param colours
+   *          - map to store feature colour definitions
+   * @param filters
+   *          - map to store feature filter definitions
+   * @param removeHTML
+   *          - process html strings into plain text
+   * @param relaxedIdmatching
+   *          - when true, ID matches to compound sequence IDs are allowed
+   * @return true if features were added
+   */
+  public boolean parse(AlignmentI align,
+          Map<String, FeatureColourI> colours,
+          Map<String, FeatureMatcherSetI> filters, boolean removeHTML,
+          boolean relaxedIdmatching)
+  {
+    Map<String, String> gffProps = new HashMap<>();
     /*
      * keep track of any sequences we try to create from the data
      */
-    List<SequenceI> newseqs = new ArrayList<SequenceI>();
+    List<SequenceI> newseqs = new ArrayList<>();
 
     String line = null;
     try
@@ -204,7 +237,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           continue;
         }
 
-        gffColumns = line.split("\\t"); // tab as regex
+        gffColumns = line.split(TAB_REGEX);
         if (gffColumns.length == 1)
         {
           if (line.trim().equalsIgnoreCase("GFF"))
@@ -218,18 +251,23 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
           }
         }
 
-        if (gffColumns.length > 1 && gffColumns.length < 4)
+        if (gffColumns.length > 0 && gffColumns.length < 4)
         {
           /*
            * if 2 or 3 tokens, we anticipate either 'startgroup', 'endgroup' or
            * a feature type colour specification
            */
           String ft = gffColumns[0];
-          if (ft.equalsIgnoreCase("startgroup"))
+          if (ft.equalsIgnoreCase(STARTFILTERS))
+          {
+            parseFilters(filters);
+            continue;
+          }
+          if (ft.equalsIgnoreCase(STARTGROUP))
           {
             featureGroup = gffColumns[1];
           }
-          else if (ft.equalsIgnoreCase("endgroup"))
+          else if (ft.equalsIgnoreCase(ENDGROUP))
           {
             // We should check whether this is the current group,
             // but at present there's no way of showing more than 1 group
@@ -290,6 +328,43 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
+   * Reads input lines from STARTFILTERS to ENDFILTERS and adds a feature type
+   * filter to the map for each line parsed. After exit from this method,
+   * nextLine() should return the line after ENDFILTERS (or we are already at
+   * end of file if ENDFILTERS was missing).
+   * 
+   * @param filters
+   * @throws IOException
+   */
+  protected void parseFilters(Map<String, FeatureMatcherSetI> filters)
+          throws IOException
+  {
+    String line;
+    while ((line = nextLine()) != null)
+    {
+      if (line.toUpperCase().startsWith(ENDFILTERS))
+      {
+        return;
+      }
+      String[] tokens = line.split(TAB_REGEX);
+      if (tokens.length != 2)
+      {
+        System.err.println(String.format("Invalid token count %d for %d",
+                tokens.length, line));
+      }
+      else
+      {
+        String featureType = tokens[0];
+        FeatureMatcherSetI fm = FeatureMatcherSet.fromString(tokens[1]);
+        if (fm != null && filters != null)
+        {
+          filters.put(featureType, fm);
+        }
+      }
+    }
+  }
+
+  /**
    * Try to parse a Jalview format feature specification and add it as a
    * sequence feature to any matching sequences in the alignment. Returns true
    * if successful (a feature was added), or false if not.
@@ -487,15 +562,16 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
   }
 
   /**
-   * Returns contents of a Jalview format features file, for visible features,
-   * as filtered by type and group. Features with a null group are displayed if
-   * their feature type is visible. Non-positional features may optionally be
-   * included (with no check on type or group).
+   * Returns contents of a Jalview format features file, for visible features, as
+   * filtered by type and group. Features with a null group are displayed if their
+   * feature type is visible. Non-positional features may optionally be included
+   * (with no check on type or group).
    * 
    * @param sequences
    *          source of features
    * @param visible
    *          map of colour for each visible feature type
+   * @param featureFilters
    * @param visibleFeatureGroups
    * @param includeNonPositional
    *          if true, include non-positional features (regardless of group or
@@ -504,6 +580,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
    */
   public String printJalviewFormat(SequenceI[] sequences,
           Map<String, FeatureColourI> visible,
+          Map<String, FeatureMatcherSetI> featureFilters,
           List<String> visibleFeatureGroups, boolean includeNonPositional)
   {
     if (!includeNonPositional && (visible == null || visible.isEmpty()))
@@ -531,10 +608,15 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
             .toArray(new String[visible.keySet().size()]);
 
     /*
+     * feature filters if any
+     */
+    outputFeatureFilters(out, visible, featureFilters);
+
+    /*
      * sort groups alphabetically, and ensure that features with a
      * null or empty group are output after those in named groups
      */
-    List<String> sortedGroups = new ArrayList<String>(visibleFeatureGroups);
+    List<String> sortedGroups = new ArrayList<>(visibleFeatureGroups);
     sortedGroups.remove(null);
     sortedGroups.remove("");
     Collections.sort(sortedGroups);
@@ -560,13 +642,76 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       }
     }
 
-    for (String group : sortedGroups)
+    /*
+     * positional features within groups
+     */
+    foundSome |= outputFeaturesByGroup(out, sortedGroups, types, sequences);
+
+    return foundSome ? out.toString() : "No Features Visible";
+  }
+
+  /**
+   * Outputs any feature filters defined for visible feature types, sandwiched by
+   * STARTFILTERS and ENDFILTERS lines
+   * 
+   * @param out
+   * @param visible
+   * @param featureFilters
+   */
+  void outputFeatureFilters(StringBuilder out,
+          Map<String, FeatureColourI> visible,
+          Map<String, FeatureMatcherSetI> featureFilters)
+  {
+    if (visible == null || featureFilters == null
+            || featureFilters.isEmpty())
+    {
+      return;
+    }
+
+    boolean first = true;
+    for (String featureType : visible.keySet())
+    {
+      FeatureMatcherSetI filter = featureFilters.get(featureType);
+      if (filter != null)
+      {
+        if (first)
+        {
+          first = false;
+          out.append(newline).append(STARTFILTERS).append(newline);
+        }
+        out.append(featureType).append(TAB).append(filter.toStableString())
+                .append(newline);
+      }
+    }
+    if (!first)
+    {
+      out.append(ENDFILTERS).append(newline).append(newline);
+    }
+
+  }
+
+  /**
+   * Appends output of sequence features within feature groups to the output
+   * buffer. Groups other than the null or empty group are sandwiched by
+   * STARTGROUP and ENDGROUP lines.
+   * 
+   * @param out
+   * @param groups
+   * @param featureTypes
+   * @param sequences
+   * @return
+   */
+  private boolean outputFeaturesByGroup(StringBuilder out,
+          List<String> groups, String[] featureTypes, SequenceI[] sequences)
+  {
+    boolean foundSome = false;
+    for (String group : groups)
     {
       boolean isNamedGroup = (group != null && !"".equals(group));
       if (isNamedGroup)
       {
         out.append(newline);
-        out.append("STARTGROUP").append(TAB);
+        out.append(STARTGROUP).append(TAB);
         out.append(group);
         out.append(newline);
       }
@@ -577,11 +722,11 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       for (int i = 0; i < sequences.length; i++)
       {
         String sequenceName = sequences[i].getName();
-        List<SequenceFeature> features = new ArrayList<SequenceFeature>();
-        if (types.length > 0)
+        List<SequenceFeature> features = new ArrayList<>();
+        if (featureTypes.length > 0)
         {
           features.addAll(sequences[i].getFeatures().getFeaturesForGroup(
-                  true, group, types));
+                  true, group, featureTypes));
         }
 
         for (SequenceFeature sequenceFeature : features)
@@ -593,13 +738,12 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
       if (isNamedGroup)
       {
-        out.append("ENDGROUP").append(TAB);
+        out.append(ENDGROUP).append(TAB);
         out.append(group);
         out.append(newline);
       }
     }
-
-    return foundSome ? out.toString() : "No Features Visible";
+    return foundSome;
   }
 
   /**
@@ -688,7 +832,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
       dataset = new Alignment(new SequenceI[] {});
     }
 
-    Map<String, FeatureColourI> featureColours = new HashMap<String, FeatureColourI>();
+    Map<String, FeatureColourI> featureColours = new HashMap<>();
     boolean parseResult = parse(dataset, featureColours, false, true);
     if (!parseResult)
     {
@@ -748,7 +892,7 @@ public class FeaturesFile extends AlignFile implements FeaturesSourceI
 
     for (SequenceI seq : sequences)
     {
-      List<SequenceFeature> features = new ArrayList<SequenceFeature>();
+      List<SequenceFeature> features = new ArrayList<>();
       if (includeNonPositionalFeatures)
       {
         features.addAll(seq.getFeatures().getNonPositionalFeatures());
index 26641b1..f26d6da 100755 (executable)
@@ -606,18 +606,4 @@ public class FileLoader implements Runnable
     return tempStructFile.toString();
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see java.lang.Object#finalize()
-   */
-  @Override
-  protected void finalize() throws Throwable
-  {
-    source = null;
-    alignFrame = null;
-    viewport = null;
-    super.finalize();
-  }
-
 }
index c0328d5..7117d0f 100755 (executable)
@@ -45,6 +45,10 @@ import java.util.zip.GZIPInputStream;
  */
 public class FileParse
 {
+  protected static final String SPACE = " ";
+
+  protected static final String TAB = "\t";
+
   /**
    * text specifying source of data. usually filename or url.
    */
index 3027ab1..6d3c18a 100755 (executable)
@@ -161,7 +161,8 @@ public class FormatAdapter extends AppletFormatAdapter
 
   public boolean getCacheSuffixDefault(FileFormatI format)
   {
-    return Cache.getDefault(format.getName() + "_JVSUFFIX", true);
+    return Cache.getDefault(format.getName().toUpperCase() + "_JVSUFFIX",
+            true);
   }
 
   public String formatSequences(FileFormatI format, AlignmentI alignment,
@@ -211,12 +212,12 @@ public class FormatAdapter extends AppletFormatAdapter
           AlignmentAnnotation na = new AlignmentAnnotation(ala[i]);
           if (selgp != null)
           {
-            hidden.makeVisibleAnnotation(selgp.getStartRes(),
-                    selgp.getEndRes(), na);
+            na.makeVisibleAnnotation(selgp.getStartRes(), selgp.getEndRes(),
+                    hidden);
           }
           else
           {
-            hidden.makeVisibleAnnotation(na);
+            na.makeVisibleAnnotation(hidden);
           }
           alv.addAnnotation(na);
         }
index d269e97..65ba74a 100644 (file)
@@ -47,11 +47,4 @@ public class InputStreamParser extends FileParse
     error = false;
   }
 
-  @Override
-  protected void finalize() throws Throwable
-  {
-    dataIn = null;
-    super.finalize();
-  }
-
 }
index f1ebcac..6b82671 100644 (file)
  */
 package jalview.io;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.io.gff.GffConstants;
 import jalview.util.MessageManager;
+import jalview.util.StringUtils;
 import jalview.util.UrlLink;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -58,7 +60,7 @@ public class SequenceAnnotationReport
 
   /*
    * Comparator to order DBRefEntry by Source + accession id (case-insensitive),
-   * with 'Primary' sources placed before others
+   * with 'Primary' sources placed before others, and 'chromosome' first of all
    */
   private static Comparator<DBRefEntry> comparator = new Comparator<DBRefEntry>()
   {
@@ -66,6 +68,14 @@ public class SequenceAnnotationReport
     @Override
     public int compare(DBRefEntry ref1, DBRefEntry ref2)
     {
+      if (ref1.isChromosome())
+      {
+        return -1;
+      }
+      if (ref2.isChromosome())
+      {
+        return 1;
+      }
       String s1 = ref1.getSource();
       String s2 = ref2.getSource();
       boolean s1Primary = isPrimarySource(s1);
@@ -78,14 +88,14 @@ public class SequenceAnnotationReport
       {
         return 1;
       }
-      int comp = s1 == null ? -1
-              : (s2 == null ? 1 : s1.compareToIgnoreCase(s2));
+      int comp = s1 == null ? -1 : (s2 == null ? 1 : s1
+              .compareToIgnoreCase(s2));
       if (comp == 0)
       {
         String a1 = ref1.getAccessionId();
         String a2 = ref2.getAccessionId();
-        comp = a1 == null ? -1
-                : (a2 == null ? 1 : a1.compareToIgnoreCase(a2));
+        comp = a1 == null ? -1 : (a2 == null ? 1 : a1
+                .compareToIgnoreCase(a2));
       }
       return comp;
     }
@@ -106,9 +116,9 @@ public class SequenceAnnotationReport
     }
   };
 
-  public SequenceAnnotationReport(String linkImageURL)
+  public SequenceAnnotationReport(String linkURL)
   {
-    this.linkImageURL = linkImageURL;
+    this.linkImageURL = linkURL;
   }
 
   /**
@@ -120,13 +130,13 @@ public class SequenceAnnotationReport
    * @param minmax
    */
   public void appendFeatures(final StringBuilder sb, int rpos,
-          List<SequenceFeature> features, Map<String, float[][]> minmax)
+          List<SequenceFeature> features, FeatureRendererModel fr)
   {
     if (features != null)
     {
       for (SequenceFeature feature : features)
       {
-        appendFeature(sb, rpos, minmax, feature);
+        appendFeature(sb, rpos, fr, feature);
       }
     }
   }
@@ -140,7 +150,7 @@ public class SequenceAnnotationReport
    * @param feature
    */
   void appendFeature(final StringBuilder sb, int rpos,
-          Map<String, float[][]> minmax, SequenceFeature feature)
+          FeatureRendererModel fr, SequenceFeature feature)
   {
     if (feature.isContactFeature())
     {
@@ -153,99 +163,92 @@ public class SequenceAnnotationReport
         sb.append(feature.getType()).append(" ").append(feature.getBegin())
                 .append(":").append(feature.getEnd());
       }
+      return;
     }
-    else
+
+    if (sb.length() > 6)
     {
-      if (sb.length() > 6)
+      sb.append("<br>");
+    }
+    // TODO: remove this hack to display link only features
+    boolean linkOnly = feature.getValue("linkonly") != null;
+    if (!linkOnly)
+    {
+      sb.append(feature.getType()).append(" ");
+      if (rpos != 0)
       {
-        sb.append("<br>");
+        // we are marking a positional feature
+        sb.append(feature.begin);
       }
-      // TODO: remove this hack to display link only features
-      boolean linkOnly = feature.getValue("linkonly") != null;
-      if (!linkOnly)
+      if (feature.begin != feature.end)
       {
-        sb.append(feature.getType()).append(" ");
-        if (rpos != 0)
-        {
-          // we are marking a positional feature
-          sb.append(feature.begin);
-        }
-        if (feature.begin != feature.end)
-        {
-          sb.append(" ").append(feature.end);
-        }
+        sb.append(" ").append(feature.end);
+      }
 
-        if (feature.getDescription() != null
-                && !feature.description.equals(feature.getType()))
-        {
-          String tmpString = feature.getDescription();
-          String tmp2up = tmpString.toUpperCase();
-          int startTag = tmp2up.indexOf("<HTML>");
-          if (startTag > -1)
-          {
-            tmpString = tmpString.substring(startTag + 6);
-            tmp2up = tmp2up.substring(startTag + 6);
-          }
-          int endTag = tmp2up.indexOf("</BODY>");
-          if (endTag > -1)
-          {
-            tmpString = tmpString.substring(0, endTag);
-            tmp2up = tmp2up.substring(0, endTag);
-          }
-          endTag = tmp2up.indexOf("</HTML>");
-          if (endTag > -1)
-          {
-            tmpString = tmpString.substring(0, endTag);
-          }
+      String description = feature.getDescription();
+      if (description != null && !description.equals(feature.getType()))
+      {
+        description = StringUtils.stripHtmlTags(description);
+        sb.append("; ").append(description);
+      }
 
-          if (startTag > -1)
-          {
-            sb.append("; ").append(tmpString);
-          }
-          else
-          {
-            if (tmpString.indexOf("<") > -1 || tmpString.indexOf(">") > -1)
-            {
-              // The description does not specify html is to
-              // be used, so we must remove < > symbols
-              tmpString = tmpString.replaceAll("<", "&lt;");
-              tmpString = tmpString.replaceAll(">", "&gt;");
+      if (showScore(feature, fr))
+      {
+        sb.append(" Score=").append(String.valueOf(feature.getScore()));
+      }
+      String status = (String) feature.getValue("status");
+      if (status != null && status.length() > 0)
+      {
+        sb.append("; (").append(status).append(")");
+      }
 
-              sb.append("; ");
-              sb.append(tmpString);
-            }
-            else
-            {
-              sb.append("; ").append(tmpString);
-            }
-          }
-        }
-        // check score should be shown
-        if (!Float.isNaN(feature.getScore()))
+      /*
+       * add attribute value if coloured by attribute
+       */
+      if (fr != null)
+      {
+        FeatureColourI fc = fr.getFeatureColours().get(feature.getType());
+        if (fc != null && fc.isColourByAttribute())
         {
-          float[][] rng = (minmax == null) ? null
-                  : minmax.get(feature.getType());
-          if (rng != null && rng[0] != null && rng[0][0] != rng[0][1])
+          String[] attName = fc.getAttributeName();
+          String attVal = feature.getValueAsString(attName);
+          if (attVal != null)
           {
-            sb.append(" Score=").append(String.valueOf(feature.getScore()));
+            sb.append("; ").append(String.join(":", attName)).append("=")
+                    .append(attVal);
           }
         }
-        String status = (String) feature.getValue("status");
-        if (status != null && status.length() > 0)
-        {
-          sb.append("; (").append(status).append(")");
-        }
-        String clinSig = (String) feature
-                .getValue(GffConstants.CLINICAL_SIGNIFICANCE);
-        if (clinSig != null)
-        {
-          sb.append("; ").append(clinSig);
-        }
       }
     }
   }
 
   /**
+   * Answers true if score should be shown, else false. Score is shown if it is
+   * not NaN, and the feature type has a non-trivial min-max score range
+   */
+  boolean showScore(SequenceFeature feature, FeatureRendererModel fr)
+  {
+    if (Float.isNaN(feature.getScore()))
+    {
+      return false;
+    }
+    if (fr == null)
+    {
+      return true;
+    }
+    float[][] minMax = fr.getMinMax().get(feature.getType());
+
+    /*
+     * minMax[0] is the [min, max] score range for positional features
+     */
+    if (minMax == null || minMax[0] == null || minMax[0][0] == minMax[0][1])
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
    * Format and appends any hyperlinks for the sequence feature to the string
    * buffer
    * 
@@ -268,19 +271,20 @@ public class SequenceAnnotationReport
           {
             for (List<String> urllink : createLinksFrom(null, urlstring))
             {
-              sb.append("<br/> <a href=\"" + urllink.get(3) + "\" target=\""
-                      + urllink.get(0) + "\">"
+              sb.append("<br/> <a href=\""
+                      + urllink.get(3)
+                      + "\" target=\""
+                      + urllink.get(0)
+                      + "\">"
                       + (urllink.get(0).toLowerCase()
-                              .equals(urllink.get(1).toLowerCase())
-                                      ? urllink.get(0)
-                                      : (urllink.get(0) + ":"
-                                              + urllink.get(1)))
-                      + "</a></br>");
+                              .equals(urllink.get(1).toLowerCase()) ? urllink
+                              .get(0) : (urllink.get(0) + ":" + urllink
+                              .get(1))) + "</a></br>");
             }
           } catch (Exception x)
           {
-            System.err.println(
-                    "problem when creating links from " + urlstring);
+            System.err.println("problem when creating links from "
+                    + urlstring);
             x.printStackTrace();
           }
         }
@@ -298,7 +302,7 @@ public class SequenceAnnotationReport
    */
   Collection<List<String>> createLinksFrom(SequenceI seq, String link)
   {
-    Map<String, List<String>> urlSets = new LinkedHashMap<String, List<String>>();
+    Map<String, List<String>> urlSets = new LinkedHashMap<>();
     UrlLink urlLink = new UrlLink(link);
     if (!urlLink.isValid())
     {
@@ -313,10 +317,10 @@ public class SequenceAnnotationReport
 
   public void createSequenceAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
     createSequenceAnnotationReport(tip, sequence, showDbRefs, showNpFeats,
-            minmax, false);
+            fr, false);
   }
 
   /**
@@ -331,13 +335,13 @@ public class SequenceAnnotationReport
    *          whether to include database references for the sequence
    * @param showNpFeats
    *          whether to include non-positional sequence features
-   * @param minmax
+   * @param fr
    * @param summary
    * @return
    */
   int createSequenceAnnotationReport(final StringBuilder sb,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax, boolean summary)
+          FeatureRendererModel fr, boolean summary)
   {
     String tmp;
     sb.append("<i>");
@@ -354,7 +358,7 @@ public class SequenceAnnotationReport
     {
       ds = ds.getDatasetSequence();
     }
-    
+
     if (showDbRefs)
     {
       maxWidth = Math.max(maxWidth, appendDbRefs(sb, ds, summary));
@@ -369,7 +373,7 @@ public class SequenceAnnotationReport
               .getNonPositionalFeatures())
       {
         int sz = -sb.length();
-        appendFeature(sb, 0, minmax, sf);
+        appendFeature(sb, 0, fr, sf);
         sz += sb.length();
         maxWidth = Math.max(maxWidth, sz);
       }
@@ -458,8 +462,7 @@ public class SequenceAnnotationReport
     }
     if (moreSources)
     {
-      sb.append("<br>").append(source)
-              .append(COMMA).append(ELLIPSIS);
+      sb.append("<br>").append(source).append(COMMA).append(ELLIPSIS);
     }
     if (ellipsis)
     {
@@ -473,10 +476,10 @@ public class SequenceAnnotationReport
 
   public void createTooltipAnnotationReport(final StringBuilder tip,
           SequenceI sequence, boolean showDbRefs, boolean showNpFeats,
-          Map<String, float[][]> minmax)
+          FeatureRendererModel fr)
   {
-    int maxWidth = createSequenceAnnotationReport(tip, sequence, showDbRefs,
-            showNpFeats, minmax, true);
+    int maxWidth = createSequenceAnnotationReport(tip, sequence,
+            showDbRefs, showNpFeats, fr, true);
 
     if (maxWidth > 60)
     {
index 6e7df71..d526a31 100755 (executable)
@@ -184,7 +184,7 @@ public class WSWUBlastClient
         }
       }
     }
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, false);
 
   }
 
index 3d0daed..a837512 100644 (file)
@@ -28,6 +28,7 @@ import java.awt.Dimension;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -70,11 +71,49 @@ public class JvCacheableInputBox<E> extends JComboBox<String>
 
   private JMenuItem menuItemClearCache = new JMenuItem();
 
+  volatile boolean enterWasPressed = false;
+
+  /**
+   * @return flag indicating if the most recent keypress was enter
+   */
+  public boolean wasEnterPressed()
+  {
+    return enterWasPressed;
+  }
+
   public JvCacheableInputBox(String newCacheKey)
   {
     super();
     this.cacheKey = newCacheKey;
     setEditable(true);
+    addKeyListener(new KeyListener()
+    {
+
+      @Override
+      public void keyTyped(KeyEvent e)
+      {
+        enterWasPressed = false;
+        if (e.getKeyCode() == KeyEvent.VK_ENTER)
+        {
+          enterWasPressed = true;
+        }
+        // let event bubble up
+      }
+
+      @Override
+      public void keyReleased(KeyEvent e)
+      {
+        // TODO Auto-generated method stub
+
+      }
+
+      @Override
+      public void keyPressed(KeyEvent e)
+      {
+        // TODO Auto-generated method stub
+
+      }
+    });
     setPrototypeDisplayValue(
             "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
     appCache = AppCache.getInstance();
index c7e1d7a..a25a014 100644 (file)
@@ -39,6 +39,8 @@ import java.util.Map;
  */
 public class Gff3Helper extends GffHelperBase
 {
+  public static final String ALLELES = "alleles";
+
   protected static final String TARGET = "Target";
 
   protected static final String ID = "ID";
@@ -399,7 +401,7 @@ public class Gff3Helper extends GffHelperBase
       /*
        * Ensembl returns dna variants as 'alleles'
        */
-      desc = StringUtils.listToDelimitedString(attributes.get("alleles"),
+      desc = StringUtils.listToDelimitedString(attributes.get(ALLELES),
               ",");
     }
 
index c0570e0..307e1d1 100644 (file)
@@ -42,6 +42,15 @@ public interface SequenceOntologyI
   // SO:0001060
   public static final String SEQUENCE_VARIANT = "sequence_variant";
 
+  // SO:0001819
+  public static final String SYNONYMOUS_VARIANT = "synonymous_variant";
+
+  // SO:0001992
+  public static final String NONSYNONYMOUS_VARIANT = "nonsynonymous_variant";
+
+  // SO:0001587
+  public static final String STOP_GAINED = "stop_gained";
+
   // SO:0000147
   public static final String EXON = "exon";
 
index f989f7b..72e906c 100644 (file)
@@ -44,7 +44,7 @@ public class SequenceOntologyLite implements SequenceOntologyI
    * initial selection of types of interest when processing Ensembl features
    * NB unlike the full SequenceOntology we don't traverse indirect
    * child-parent relationships here so e.g. need to list every sub-type
-   * of gene (direct or indirect) that is of interest
+   * (direct or indirect) that is of interest
    */
   // @formatter:off
   private final String[][] TERMS = new String[][] {
@@ -75,16 +75,26 @@ public class SequenceOntologyLite implements SequenceOntologyI
     // there are many more sub-types of ncRNA...
     
     /*
-     * sequence_variant sub-types:
+     * sequence_variant sub-types
      */
     { "sequence_variant", "sequence_variant" },
+    { "structural_variant", "sequence_variant" },
     { "feature_variant", "sequence_variant" },
     { "gene_variant", "sequence_variant" },
+    { "transcript_variant", "sequence_variant" },
     // NB Ensembl uses NMD_transcript_variant as if a 'transcript'
     // but we model it here correctly as per the SO
     { "NMD_transcript_variant", "sequence_variant" },
-    { "transcript_variant", "sequence_variant" },
-    { "structural_variant", "sequence_variant" },
+    { "missense_variant", "sequence_variant" },
+    { "synonymous_variant", "sequence_variant" },
+    { "frameshift_variant", "sequence_variant" },
+    { "5_prime_UTR_variant", "sequence_variant" },
+    { "3_prime_UTR_variant", "sequence_variant" },
+    { "stop_gained", "sequence_variant" },
+    { "stop_lost", "sequence_variant" },
+    { "inframe_deletion", "sequence_variant" },
+    { "inframe_insertion", "sequence_variant" },
+    { "splice_region_variant", "sequence_variant" },
     
     /*
      * no sub-types of exon or CDS yet seen in Ensembl
@@ -121,8 +131,8 @@ public class SequenceOntologyLite implements SequenceOntologyI
 
   public SequenceOntologyLite()
   {
-    termsFound = new ArrayList<String>();
-    termsNotFound = new ArrayList<String>();
+    termsFound = new ArrayList<>();
+    termsNotFound = new ArrayList<>();
     loadStaticData();
   }
 
@@ -131,13 +141,13 @@ public class SequenceOntologyLite implements SequenceOntologyI
    */
   private void loadStaticData()
   {
-    parents = new HashMap<String, List<String>>();
+    parents = new HashMap<>();
     for (String[] pair : TERMS)
     {
       List<String> p = parents.get(pair[0]);
       if (p == null)
       {
-        p = new ArrayList<String>();
+        p = new ArrayList<>();
         parents.put(pair[0], p);
       }
       p.add(pair[1]);
diff --git a/src/jalview/io/vcf/VCFLoader.java b/src/jalview/io/vcf/VCFLoader.java
new file mode 100644 (file)
index 0000000..de2f18a
--- /dev/null
@@ -0,0 +1,1474 @@
+package jalview.io.vcf;
+
+import jalview.analysis.AlignmentUtils;
+import jalview.analysis.Dna;
+import jalview.api.AlignViewControllerGuiI;
+import jalview.bin.Cache;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureAttributeType;
+import jalview.datamodel.features.FeatureSource;
+import jalview.datamodel.features.FeatureSources;
+import jalview.ext.ensembl.EnsemblMap;
+import jalview.ext.htsjdk.HtsContigDb;
+import jalview.ext.htsjdk.VCFReader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+import jalview.util.MappingUtils;
+import jalview.util.MessageManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import htsjdk.samtools.SAMException;
+import htsjdk.samtools.SAMSequenceDictionary;
+import htsjdk.samtools.SAMSequenceRecord;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+import htsjdk.variant.vcf.VCFHeader;
+import htsjdk.variant.vcf.VCFHeaderLine;
+import htsjdk.variant.vcf.VCFHeaderLineCount;
+import htsjdk.variant.vcf.VCFHeaderLineType;
+import htsjdk.variant.vcf.VCFInfoHeaderLine;
+
+/**
+ * A class to read VCF data (using the htsjdk) and add variants as sequence
+ * features on dna and any related protein product sequences
+ * 
+ * @author gmcarstairs
+ */
+public class VCFLoader
+{
+  /**
+   * A class to model the mapping from sequence to VCF coordinates. Cases include
+   * <ul>
+   * <li>a direct 1:1 mapping where the sequence is one of the VCF contigs</li>
+   * <li>a mapping of sequence to chromosomal coordinates, where sequence and VCF
+   * use the same reference assembly</li>
+   * <li>a modified mapping of sequence to chromosomal coordinates, where sequence
+   * and VCF use different reference assembles</li>
+   * </ul>
+   */
+  class VCFMap
+  {
+    final String chromosome;
+
+    final MapList map;
+
+    VCFMap(String chr, MapList m)
+    {
+      chromosome = chr;
+      map = m;
+    }
+
+    @Override
+    public String toString()
+    {
+      return chromosome + ":" + map.toString();
+    }
+  }
+
+  /*
+   * Lookup keys, and default values, for Preference entries that describe
+   * patterns for VCF and VEP fields to capture 
+   */
+  private static final String VEP_FIELDS_PREF = "VEP_FIELDS";
+
+  private static final String VCF_FIELDS_PREF = "VCF_FIELDS";
+
+  private static final String DEFAULT_VCF_FIELDS = ".*";
+
+  private static final String DEFAULT_VEP_FIELDS = ".*";// "Allele,Consequence,IMPACT,SWISSPROT,SIFT,PolyPhen,CLIN_SIG";
+
+  /*
+   * keys to fields of VEP CSQ consequence data
+   * see https://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private static final String CSQ_CONSEQUENCE_KEY = "Consequence";
+  private static final String CSQ_ALLELE_KEY = "Allele";
+  private static final String CSQ_ALLELE_NUM_KEY = "ALLELE_NUM"; // 0 (ref), 1...
+  private static final String CSQ_FEATURE_KEY = "Feature"; // Ensembl stable id
+
+  /*
+   * default VCF INFO key for VEP consequence data
+   * NB this can be overridden running VEP with --vcf_info_field
+   * - we don't handle this case (require identifier to be CSQ)
+   */
+  private static final String CSQ_FIELD = "CSQ";
+
+  /*
+   * separator for fields in consequence data is '|'
+   */
+  private static final String PIPE_REGEX = "\\|";
+
+  /*
+   * key for Allele Frequency output by VEP
+   * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private static final String ALLELE_FREQUENCY_KEY = "AF";
+
+  /*
+   * delimiter that separates multiple consequence data blocks
+   */
+  private static final String COMMA = ",";
+
+  /*
+   * the feature group assigned to a VCF variant in Jalview
+   */
+  private static final String FEATURE_GROUP_VCF = "VCF";
+
+  /*
+   * internal delimiter used to build keys for assemblyMappings
+   * 
+   */
+  private static final String EXCL = "!";
+
+  /*
+   * the VCF file we are processing
+   */
+  protected String vcfFilePath;
+
+  /*
+   * mappings between VCF and sequence reference assembly regions, as 
+   * key = "species!chromosome!fromAssembly!toAssembly
+   * value = Map{fromRange, toRange}
+   */
+  private Map<String, Map<int[], int[]>> assemblyMappings;
+
+  private VCFReader reader;
+
+  /*
+   * holds details of the VCF header lines (metadata)
+   */
+  private VCFHeader header;
+
+  /*
+   * a Dictionary of contigs (if present) referenced in the VCF file
+   */
+  private SAMSequenceDictionary dictionary;
+
+  /*
+   * the position (0...) of field in each block of
+   * CSQ (consequence) data (if declared in the VCF INFO header for CSQ)
+   * see http://www.ensembl.org/info/docs/tools/vep/vep_formats.html
+   */
+  private int csqConsequenceFieldIndex = -1;
+  private int csqAlleleFieldIndex = -1;
+  private int csqAlleleNumberFieldIndex = -1;
+  private int csqFeatureFieldIndex = -1;
+
+  // todo the same fields for SnpEff ANN data if wanted
+  // see http://snpeff.sourceforge.net/SnpEff_manual.html#input
+
+  /*
+   * a unique identifier under which to save metadata about feature
+   * attributes (selected INFO field data)
+   */
+  private String sourceId;
+
+  /*
+   * The INFO IDs of data that is both present in the VCF file, and
+   * also matched by any filters for data of interest
+   */
+  List<String> vcfFieldsOfInterest;
+
+  /*
+   * The field offsets and identifiers for VEP (CSQ) data that is both present
+   * in the VCF file, and also matched by any filters for data of interest
+   * for example 0 -> Allele, 1 -> Consequence, ..., 36 -> SIFT, ...
+   */
+  Map<Integer, String> vepFieldsOfInterest;
+
+  /**
+   * Constructor given a VCF file
+   * 
+   * @param alignment
+   */
+  public VCFLoader(String vcfFile)
+  {
+    try
+    {
+      initialise(vcfFile);
+    } catch (IOException e)
+    {
+      System.err.println("Error opening VCF file: " + e.getMessage());
+    }
+
+    // map of species!chromosome!fromAssembly!toAssembly to {fromRange, toRange}
+    assemblyMappings = new HashMap<>();
+  }
+
+  /**
+   * Starts a new thread to query and load VCF variant data on to the given
+   * sequences
+   * <p>
+   * This method is not thread safe - concurrent threads should use separate
+   * instances of this class.
+   * 
+   * @param seqs
+   * @param gui
+   */
+  public void loadVCF(SequenceI[] seqs, final AlignViewControllerGuiI gui)
+  {
+    if (gui != null)
+    {
+      gui.setStatus(MessageManager.getString("label.searching_vcf"));
+    }
+
+    new Thread()
+    {
+      @Override
+      public void run()
+      {
+        VCFLoader.this.doLoad(seqs, gui);
+      }
+    }.start();
+  }
+
+  /**
+   * Reads the specified contig sequence and adds its VCF variants to it
+   * 
+   * @param contig
+   *          the id of a single sequence (contig) to load
+   * @return
+   */
+  public SequenceI loadVCFContig(String contig)
+  {
+    String ref = header.getOtherHeaderLine(VCFHeader.REFERENCE_KEY)
+            .getValue();
+    if (ref.startsWith("file://"))
+    {
+      ref = ref.substring(7);
+    }
+
+    SequenceI seq = null;
+    File dbFile = new File(ref);
+
+    if (dbFile.exists())
+    {
+      HtsContigDb db = new HtsContigDb("", dbFile);
+      seq = db.getSequenceProxy(contig);
+      loadSequenceVCF(seq, ref);
+      db.close();
+    }
+    else
+    {
+      System.err.println("VCF reference not found: " + ref);
+    }
+
+    return seq;
+  }
+
+  /**
+   * Loads VCF on to one or more sequences
+   * 
+   * @param seqs
+   * @param gui
+   *          optional callback handler for messages
+   */
+  protected void doLoad(SequenceI[] seqs, AlignViewControllerGuiI gui)
+  {
+    try
+    {
+      VCFHeaderLine ref = header
+              .getOtherHeaderLine(VCFHeader.REFERENCE_KEY);
+      String vcfAssembly = ref.getValue();
+
+      int varCount = 0;
+      int seqCount = 0;
+
+      /*
+       * query for VCF overlapping each sequence in turn
+       */
+      for (SequenceI seq : seqs)
+      {
+        int added = loadSequenceVCF(seq, vcfAssembly);
+        if (added > 0)
+        {
+          seqCount++;
+          varCount += added;
+          transferAddedFeatures(seq);
+        }
+      }
+      if (gui != null)
+      {
+        String msg = MessageManager.formatMessage("label.added_vcf",
+                varCount, seqCount);
+        gui.setStatus(msg);
+        if (gui.getFeatureSettingsUI() != null)
+        {
+          gui.getFeatureSettingsUI().discoverAllFeatureData();
+        }
+      }
+    } catch (Throwable e)
+    {
+      System.err.println("Error processing VCF: " + e.getMessage());
+      e.printStackTrace();
+      if (gui != null)
+      {
+        gui.setStatus("Error occurred - see console for details");
+      }
+    } finally
+    {
+      if (reader != null)
+      {
+        try
+        {
+          reader.close();
+        } catch (IOException e)
+        {
+          // ignore
+        }
+      }
+      header = null;
+      dictionary = null;
+    }
+  }
+
+  /**
+   * Opens the VCF file and parses header data
+   * 
+   * @param filePath
+   * @throws IOException
+   */
+  private void initialise(String filePath) throws IOException
+  {
+    vcfFilePath = filePath;
+
+    reader = new VCFReader(filePath);
+
+    header = reader.getFileHeader();
+
+    try
+    {
+      dictionary = header.getSequenceDictionary();
+    } catch (SAMException e)
+    {
+      // ignore - thrown if any contig line lacks length info
+    }
+
+    sourceId = filePath;
+
+    saveMetadata(sourceId);
+
+    /*
+     * get offset of CSQ ALLELE_NUM and Feature if declared
+     */
+    parseCsqHeader();
+  }
+
+  /**
+   * Reads metadata (such as INFO field descriptions and datatypes) and saves
+   * them for future reference
+   * 
+   * @param theSourceId
+   */
+  void saveMetadata(String theSourceId)
+  {
+    List<Pattern> vcfFieldPatterns = getFieldMatchers(VCF_FIELDS_PREF,
+            DEFAULT_VCF_FIELDS);
+    vcfFieldsOfInterest = new ArrayList<>();
+
+    FeatureSource metadata = new FeatureSource(theSourceId);
+
+    for (VCFInfoHeaderLine info : header.getInfoHeaderLines())
+    {
+      String attributeId = info.getID();
+      String desc = info.getDescription();
+      VCFHeaderLineType type = info.getType();
+      FeatureAttributeType attType = null;
+      switch (type)
+      {
+      case Character:
+        attType = FeatureAttributeType.Character;
+        break;
+      case Flag:
+        attType = FeatureAttributeType.Flag;
+        break;
+      case Float:
+        attType = FeatureAttributeType.Float;
+        break;
+      case Integer:
+        attType = FeatureAttributeType.Integer;
+        break;
+      case String:
+        attType = FeatureAttributeType.String;
+        break;
+      }
+      metadata.setAttributeName(attributeId, desc);
+      metadata.setAttributeType(attributeId, attType);
+
+      if (isFieldWanted(attributeId, vcfFieldPatterns))
+      {
+        vcfFieldsOfInterest.add(attributeId);
+      }
+    }
+
+    FeatureSources.getInstance().addSource(theSourceId, metadata);
+  }
+
+  /**
+   * Answers true if the field id is matched by any of the filter patterns, else
+   * false. Matching is against regular expression patterns, and is not
+   * case-sensitive.
+   * 
+   * @param id
+   * @param filters
+   * @return
+   */
+  private boolean isFieldWanted(String id, List<Pattern> filters)
+  {
+    for (Pattern p : filters)
+    {
+      if (p.matcher(id.toUpperCase()).matches())
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Records 'wanted' fields defined in the CSQ INFO header (if there is one).
+   * Also records the position of selected fields (Allele, ALLELE_NUM, Feature)
+   * required for processing.
+   * <p>
+   * CSQ fields are declared in the CSQ INFO Description e.g.
+   * <p>
+   * Description="Consequence ...from ... VEP. Format: Allele|Consequence|...
+   */
+  protected void parseCsqHeader()
+  {
+    List<Pattern> vepFieldFilters = getFieldMatchers(VEP_FIELDS_PREF,
+            DEFAULT_VEP_FIELDS);
+    vepFieldsOfInterest = new HashMap<>();
+
+    VCFInfoHeaderLine csqInfo = header.getInfoHeaderLine(CSQ_FIELD);
+    if (csqInfo == null)
+    {
+      return;
+    }
+
+    /*
+     * parse out the pipe-separated list of CSQ fields; we assume here that
+     * these form the last part of the description, and contain no spaces
+     */
+    String desc = csqInfo.getDescription();
+    int spacePos = desc.lastIndexOf(" ");
+    desc = desc.substring(spacePos + 1);
+
+    if (desc != null)
+    {
+      String[] format = desc.split(PIPE_REGEX);
+      int index = 0;
+      for (String field : format)
+      {
+        if (CSQ_CONSEQUENCE_KEY.equals(field))
+        {
+          csqConsequenceFieldIndex = index;
+        }
+        if (CSQ_ALLELE_NUM_KEY.equals(field))
+        {
+          csqAlleleNumberFieldIndex = index;
+        }
+        if (CSQ_ALLELE_KEY.equals(field))
+        {
+          csqAlleleFieldIndex = index;
+        }
+        if (CSQ_FEATURE_KEY.equals(field))
+        {
+          csqFeatureFieldIndex = index;
+        }
+
+        if (isFieldWanted(field, vepFieldFilters))
+        {
+          vepFieldsOfInterest.put(index, field);
+        }
+
+        index++;
+      }
+    }
+  }
+
+  /**
+   * Reads the Preference value for the given key, with default specified if no
+   * preference set. The value is interpreted as a comma-separated list of
+   * regular expressions, and converted into a list of compiled patterns ready
+   * for matching. Patterns are forced to upper-case for non-case-sensitive
+   * matching.
+   * <p>
+   * This supports user-defined filters for fields of interest to capture while
+   * processing data. For example, VCF_FIELDS = AF,AC* would mean that VCF INFO
+   * fields with an ID of AF, or starting with AC, would be matched.
+   * 
+   * @param key
+   * @param def
+   * @return
+   */
+  private List<Pattern> getFieldMatchers(String key, String def)
+  {
+    String pref = Cache.getDefault(key, def);
+    List<Pattern> patterns = new ArrayList<>();
+    String[] tokens = pref.split(",");
+    for (String token : tokens)
+    {
+      try
+      {
+      patterns.add(Pattern.compile(token.toUpperCase()));
+      } catch (PatternSyntaxException e)
+      {
+        System.err.println("Invalid pattern ignored: " + token);
+      }
+    }
+    return patterns;
+  }
+
+  /**
+   * Transfers VCF features to sequences to which this sequence has a mapping.
+   * If the mapping is 3:1, computes peptide variants from nucleotide variants.
+   * 
+   * @param seq
+   */
+  protected void transferAddedFeatures(SequenceI seq)
+  {
+    DBRefEntry[] dbrefs = seq.getDBRefs();
+    if (dbrefs == null)
+    {
+      return;
+    }
+    for (DBRefEntry dbref : dbrefs)
+    {
+      Mapping mapping = dbref.getMap();
+      if (mapping == null || mapping.getTo() == null)
+      {
+        continue;
+      }
+
+      SequenceI mapTo = mapping.getTo();
+      MapList map = mapping.getMap();
+      if (map.getFromRatio() == 3)
+      {
+        /*
+         * dna-to-peptide product mapping
+         */
+        AlignmentUtils.computeProteinFeatures(seq, mapTo, map);
+      }
+      else
+      {
+        /*
+         * nucleotide-to-nucleotide mapping e.g. transcript to CDS
+         */
+        List<SequenceFeature> features = seq.getFeatures()
+                .getPositionalFeatures(SequenceOntologyI.SEQUENCE_VARIANT);
+        for (SequenceFeature sf : features)
+        {
+          if (FEATURE_GROUP_VCF.equals(sf.getFeatureGroup()))
+          {
+            transferFeature(sf, mapTo, map);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Tries to add overlapping variants read from a VCF file to the given sequence,
+   * and returns the number of variant features added
+   * 
+   * @param seq
+   * @param vcfAssembly
+   * @return
+   */
+  protected int loadSequenceVCF(SequenceI seq, String vcfAssembly)
+  {
+    VCFMap vcfMap = getVcfMap(seq, vcfAssembly);
+    if (vcfMap == null)
+    {
+      return 0;
+    }
+
+    /*
+     * work with the dataset sequence here
+     */
+    SequenceI dss = seq.getDatasetSequence();
+    if (dss == null)
+    {
+      dss = seq;
+    }
+    return addVcfVariants(dss, vcfMap);
+  }
+
+  /**
+   * Answers a map from sequence coordinates to VCF chromosome ranges
+   * 
+   * @param seq
+   * @param vcfAssembly
+   * @return
+   */
+  private VCFMap getVcfMap(SequenceI seq, String vcfAssembly)
+  {
+    /*
+     * simplest case: sequence has id and length matching a VCF contig
+     */
+    VCFMap vcfMap = null;
+    if (dictionary != null)
+    {
+      vcfMap = getContigMap(seq);
+    }
+    if (vcfMap != null)
+    {
+      return vcfMap;
+    }
+
+    /*
+     * otherwise, map to VCF from chromosomal coordinates 
+     * of the sequence (if known)
+     */
+    GeneLociI seqCoords = seq.getGeneLoci();
+    if (seqCoords == null)
+    {
+      Cache.log.warn(String.format(
+              "Can't query VCF for %s as chromosome coordinates not known",
+              seq.getName()));
+      return null;
+    }
+
+    String species = seqCoords.getSpeciesId();
+    String chromosome = seqCoords.getChromosomeId();
+    String seqRef = seqCoords.getAssemblyId();
+    MapList map = seqCoords.getMap();
+
+    if (!vcfSpeciesMatchesSequence(vcfAssembly, species))
+    {
+      return null;
+    }
+
+    if (vcfAssemblyMatchesSequence(vcfAssembly, seqRef))
+    {
+      return new VCFMap(chromosome, map);
+    }
+
+    if (!"GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+            || !vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+    {
+      return null;
+    }
+
+    /*
+     * map chromosomal coordinates from sequence to VCF if the VCF
+     * data has a different reference assembly to the sequence
+     */
+    // TODO generalise for cases other than GRCh38 -> GRCh37 !
+    // - or get the user to choose in a dialog
+
+    List<int[]> toVcfRanges = new ArrayList<>();
+    List<int[]> fromSequenceRanges = new ArrayList<>();
+    String toRef = "GRCh37";
+
+    for (int[] range : map.getToRanges())
+    {
+      int[] fromRange = map.locateInFrom(range[0], range[1]);
+      if (fromRange == null)
+      {
+        // corrupted map?!?
+        continue;
+      }
+
+      int[] newRange = mapReferenceRange(range, chromosome, "human", seqRef,
+              toRef);
+      if (newRange == null)
+      {
+        Cache.log.error(
+                String.format("Failed to map %s:%s:%s:%d:%d to %s", species,
+                        chromosome, seqRef, range[0], range[1], toRef));
+        continue;
+      }
+      else
+      {
+        toVcfRanges.add(newRange);
+        fromSequenceRanges.add(fromRange);
+      }
+    }
+
+    return new VCFMap(chromosome,
+            new MapList(fromSequenceRanges, toVcfRanges, 1, 1));
+  }
+
+  /**
+   * If the sequence id matches a contig declared in the VCF file, and the
+   * sequence length matches the contig length, then returns a 1:1 map of the
+   * sequence to the contig, else returns null
+   * 
+   * @param seq
+   * @return
+   */
+  private VCFMap getContigMap(SequenceI seq)
+  {
+    String id = seq.getName();
+    SAMSequenceRecord contig = dictionary.getSequence(id);
+    if (contig != null)
+    {
+      int len = seq.getLength();
+      if (len == contig.getSequenceLength())
+      {
+        MapList map = new MapList(new int[] { 1, len },
+                new int[]
+                { 1, len }, 1, 1);
+        return new VCFMap(id, map);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Answers true if we determine that the VCF data uses the same reference
+   * assembly as the sequence, else false
+   * 
+   * @param vcfAssembly
+   * @param seqRef
+   * @return
+   */
+  private boolean vcfAssemblyMatchesSequence(String vcfAssembly,
+          String seqRef)
+  {
+    // TODO improve on this stub, which handles gnomAD and
+    // hopes for the best for other cases
+
+    if ("GRCh38".equalsIgnoreCase(seqRef) // Ensembl
+            && vcfAssembly.contains("Homo_sapiens_assembly19")) // gnomAD
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Answers true if the species inferred from the VCF reference identifier
+   * matches that for the sequence
+   * 
+   * @param vcfAssembly
+   * @param speciesId
+   * @return
+   */
+  boolean vcfSpeciesMatchesSequence(String vcfAssembly, String speciesId)
+  {
+    // PROBLEM 1
+    // there are many aliases for species - how to equate one with another?
+    // PROBLEM 2
+    // VCF ##reference header is an unstructured URI - how to extract species?
+    // perhaps check if ref includes any (Ensembl) alias of speciesId??
+    // TODO ask the user to confirm this??
+
+    if (vcfAssembly.contains("Homo_sapiens") // gnomAD exome data example
+            && "HOMO_SAPIENS".equals(speciesId)) // Ensembl species id
+    {
+      return true;
+    }
+
+    if (vcfAssembly.contains("c_elegans") // VEP VCF response example
+            && "CAENORHABDITIS_ELEGANS".equals(speciesId)) // Ensembl
+    {
+      return true;
+    }
+
+    // this is not a sustainable solution...
+
+    return false;
+  }
+
+  /**
+   * Queries the VCF reader for any variants that overlap the mapped chromosome
+   * ranges of the sequence, and adds as variant features. Returns the number of
+   * overlapping variants found.
+   * 
+   * @param seq
+   * @param map
+   *          mapping from sequence to VCF coordinates
+   * @return
+   */
+  protected int addVcfVariants(SequenceI seq, VCFMap map)
+  {
+    boolean forwardStrand = map.map.isToForwardStrand();
+
+    /*
+     * query the VCF for overlaps of each contiguous chromosomal region
+     */
+    int count = 0;
+
+    for (int[] range : map.map.getToRanges())
+    {
+      int vcfStart = Math.min(range[0], range[1]);
+      int vcfEnd = Math.max(range[0], range[1]);
+      CloseableIterator<VariantContext> variants = reader
+              .query(map.chromosome, vcfStart, vcfEnd);
+      while (variants.hasNext())
+      {
+        VariantContext variant = variants.next();
+
+        int[] featureRange = map.map.locateInFrom(variant.getStart(),
+                variant.getEnd());
+
+        if (featureRange != null)
+        {
+          int featureStart = Math.min(featureRange[0], featureRange[1]);
+          int featureEnd = Math.max(featureRange[0], featureRange[1]);
+          count += addAlleleFeatures(seq, variant, featureStart, featureEnd,
+                  forwardStrand);
+        }
+      }
+      variants.close();
+    }
+
+    return count;
+  }
+
+  /**
+   * A convenience method to get the AF value for the given alternate allele
+   * index
+   * 
+   * @param variant
+   * @param alleleIndex
+   * @return
+   */
+  protected float getAlleleFrequency(VariantContext variant, int alleleIndex)
+  {
+    float score = 0f;
+    String attributeValue = getAttributeValue(variant,
+            ALLELE_FREQUENCY_KEY, alleleIndex);
+    if (attributeValue != null)
+    {
+      try
+      {
+        score = Float.parseFloat(attributeValue);
+      } catch (NumberFormatException e)
+      {
+        // leave as 0
+      }
+    }
+
+    return score;
+  }
+
+  /**
+   * A convenience method to get an attribute value for an alternate allele
+   * 
+   * @param variant
+   * @param attributeName
+   * @param alleleIndex
+   * @return
+   */
+  protected String getAttributeValue(VariantContext variant,
+          String attributeName, int alleleIndex)
+  {
+    Object att = variant.getAttribute(attributeName);
+
+    if (att instanceof String)
+    {
+      return (String) att;
+    }
+    else if (att instanceof ArrayList)
+    {
+      return ((List<String>) att).get(alleleIndex);
+    }
+
+    return null;
+  }
+
+  /**
+   * Adds one variant feature for each allele in the VCF variant record, and
+   * returns the number of features added.
+   * 
+   * @param seq
+   * @param variant
+   * @param featureStart
+   * @param featureEnd
+   * @param forwardStrand
+   * @return
+   */
+  protected int addAlleleFeatures(SequenceI seq, VariantContext variant,
+          int featureStart, int featureEnd, boolean forwardStrand)
+  {
+    int added = 0;
+
+    /*
+     * Javadoc says getAlternateAlleles() imposes no order on the list returned
+     * so we proceed defensively to get them in strict order
+     */
+    int altAlleleCount = variant.getAlternateAlleles().size();
+    for (int i = 0; i < altAlleleCount; i++)
+    {
+      added += addAlleleFeature(seq, variant, i, featureStart, featureEnd,
+              forwardStrand);
+    }
+    return added;
+  }
+
+  /**
+   * Inspects one allele and attempts to add a variant feature for it to the
+   * sequence. The additional data associated with this allele is extracted to
+   * store in the feature's key-value map. Answers the number of features added (0
+   * or 1).
+   * 
+   * @param seq
+   * @param variant
+   * @param altAlleleIndex
+   *          (0, 1..)
+   * @param featureStart
+   * @param featureEnd
+   * @param forwardStrand
+   * @return
+   */
+  protected int addAlleleFeature(SequenceI seq, VariantContext variant,
+          int altAlleleIndex, int featureStart, int featureEnd,
+          boolean forwardStrand)
+  {
+    String reference = variant.getReference().getBaseString();
+    Allele alt = variant.getAlternateAllele(altAlleleIndex);
+    String allele = alt.getBaseString();
+
+    /*
+     * insertion after a genomic base, if on reverse strand, has to be 
+     * converted to insertion of complement after the preceding position 
+     */
+    int referenceLength = reference.length();
+    if (!forwardStrand && allele.length() > referenceLength
+            && allele.startsWith(reference))
+    {
+      featureStart -= referenceLength;
+      featureEnd = featureStart;
+      char insertAfter = seq.getCharAt(featureStart - seq.getStart());
+      reference = Dna.reverseComplement(String.valueOf(insertAfter));
+      allele = allele.substring(referenceLength) + reference;
+    }
+
+    /*
+     * build the ref,alt allele description e.g. "G,A", using the base
+     * complement if the sequence is on the reverse strand
+     */
+    StringBuilder sb = new StringBuilder();
+    sb.append(forwardStrand ? reference : Dna.reverseComplement(reference));
+    sb.append(COMMA);
+    sb.append(forwardStrand ? allele : Dna.reverseComplement(allele));
+    String alleles = sb.toString(); // e.g. G,A
+
+    /*
+     * pick out the consequence data (if any) that is for the current allele
+     * and feature (transcript) that matches the current sequence
+     */
+    String consequence = getConsequenceForAlleleAndFeature(variant, CSQ_FIELD,
+            altAlleleIndex, csqAlleleFieldIndex,
+            csqAlleleNumberFieldIndex, seq.getName().toLowerCase(),
+            csqFeatureFieldIndex);
+
+    /*
+     * pick out the ontology term for the consequence type
+     */
+    String type = SequenceOntologyI.SEQUENCE_VARIANT;
+    if (consequence != null)
+    {
+      type = getOntologyTerm(consequence);
+    }
+
+    float score = getAlleleFrequency(variant, altAlleleIndex);
+
+    SequenceFeature sf = new SequenceFeature(type, alleles, featureStart,
+            featureEnd, score, FEATURE_GROUP_VCF);
+    sf.setSource(sourceId);
+
+    sf.setValue(Gff3Helper.ALLELES, alleles);
+
+    addAlleleProperties(variant, sf, altAlleleIndex, consequence);
+
+    seq.addSequenceFeature(sf);
+
+    return 1;
+  }
+
+  /**
+   * Determines the Sequence Ontology term to use for the variant feature type in
+   * Jalview. The default is 'sequence_variant', but a more specific term is used
+   * if:
+   * <ul>
+   * <li>VEP (or SnpEff) Consequence annotation is included in the VCF</li>
+   * <li>sequence id can be matched to VEP Feature (or SnpEff Feature_ID)</li>
+   * </ul>
+   * 
+   * @param consequence
+   * @return
+   * @see http://www.sequenceontology.org/browser/current_svn/term/SO:0001060
+   */
+  String getOntologyTerm(String consequence)
+  {
+    String type = SequenceOntologyI.SEQUENCE_VARIANT;
+
+    /*
+     * could we associate Consequence data with this allele and feature (transcript)?
+     * if so, prefer the consequence term from that data
+     */
+    if (csqAlleleFieldIndex == -1) // && snpEffAlleleFieldIndex == -1
+    {
+      /*
+       * no Consequence data so we can't refine the ontology term
+       */
+      return type;
+    }
+
+    if (consequence != null)
+    {
+      String[] csqFields = consequence.split(PIPE_REGEX);
+      if (csqFields.length > csqConsequenceFieldIndex)
+      {
+        type = csqFields[csqConsequenceFieldIndex];
+      }
+    }
+    else
+    {
+      // todo the same for SnpEff consequence data matching if wanted
+    }
+
+    /*
+     * if of the form (e.g.) missense_variant&splice_region_variant,
+     * just take the first ('most severe') consequence
+     */
+    if (type != null)
+    {
+      int pos = type.indexOf('&');
+      if (pos > 0)
+      {
+        type = type.substring(0, pos);
+      }
+    }
+    return type;
+  }
+
+  /**
+   * Returns matched consequence data if it can be found, else null.
+   * <ul>
+   * <li>inspects the VCF data for key 'vcfInfoId'</li>
+   * <li>splits this on comma (to distinct consequences)</li>
+   * <li>returns the first consequence (if any) where</li>
+   * <ul>
+   * <li>the allele matches the altAlleleIndex'th allele of variant</li>
+   * <li>the feature matches the sequence name (e.g. transcript id)</li>
+   * </ul>
+   * </ul>
+   * If matched, the consequence is returned (as pipe-delimited fields).
+   * 
+   * @param variant
+   * @param vcfInfoId
+   * @param altAlleleIndex
+   * @param alleleFieldIndex
+   * @param alleleNumberFieldIndex
+   * @param seqName
+   * @param featureFieldIndex
+   * @return
+   */
+  private String getConsequenceForAlleleAndFeature(VariantContext variant,
+          String vcfInfoId, int altAlleleIndex, int alleleFieldIndex,
+          int alleleNumberFieldIndex,
+          String seqName, int featureFieldIndex)
+  {
+    if (alleleFieldIndex == -1 || featureFieldIndex == -1)
+    {
+      return null;
+    }
+    Object value = variant.getAttribute(vcfInfoId);
+
+    if (value == null || !(value instanceof List<?>))
+    {
+      return null;
+    }
+
+    /*
+     * inspect each consequence in turn (comma-separated blocks
+     * extracted by htsjdk)
+     */
+    List<String> consequences = (List<String>) value;
+
+    for (String consequence : consequences)
+    {
+      String[] csqFields = consequence.split(PIPE_REGEX);
+      if (csqFields.length > featureFieldIndex)
+      {
+        String featureIdentifier = csqFields[featureFieldIndex];
+        if (featureIdentifier.length() > 4
+                && seqName.indexOf(featureIdentifier.toLowerCase()) > -1)
+        {
+          /*
+           * feature (transcript) matched - now check for allele match
+           */
+          if (matchAllele(variant, altAlleleIndex, csqFields,
+                  alleleFieldIndex, alleleNumberFieldIndex))
+          {
+            return consequence;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  private boolean matchAllele(VariantContext variant, int altAlleleIndex,
+          String[] csqFields, int alleleFieldIndex,
+          int alleleNumberFieldIndex)
+  {
+    /*
+     * if ALLELE_NUM is present, it must match altAlleleIndex
+     * NB first alternate allele is 1 for ALLELE_NUM, 0 for altAlleleIndex
+     */
+    if (alleleNumberFieldIndex > -1)
+    {
+      if (csqFields.length <= alleleNumberFieldIndex)
+      {
+        return false;
+      }
+      String alleleNum = csqFields[alleleNumberFieldIndex];
+      return String.valueOf(altAlleleIndex + 1).equals(alleleNum);
+    }
+
+    /*
+     * else consequence allele must match variant allele
+     */
+    if (alleleFieldIndex > -1 && csqFields.length > alleleFieldIndex)
+    {
+      String csqAllele = csqFields[alleleFieldIndex];
+      String vcfAllele = variant.getAlternateAllele(altAlleleIndex)
+              .getBaseString();
+      return csqAllele.equals(vcfAllele);
+    }
+    return false;
+  }
+
+  /**
+   * Add any allele-specific VCF key-value data to the sequence feature
+   * 
+   * @param variant
+   * @param sf
+   * @param altAlelleIndex
+   *          (0, 1..)
+   * @param consequence
+   *          if not null, the consequence specific to this sequence (transcript
+   *          feature) and allele
+   */
+  protected void addAlleleProperties(VariantContext variant,
+          SequenceFeature sf, final int altAlelleIndex, String consequence)
+  {
+    Map<String, Object> atts = variant.getAttributes();
+
+    for (Entry<String, Object> att : atts.entrySet())
+    {
+      String key = att.getKey();
+
+      /*
+       * extract Consequence data (if present) that we are able to
+       * associated with the allele for this variant feature
+       */
+      if (CSQ_FIELD.equals(key))
+      {
+        addConsequences(variant, sf, consequence);
+        continue;
+      }
+
+      /*
+       * filter out fields we don't want to capture
+       */
+      if (!vcfFieldsOfInterest.contains(key))
+      {
+        continue;
+      }
+
+      /*
+       * filter out fields we don't want to capture
+       */
+      if (!vcfFieldsOfInterest.contains(key))
+      {
+        continue;
+      }
+
+      /*
+       * we extract values for other data which are allele-specific; 
+       * these may be per alternate allele (INFO[key].Number = 'A') 
+       * or per allele including reference (INFO[key].Number = 'R') 
+       */
+      VCFInfoHeaderLine infoHeader = header.getInfoHeaderLine(key);
+      if (infoHeader == null)
+      {
+        /*
+         * can't be sure what data belongs to this allele, so
+         * play safe and don't take any
+         */
+        continue;
+      }
+
+      VCFHeaderLineCount number = infoHeader.getCountType();
+      int index = altAlelleIndex;
+      if (number == VCFHeaderLineCount.R)
+      {
+        /*
+         * one value per allele including reference, so bump index
+         * e.g. the 3rd value is for the  2nd alternate allele
+         */
+        index++;
+      }
+      else if (number != VCFHeaderLineCount.A)
+      {
+        /*
+         * don't save other values as not allele-related
+         */
+        continue;
+      }
+
+      /*
+       * take the index'th value
+       */
+      String value = getAttributeValue(variant, key, index);
+      if (value != null)
+      {
+        sf.setValue(key, value);
+      }
+    }
+  }
+
+  /**
+   * Inspects CSQ data blocks (consequences) and adds attributes on the sequence
+   * feature.
+   * <p>
+   * If <code>myConsequence</code> is not null, then this is the specific
+   * consequence data (pipe-delimited fields) that is for the current allele and
+   * transcript (sequence) being processed)
+   * 
+   * @param variant
+   * @param sf
+   * @param myConsequence
+   */
+  protected void addConsequences(VariantContext variant, SequenceFeature sf,
+          String myConsequence)
+  {
+    Object value = variant.getAttribute(CSQ_FIELD);
+
+    if (value == null || !(value instanceof List<?>))
+    {
+      return;
+    }
+
+    List<String> consequences = (List<String>) value;
+
+    /*
+     * inspect CSQ consequences; restrict to the consequence
+     * associated with the current transcript (Feature)
+     */
+    Map<String, String> csqValues = new HashMap<>();
+
+    for (String consequence : consequences)
+    {
+      if (myConsequence == null || myConsequence.equals(consequence))
+      {
+        String[] csqFields = consequence.split(PIPE_REGEX);
+
+        /*
+         * inspect individual fields of this consequence, copying non-null
+         * values which are 'fields of interest'
+         */
+        int i = 0;
+        for (String field : csqFields)
+        {
+          if (field != null && field.length() > 0)
+          {
+            String id = vepFieldsOfInterest.get(i);
+            if (id != null)
+            {
+              csqValues.put(id, field);
+            }
+          }
+          i++;
+        }
+      }
+    }
+
+    if (!csqValues.isEmpty())
+    {
+      sf.setValue(CSQ_FIELD, csqValues);
+    }
+  }
+
+  /**
+   * A convenience method to complement a dna base and return the string value
+   * of its complement
+   * 
+   * @param reference
+   * @return
+   */
+  protected String complement(byte[] reference)
+  {
+    return String.valueOf(Dna.getComplement((char) reference[0]));
+  }
+
+  /**
+   * Determines the location of the query range (chromosome positions) in a
+   * different reference assembly.
+   * <p>
+   * If the range is just a subregion of one for which we already have a mapping
+   * (for example, an exon sub-region of a gene), then the mapping is just
+   * computed arithmetically.
+   * <p>
+   * Otherwise, calls the Ensembl REST service that maps from one assembly
+   * reference's coordinates to another's
+   * 
+   * @param queryRange
+   *          start-end chromosomal range in 'fromRef' coordinates
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   *          assembly reference for the query coordinates
+   * @param toRef
+   *          assembly reference we wish to translate to
+   * @return the start-end range in 'toRef' coordinates
+   */
+  protected int[] mapReferenceRange(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    /*
+     * first try shorcut of computing the mapping as a subregion of one
+     * we already have (e.g. for an exon, if we have the gene mapping)
+     */
+    int[] mappedRange = findSubsumedRangeMapping(queryRange, chromosome,
+            species, fromRef, toRef);
+    if (mappedRange != null)
+    {
+      return mappedRange;
+    }
+
+    /*
+     * call (e.g.) http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37
+     */
+    EnsemblMap mapper = new EnsemblMap();
+    int[] mapping = mapper.getAssemblyMapping(species, chromosome, fromRef,
+            toRef, queryRange);
+
+    if (mapping == null)
+    {
+      // mapping service failure
+      return null;
+    }
+
+    /*
+     * save mapping for possible future re-use
+     */
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (!assemblyMappings.containsKey(key))
+    {
+      assemblyMappings.put(key, new HashMap<int[], int[]>());
+    }
+
+    assemblyMappings.get(key).put(queryRange, mapping);
+
+    return mapping;
+  }
+
+  /**
+   * If we already have a 1:1 contiguous mapping which subsumes the given query
+   * range, this method just calculates and returns the subset of that mapping,
+   * else it returns null. In practical terms, if a gene has a contiguous
+   * mapping between (for example) GRCh37 and GRCh38, then we assume that its
+   * subsidiary exons occupy unchanged relative positions, and just compute
+   * these as offsets, rather than do another lookup of the mapping.
+   * <p>
+   * If in future these assumptions prove invalid (e.g. for bacterial dna?!),
+   * simply remove this method or let it always return null.
+   * <p>
+   * Warning: many rapid calls to the /map service map result in a 429 overload
+   * error response
+   * 
+   * @param queryRange
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected int[] findSubsumedRangeMapping(int[] queryRange, String chromosome,
+          String species, String fromRef, String toRef)
+  {
+    String key = makeRangesKey(chromosome, species, fromRef, toRef);
+    if (assemblyMappings.containsKey(key))
+    {
+      Map<int[], int[]> mappedRanges = assemblyMappings.get(key);
+      for (Entry<int[], int[]> mappedRange : mappedRanges.entrySet())
+      {
+        int[] fromRange = mappedRange.getKey();
+        int[] toRange = mappedRange.getValue();
+        if (fromRange[1] - fromRange[0] == toRange[1] - toRange[0])
+        {
+          /*
+           * mapping is 1:1 in length, so we trust it to have no discontinuities
+           */
+          if (MappingUtils.rangeContains(fromRange, queryRange))
+          {
+            /*
+             * fromRange subsumes our query range
+             */
+            int offset = queryRange[0] - fromRange[0];
+            int mappedRangeFrom = toRange[0] + offset;
+            int mappedRangeTo = mappedRangeFrom + (queryRange[1] - queryRange[0]);
+            return new int[] { mappedRangeFrom, mappedRangeTo };
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Transfers the sequence feature to the target sequence, locating its start
+   * and end range based on the mapping. Features which do not overlap the
+   * target sequence are ignored.
+   * 
+   * @param sf
+   * @param targetSequence
+   * @param mapping
+   *          mapping from the feature's coordinates to the target sequence
+   */
+  protected void transferFeature(SequenceFeature sf,
+          SequenceI targetSequence, MapList mapping)
+  {
+    int[] mappedRange = mapping.locateInTo(sf.getBegin(), sf.getEnd());
+  
+    if (mappedRange != null)
+    {
+      String group = sf.getFeatureGroup();
+      int newBegin = Math.min(mappedRange[0], mappedRange[1]);
+      int newEnd = Math.max(mappedRange[0], mappedRange[1]);
+      SequenceFeature copy = new SequenceFeature(sf, newBegin, newEnd,
+              group, sf.getScore());
+      targetSequence.addSequenceFeature(copy);
+    }
+  }
+
+  /**
+   * Formats a ranges map lookup key
+   * 
+   * @param chromosome
+   * @param species
+   * @param fromRef
+   * @param toRef
+   * @return
+   */
+  protected static String makeRangesKey(String chromosome, String species,
+          String fromRef, String toRef)
+  {
+    return species + EXCL + chromosome + EXCL + fromRef + EXCL
+            + toRef;
+  }
+}
index 083cd26..29f3fa9 100644 (file)
@@ -39,19 +39,6 @@ public class JSFunctionExec implements Runnable
     jvlite.setExecutor(this);
   }
 
-  @Override
-  protected void finalize() throws Throwable
-  {
-    jvlite = null;
-    executor = null;
-    if (jsExecQueue != null)
-    {
-      jsExecQueue.clear();
-    }
-    jsExecQueue = null;
-    super.finalize();
-  }
-
   private Vector jsExecQueue;
 
   private Thread executor = null;
index 874bfd3..6071933 100644 (file)
@@ -299,13 +299,6 @@ public class MouseOverStructureListener extends JSFunctionExec
   }
 
   @Override
-  public void finalize() throws Throwable
-  {
-    jvlite = null;
-    super.finalize();
-  }
-
-  @Override
   public void releaseReferences(Object svl)
   {
 
index 86d0c85..1cf482d 100755 (executable)
@@ -147,6 +147,8 @@ public class GAlignFrame extends JInternalFrame
 
   protected JMenuItem runGroovy = new JMenuItem();
 
+  protected JMenuItem loadVcf;
+
   protected JCheckBoxMenuItem autoCalculate = new JCheckBoxMenuItem();
 
   protected JCheckBoxMenuItem sortByTree = new JCheckBoxMenuItem();
@@ -1308,6 +1310,16 @@ public class GAlignFrame extends JInternalFrame
         associatedData_actionPerformed(e);
       }
     });
+    loadVcf = new JMenuItem(MessageManager.getString("label.load_vcf_file"));
+    loadVcf.setToolTipText(MessageManager.getString("label.load_vcf"));
+    loadVcf.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        loadVcf_actionPerformed();
+      }
+    });
     autoCalculate.setText(
             MessageManager.getString("label.autocalculate_consensus"));
     autoCalculate.setState(
@@ -1710,6 +1722,7 @@ public class GAlignFrame extends JInternalFrame
     fileMenu.add(exportAnnotations);
     fileMenu.add(loadTreeMenuItem);
     fileMenu.add(associatedData);
+    fileMenu.add(loadVcf);
     fileMenu.addSeparator();
     fileMenu.add(closeMenuItem);
 
@@ -1855,6 +1868,10 @@ public class GAlignFrame extends JInternalFrame
     // selectMenu.add(listenToViewSelections);
   }
 
+  protected void loadVcf_actionPerformed()
+  {
+  }
+
   /**
    * Constructs the entries on the Colour menu (but does not add them to the
    * menu).
index abc0b3d..a6e0ace 100644 (file)
@@ -39,6 +39,8 @@ import javax.swing.JMenuBar;
 import javax.swing.JMenuItem;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.text.EditorKit;
+import javax.swing.text.html.HTMLEditorKit;
 
 /**
  * DOCUMENT ME!
@@ -85,6 +87,7 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame
   {
     try
     {
+      textarea.setEditorKit(new HTMLEditorKit());
       setJMenuBar(editMenubar);
       jbInit();
     } catch (Exception e)
@@ -272,4 +275,20 @@ public class GCutAndPasteHtmlTransfer extends JInternalFrame
   {
 
   }
+
+  /**
+   * Adds the given stylesheet rule to the Html editor. However note that CSS
+   * support is limited.
+   * 
+   * @param rule
+   * @see javax.swing.text.html.CSS
+   */
+  public void addStylesheetRule(String rule)
+  {
+    EditorKit editorKit = textarea.getEditorKit();
+    if (editorKit != null)
+    {
+      ((HTMLEditorKit) editorKit).getStyleSheet().addRule(rule);
+    }
+  }
 }
index 1ca0802..26e0919 100755 (executable)
@@ -524,7 +524,9 @@ public class GPreferences extends JPanel
             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
@@ -1206,14 +1208,14 @@ public class GPreferences extends JPanel
     pathLabel.setFont(new java.awt.Font("SansSerif", 0, 11));
     pathLabel.setHorizontalAlignment(SwingConstants.LEFT);
     pathLabel.setText(MessageManager.getString("label.chimera_path"));
-    final String tooltip = JvSwingUtils.wrapTooltip(true,
-            MessageManager.getString("label.chimera_path_tip"));
-    pathLabel.setToolTipText(tooltip);
     pathLabel.setBounds(new Rectangle(10, ypos, 140, height));
     structureTab.add(pathLabel);
 
     chimeraPath.setFont(LABEL_FONT);
     chimeraPath.setText("");
+    final String tooltip = JvSwingUtils.wrapTooltip(true,
+            MessageManager.getString("label.chimera_path_tip"));
+    chimeraPath.setToolTipText(tooltip);
     chimeraPath.setBounds(new Rectangle(160, ypos, 300, height));
     chimeraPath.addMouseListener(new MouseAdapter()
     {
@@ -1512,6 +1514,9 @@ public class GPreferences extends JPanel
     startupCheckbox.setSelected(true);
     startupFileTextfield.setFont(LABEL_FONT);
     startupFileTextfield.setBounds(new Rectangle(172, 310, 330, 20));
+    final String tooltip = JvSwingUtils.wrapTooltip(true,
+            MessageManager.getString("label.double_click_to_browse"));
+    startupFileTextfield.setToolTipText(tooltip);
     startupFileTextfield.addMouseListener(new MouseAdapter()
     {
       @Override
index 7c4672a..240e1fd 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8.2)
- * Copyright (C) 2014 The Jalview Authors
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
  * 
  * This file is part of Jalview.
  * 
@@ -29,6 +29,7 @@ import jalview.fts.service.pdb.PDBFTSRestClient;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.Desktop;
 import jalview.gui.JvSwingUtils;
+import jalview.gui.StructureViewer;
 import jalview.util.MessageManager;
 
 import java.awt.BorderLayout;
@@ -36,6 +37,7 @@ import java.awt.CardLayout;
 import java.awt.Component;
 import java.awt.Dimension;
 import java.awt.FlowLayout;
+import java.awt.Font;
 import java.awt.GridLayout;
 import java.awt.event.ActionEvent;
 import java.awt.event.ItemEvent;
@@ -70,6 +72,8 @@ import javax.swing.event.DocumentListener;
 import javax.swing.event.InternalFrameEvent;
 import javax.swing.table.TableColumn;
 
+import net.miginfocom.swing.MigLayout;
+
 @SuppressWarnings("serial")
 /**
  * GUI layout for structure chooser
@@ -80,58 +84,50 @@ import javax.swing.table.TableColumn;
 public abstract class GStructureChooser extends JPanel
         implements ItemListener
 {
+  private static final Font VERDANA_12 = new Font("Verdana", 0, 12);
+
+  protected static final String VIEWS_FILTER = "VIEWS_FILTER";
+
+  protected static final String VIEWS_FROM_FILE = "VIEWS_FROM_FILE";
+
+  protected static final String VIEWS_ENTER_ID = "VIEWS_ENTER_ID";
+
+  /*
+   * 'cached' structure view
+   */
+  protected static final String VIEWS_LOCAL_PDB = "VIEWS_LOCAL_PDB";
+
   protected JPanel statusPanel = new JPanel();
 
   public JLabel statusBar = new JLabel();
 
-  private JPanel pnl_actionsAndStatus = new JPanel(new BorderLayout());
-
   protected String frameTitle = MessageManager
           .getString("label.structure_chooser");
 
   protected JInternalFrame mainFrame = new JInternalFrame(frameTitle);
 
-  protected JComboBox<FilterOption> cmb_filterOption = new JComboBox<FilterOption>();
+  protected JComboBox<FilterOption> cmb_filterOption = new JComboBox<>();
 
   protected AlignmentPanel ap;
 
   protected StringBuilder errorWarning = new StringBuilder();
 
-  protected JLabel lbl_result = new JLabel(
-          MessageManager.getString("label.select"));
-
-  protected JButton btn_view = new JButton();
+  protected JButton btn_add;
 
-  protected JButton btn_cancel = new JButton();
+  protected JButton btn_newView;
 
   protected JButton btn_pdbFromFile = new JButton();
 
-  protected JTextField txt_search = new JTextField(14);
-
-  private JPanel pnl_actions = new JPanel();
-
-  private JPanel pnl_main = new JPanel();
-
-  private JPanel pnl_idInput = new JPanel(new FlowLayout());
+  protected JCheckBox chk_superpose = new JCheckBox(
+          MessageManager.getString("label.superpose_structures"));
 
-  private JPanel pnl_fileChooser = new JPanel(new FlowLayout());
-
-  private JPanel pnl_idInputBL = new JPanel(new BorderLayout());
-
-  private JPanel pnl_fileChooserBL = new JPanel(new BorderLayout());
-
-  private JPanel pnl_locPDB = new JPanel(new BorderLayout());
+  protected JTextField txt_search = new JTextField(14);
 
   protected JPanel pnl_switchableViews = new JPanel(new CardLayout());
 
   protected CardLayout layout_switchableViews = (CardLayout) (pnl_switchableViews
           .getLayout());
 
-  private BorderLayout mainLayout = new BorderLayout();
-
-  protected JCheckBox chk_rememberSettings = new JCheckBox(
-          MessageManager.getString("label.dont_ask_me_again"));
-
   protected JCheckBox chk_invertFilter = new JCheckBox(
           MessageManager.getString("label.invert"));
 
@@ -147,33 +143,20 @@ public abstract class GStructureChooser extends JPanel
   protected ImageIcon warningImage = new ImageIcon(
           getClass().getResource("/images/warning.gif"));
 
-  protected JLabel lbl_warning = new JLabel(warningImage);
-
   protected JLabel lbl_loading = new JLabel(loadingImage);
 
   protected JLabel lbl_pdbManualFetchStatus = new JLabel(errorImage);
 
   protected JLabel lbl_fromFileStatus = new JLabel(errorImage);
 
-  protected AssciateSeqPanel idInputAssSeqPanel = new AssciateSeqPanel();
-
-  protected AssciateSeqPanel fileChooserAssSeqPanel = new AssciateSeqPanel();
-
-  protected static final String VIEWS_FILTER = "VIEWS_FILTER";
+  protected AssociateSeqPanel idInputAssSeqPanel = new AssociateSeqPanel();
 
-  protected static final String VIEWS_FROM_FILE = "VIEWS_FROM_FILE";
+  protected AssociateSeqPanel fileChooserAssSeqPanel = new AssociateSeqPanel();
 
-  protected static final String VIEWS_ENTER_ID = "VIEWS_ENTER_ID";
-
-  /**
-   * 'cached' structure view
-   */
-  protected static final String VIEWS_LOCAL_PDB = "VIEWS_LOCAL_PDB";
+  protected JComboBox<StructureViewer> targetView = new JComboBox<>();
 
   protected JTable tbl_local_pdb = new JTable();
 
-  protected JScrollPane scrl_localPDB = new JScrollPane(tbl_local_pdb);
-
   protected JTabbedPane pnl_filter = new JTabbedPane();
 
   protected FTSDataColumnPreferences pdbDocFieldPrefs = new FTSDataColumnPreferences(
@@ -182,7 +165,7 @@ public abstract class GStructureChooser extends JPanel
 
   protected FTSDataColumnI[] previousWantedFields;
 
-  protected static Map<String, Integer> tempUserPrefs = new HashMap<String, Integer>();
+  protected static Map<String, Integer> tempUserPrefs = new HashMap<>();
 
   private JTable tbl_summary = new JTable()
   {
@@ -262,8 +245,6 @@ public abstract class GStructureChooser extends JPanel
     }
   };
 
-  protected JScrollPane scrl_foundStructures = new JScrollPane(tbl_summary);
-
   public GStructureChooser()
   {
     try
@@ -319,9 +300,9 @@ public abstract class GStructureChooser extends JPanel
           mainFrame.dispose();
           break;
         case KeyEvent.VK_ENTER: // enter key
-          if (btn_view.isEnabled())
+          if (btn_add.isEnabled())
           {
-            ok_ActionPerformed();
+            add_ActionPerformed();
           }
           break;
         case KeyEvent.VK_TAB: // tab key
@@ -331,7 +312,7 @@ public abstract class GStructureChooser extends JPanel
           }
           else
           {
-            btn_view.requestFocus();
+            btn_add.requestFocus();
           }
           evt.consume();
           break;
@@ -340,6 +321,30 @@ public abstract class GStructureChooser extends JPanel
         }
       }
     });
+
+    JButton btn_cancel = new JButton(
+            MessageManager.getString("action.cancel"));
+    btn_cancel.setFont(VERDANA_12);
+    btn_cancel.addActionListener(new java.awt.event.ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent e)
+      {
+        closeAction(pnl_filter.getHeight());
+      }
+    });
+    btn_cancel.addKeyListener(new KeyAdapter()
+    {
+      @Override
+      public void keyPressed(KeyEvent evt)
+      {
+        if (evt.getKeyCode() == KeyEvent.VK_ENTER)
+        {
+          closeAction(pnl_filter.getHeight());
+        }
+      }
+    });
+
     tbl_local_pdb.setAutoCreateRowSorter(true);
     tbl_local_pdb.getTableHeader().setReorderingAllowed(false);
     tbl_local_pdb.addMouseListener(new MouseAdapter()
@@ -368,9 +373,9 @@ public abstract class GStructureChooser extends JPanel
           mainFrame.dispose();
           break;
         case KeyEvent.VK_ENTER: // enter key
-          if (btn_view.isEnabled())
+          if (btn_add.isEnabled())
           {
-            ok_ActionPerformed();
+            add_ActionPerformed();
           }
           break;
         case KeyEvent.VK_TAB: // tab key
@@ -380,9 +385,9 @@ public abstract class GStructureChooser extends JPanel
           }
           else
           {
-            if (btn_view.isEnabled())
+            if (btn_add.isEnabled())
             {
-              btn_view.requestFocus();
+              btn_add.requestFocus();
             }
             else
             {
@@ -396,51 +401,52 @@ public abstract class GStructureChooser extends JPanel
         }
       }
     });
-    btn_view.setFont(new java.awt.Font("Verdana", 0, 12));
-    btn_view.setText(MessageManager.getString("action.view"));
-    btn_view.addActionListener(new java.awt.event.ActionListener()
+
+    btn_newView = new JButton(MessageManager.getString("action.new_view"));
+    btn_newView.setFont(VERDANA_12);
+    btn_newView.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        ok_ActionPerformed();
+        newView_ActionPerformed();
       }
     });
-    btn_view.addKeyListener(new KeyAdapter()
+    btn_newView.addKeyListener(new KeyAdapter()
     {
       @Override
       public void keyPressed(KeyEvent evt)
       {
         if (evt.getKeyCode() == KeyEvent.VK_ENTER)
         {
-          ok_ActionPerformed();
+          newView_ActionPerformed();
         }
       }
     });
 
-    btn_cancel.setFont(new java.awt.Font("Verdana", 0, 12));
-    btn_cancel.setText(MessageManager.getString("action.cancel"));
-    btn_cancel.addActionListener(new java.awt.event.ActionListener()
+    btn_add = new JButton(MessageManager.getString("action.add"));
+    btn_add.setFont(VERDANA_12);
+    btn_add.addActionListener(new java.awt.event.ActionListener()
     {
       @Override
       public void actionPerformed(ActionEvent e)
       {
-        closeAction(pnl_filter.getHeight());
+        add_ActionPerformed();
       }
     });
-    btn_cancel.addKeyListener(new KeyAdapter()
+    btn_add.addKeyListener(new KeyAdapter()
     {
       @Override
       public void keyPressed(KeyEvent evt)
       {
         if (evt.getKeyCode() == KeyEvent.VK_ENTER)
         {
-          closeAction(pnl_filter.getHeight());
+          add_ActionPerformed();
         }
       }
     });
 
-    btn_pdbFromFile.setFont(new java.awt.Font("Verdana", 0, 12));
+    btn_pdbFromFile.setFont(VERDANA_12);
     String btn_title = MessageManager.getString("label.select_pdb_file");
     btn_pdbFromFile.setText(btn_title + "              ");
     btn_pdbFromFile.addActionListener(new java.awt.event.ActionListener()
@@ -463,20 +469,17 @@ public abstract class GStructureChooser extends JPanel
       }
     });
 
+    JScrollPane scrl_foundStructures = new JScrollPane(tbl_summary);
     scrl_foundStructures.setPreferredSize(new Dimension(width, height));
 
+    JScrollPane scrl_localPDB = new JScrollPane(tbl_local_pdb);
     scrl_localPDB.setPreferredSize(new Dimension(width, height));
     scrl_localPDB.setHorizontalScrollBarPolicy(
             JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
 
-    cmb_filterOption.setFont(new java.awt.Font("Verdana", 0, 12));
-    chk_invertFilter.setFont(new java.awt.Font("Verdana", 0, 12));
-    chk_rememberSettings.setFont(new java.awt.Font("Verdana", 0, 12));
-    chk_rememberSettings.setVisible(false);
+    chk_invertFilter.setFont(VERDANA_12);
     txt_search.setToolTipText(JvSwingUtils.wrapTooltip(true,
-            MessageManager.getString("label.enter_pdb_id")));
-    cmb_filterOption.setToolTipText(
-            MessageManager.getString("info.select_filter_option"));
+            MessageManager.getString("label.enter_pdb_id_tip")));
     txt_search.getDocument().addDocumentListener(new DocumentListener()
     {
       @Override
@@ -498,8 +501,10 @@ public abstract class GStructureChooser extends JPanel
       }
     });
 
+    cmb_filterOption.setFont(VERDANA_12);
+    cmb_filterOption.setToolTipText(
+            MessageManager.getString("info.select_filter_option"));
     cmb_filterOption.addItemListener(this);
-
     // add CustomComboSeparatorsRenderer to filter option combo-box
     cmb_filterOption.setRenderer(new CustomComboSeparatorsRenderer(
             (ListCellRenderer<Object>) cmb_filterOption.getRenderer())
@@ -514,23 +519,33 @@ public abstract class GStructureChooser extends JPanel
 
     chk_invertFilter.addItemListener(this);
 
-    pnl_actions.add(chk_rememberSettings);
-    pnl_actions.add(btn_view);
-    pnl_actions.add(btn_cancel);
+    targetView.setVisible(false);
 
-    // pnl_filter.add(lbl_result);
+    JPanel actionsPanel = new JPanel(new MigLayout());
+    actionsPanel.add(targetView, "left");
+    actionsPanel.add(btn_add, "wrap");
+    actionsPanel.add(chk_superpose, "left");
+    actionsPanel.add(btn_newView);
+    actionsPanel.add(btn_cancel, "right");
+
+    JPanel pnl_main = new JPanel();
     pnl_main.add(cmb_filterOption);
     pnl_main.add(lbl_loading);
     pnl_main.add(chk_invertFilter);
     lbl_loading.setVisible(false);
 
+    JPanel pnl_fileChooser = new JPanel(new FlowLayout());
     pnl_fileChooser.add(btn_pdbFromFile);
     pnl_fileChooser.add(lbl_fromFileStatus);
+    JPanel pnl_fileChooserBL = new JPanel(new BorderLayout());
     pnl_fileChooserBL.add(fileChooserAssSeqPanel, BorderLayout.NORTH);
     pnl_fileChooserBL.add(pnl_fileChooser, BorderLayout.CENTER);
 
+    JPanel pnl_idInput = new JPanel(new FlowLayout());
     pnl_idInput.add(txt_search);
     pnl_idInput.add(lbl_pdbManualFetchStatus);
+
+    JPanel pnl_idInputBL = new JPanel(new BorderLayout());
     pnl_idInputBL.add(idInputAssSeqPanel, BorderLayout.NORTH);
     pnl_idInputBL.add(pnl_idInput, BorderLayout.CENTER);
 
@@ -546,13 +561,15 @@ public abstract class GStructureChooser extends JPanel
         JTabbedPane sourceTabbedPane = (JTabbedPane) changeEvent
                 .getSource();
         int index = sourceTabbedPane.getSelectedIndex();
-        btn_view.setVisible(true);
+        btn_add.setVisible(targetView.isVisible());
+        btn_newView.setVisible(true);
         btn_cancel.setVisible(true);
         if (sourceTabbedPane.getTitleAt(index).equals(configureCols))
         {
-          btn_view.setEnabled(false);
+          btn_add.setEnabled(false);
           btn_cancel.setEnabled(false);
-          btn_view.setVisible(false);
+          btn_add.setVisible(false);
+          btn_newView.setEnabled(false);
           btn_cancel.setVisible(false);
           previousWantedFields = pdbDocFieldPrefs
                   .getStructureSummaryFields()
@@ -578,6 +595,7 @@ public abstract class GStructureChooser extends JPanel
     pnl_filter.add(foundStructureSummary, scrl_foundStructures);
     pnl_filter.add(configureCols, pdbDocFieldPrefs);
 
+    JPanel pnl_locPDB = new JPanel(new BorderLayout());
     pnl_locPDB.add(scrl_localPDB);
 
     pnl_switchableViews.add(pnl_fileChooserBL, VIEWS_FROM_FILE);
@@ -585,12 +603,14 @@ public abstract class GStructureChooser extends JPanel
     pnl_switchableViews.add(pnl_filter, VIEWS_FILTER);
     pnl_switchableViews.add(pnl_locPDB, VIEWS_LOCAL_PDB);
 
-    this.setLayout(mainLayout);
+    this.setLayout(new BorderLayout());
     this.add(pnl_main, java.awt.BorderLayout.NORTH);
     this.add(pnl_switchableViews, java.awt.BorderLayout.CENTER);
     // this.add(pnl_actions, java.awt.BorderLayout.SOUTH);
     statusPanel.setLayout(new GridLayout());
-    pnl_actionsAndStatus.add(pnl_actions, BorderLayout.CENTER);
+
+    JPanel pnl_actionsAndStatus = new JPanel(new BorderLayout());
+    pnl_actionsAndStatus.add(actionsPanel, BorderLayout.CENTER);
     pnl_actionsAndStatus.add(statusPanel, BorderLayout.SOUTH);
     statusPanel.add(statusBar, null);
     this.add(pnl_actionsAndStatus, java.awt.BorderLayout.SOUTH);
@@ -801,13 +821,13 @@ public abstract class GStructureChooser extends JPanel
    * @author tcnofoegbu
    *
    */
-  public class AssciateSeqPanel extends JPanel implements ItemListener
+  public class AssociateSeqPanel extends JPanel implements ItemListener
   {
-    private JComboBox<AssociateSeqOptions> cmb_assSeq = new JComboBox<AssociateSeqOptions>();
+    private JComboBox<AssociateSeqOptions> cmb_assSeq = new JComboBox<>();
 
     private JLabel lbl_associateSeq = new JLabel();
 
-    public AssciateSeqPanel()
+    public AssociateSeqPanel()
     {
       this.setLayout(new FlowLayout());
       this.add(cmb_assSeq);
@@ -901,19 +921,21 @@ public abstract class GStructureChooser extends JPanel
 
   protected abstract void stateChanged(ItemEvent e);
 
-  protected abstract void ok_ActionPerformed();
+  protected abstract void add_ActionPerformed();
+
+  protected abstract void newView_ActionPerformed();
 
   protected abstract void pdbFromFile_actionPerformed();
 
   protected abstract void txt_search_ActionPerformed();
 
-  public abstract void populateCmbAssociateSeqOptions(
+  protected abstract void populateCmbAssociateSeqOptions(
           JComboBox<AssociateSeqOptions> cmb_assSeq,
           JLabel lbl_associateSeq);
 
-  public abstract void cmbAssSeqStateChanged();
+  protected abstract void cmbAssSeqStateChanged();
 
-  public abstract void tabRefresh();
+  protected abstract void tabRefresh();
 
-  public abstract void validateSelections();
+  protected abstract void validateSelections();
 }
\ No newline at end of file
index 41772d4..adca17e 100644 (file)
@@ -162,7 +162,8 @@ public class AnnotationRenderer
           boolean validRes, boolean validEnd)
   {
     g.setColor(STEM_COLOUR);
-    int sCol = (lastSSX / charWidth) + startRes;
+    int sCol = (lastSSX / charWidth)
+            + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
     int x2 = (x * charWidth);
 
@@ -228,7 +229,8 @@ public class AnnotationRenderer
     // System.out.println(nonCanColor);
 
     g.setColor(nonCanColor);
-    int sCol = (lastSSX / charWidth) + startRes;
+    int sCol = (lastSSX / charWidth)
+            + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
     int x2 = (x * charWidth);
 
@@ -600,7 +602,7 @@ public class AnnotationRenderer
         {
           if (hasHiddenColumns)
           {
-            column = hiddenColumns.adjustForHiddenColumns(startRes + x);
+            column = hiddenColumns.visibleToAbsoluteColumn(startRes + x);
             if (column > row_annotations.length - 1)
             {
               break;
@@ -1147,7 +1149,8 @@ public class AnnotationRenderer
   {
     g.setColor(HELIX_COLOUR);
 
-    int sCol = (lastSSX / charWidth) + startRes;
+    int sCol = (lastSSX / charWidth)
+            + hiddenColumns.visibleToAbsoluteColumn(startRes);
     int x1 = lastSSX;
     int x2 = (x * charWidth);
 
@@ -1247,7 +1250,7 @@ public class AnnotationRenderer
       column = sRes + x;
       if (hasHiddenColumns)
       {
-        column = hiddenColumns.adjustForHiddenColumns(column);
+        column = hiddenColumns.visibleToAbsoluteColumn(column);
       }
 
       if (column > aaMax)
@@ -1327,7 +1330,7 @@ public class AnnotationRenderer
       column = sRes + x;
       if (hasHiddenColumns)
       {
-        column = hiddenColumns.adjustForHiddenColumns(column);
+        column = hiddenColumns.visibleToAbsoluteColumn(column);
       }
 
       if (column > aaMax)
index 1c50aab..e9b4de4 100644 (file)
@@ -44,8 +44,6 @@ public class OverviewRenderer
   // transparency of hidden cols/seqs overlay
   private final float TRANSPARENCY = 0.5f;
 
-  private final Color HIDDEN_COLOUR = Color.DARK_GRAY.darker();
-
   public static final String UPDATE = "OverviewUpdate";
 
   private static final int MAX_PROGRESS = 100;
@@ -152,7 +150,7 @@ public class OverviewRenderer
         if (pixelCol <= endCol)
         {
           rgbcolor = getColumnColourFromSequence(allGroups, seq,
-                  alignmentCol, finder);
+                  alignmentCol);
     
           // fill in the appropriate number of pixels
           for (int row = pixelRow; row <= endRow; ++row)
@@ -216,27 +214,23 @@ public class OverviewRenderer
   }
 
   /*
-   * Find the colour of a sequence at a specified column position
+   * Find the RGB value of the colour of a sequence at a specified column position
    * 
    * @param seq
    *          sequence to get colour for
    * @param lastcol
    *          column position to get colour for
-   * @param fcfinder
-   *          FeatureColourFinder to use
    * @return colour of sequence at this position, as RGB
    */
-  private int getColumnColourFromSequence(SequenceGroup[] allGroups,
-          jalview.datamodel.SequenceI seq,
-          int lastcol, FeatureColourFinder fcfinder)
+  int getColumnColourFromSequence(SequenceGroup[] allGroups,
+          SequenceI seq, int lastcol)
   {
-    Color color = Color.white;
+    Color color = resColFinder.GAP_COLOUR;
 
     if ((seq != null) && (seq.getLength() > lastcol))
     {
       color = resColFinder.getResidueColour(true, shader, allGroups, seq,
-              lastcol,
-              fcfinder);
+              lastcol, finder);
     }
 
     return color.getRGB();
@@ -359,15 +353,13 @@ public class OverviewRenderer
    *          the graphics object to draw on
    * @param anno
    *          alignment annotation information
-   * @param charWidth
-   *          alignment character width value
    * @param y
    *          y-position for the annotation graph
    * @param cols
    *          the collection of columns used in the overview panel
    */
-  public void drawGraph(Graphics g, AlignmentAnnotation anno, int charWidth,
-          int y, AlignmentColsCollectionI cols)
+  public void drawGraph(Graphics g, AlignmentAnnotation anno, int y,
+          AlignmentColsCollectionI cols)
   {
     Annotation[] annotations = anno.annotations;
     g.setColor(Color.white);
index 9fec256..dc3272f 100644 (file)
 package jalview.renderer;
 
 import jalview.api.AlignViewportI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.SequenceI;
 
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 
 /**
@@ -34,12 +36,24 @@ import java.util.List;
  */
 public class ScaleRenderer
 {
+  /**
+   * Represents one major or minor scale mark
+   */
   public final class ScaleMark
   {
+    /**
+     * true for a major scale mark, false for minor
+     */
     public final boolean major;
 
+    /**
+     * visible column position (0..) e.g. 19
+     */
     public final int column;
 
+    /**
+     * text (if any) to show e.g. "20"
+     */
     public final String text;
 
     ScaleMark(boolean isMajor, int col, String txt)
@@ -68,17 +82,27 @@ public class ScaleRenderer
     int scalestartx = (startx / 10) * 10;
 
     SequenceI refSeq = av.getAlignment().getSeqrep();
-    int refSp = 0, refStartI = 0, refEndI = -1;
+    int refSp = 0;
+    int refStartI = 0;
+    int refEndI = -1;
+
+    HiddenColumns hc = av.getAlignment().getHiddenColumns();
+
     if (refSeq != null)
     {
-      // find bounds and set origin appopriately
-      // locate first visible position for this sequence
-      int[] refbounds = av.getAlignment().getHiddenColumns()
-              .locateVisibleBoundsOfSequence(refSeq);
+      // find bounds and set origin appropriately
+      // locate first residue in sequence which is not hidden
+      Iterator<int[]> it = hc.iterator();
+      int index = refSeq.firstResidueOutsideIterator(it);
+      refSp = hc.absoluteToVisibleColumn(index);
+
+      refStartI = refSeq.findIndex(refSeq.getStart()) - 1;
+
+      int seqlength = refSeq.getLength();
+      // get sequence position past the end of the sequence
+      int pastEndPos = refSeq.findPosition(seqlength + 1);
+      refEndI = refSeq.findIndex(pastEndPos - 1) - 1;
 
-      refSp = refbounds[0];
-      refStartI = refbounds[4];
-      refEndI = refbounds[5];
       scalestartx = refSp + ((scalestartx - refSp) / 10) * 10;
     }
 
@@ -86,24 +110,22 @@ public class ScaleRenderer
     {
       scalestartx += 5;
     }
-    List<ScaleMark> marks = new ArrayList<ScaleMark>();
+    List<ScaleMark> marks = new ArrayList<>();
     String string;
     int refN, iadj;
     // todo: add a 'reference origin column' to set column number relative to
-    for (int i = scalestartx; i < endx; i += 5)
+    for (int i = scalestartx; i <= endx; i += 5)
     {
       if (((i - refSp) % 10) == 0)
       {
         if (refSeq == null)
         {
-          iadj = av.getAlignment().getHiddenColumns()
-                  .adjustForHiddenColumns(i - 1) + 1;
+          iadj = hc.visibleToAbsoluteColumn(i - 1) + 1;
           string = String.valueOf(iadj);
         }
         else
         {
-          iadj = av.getAlignment().getHiddenColumns()
-                  .adjustForHiddenColumns(i - 1);
+          iadj = hc.visibleToAbsoluteColumn(i - 1);
           refN = refSeq.findPosition(iadj);
           // TODO show bounds if position is a gap
           // - ie L--R -> "1L|2R" for
index e81e519..795cd36 100644 (file)
@@ -216,7 +216,8 @@ public class FeatureRenderer extends FeatureRendererModel
       return null;
     }
 
-    if (Comparison.isGap(seq.getCharAt(column)))
+    // column is 'base 1' but getCharAt is an array index (ie from 0)
+    if (Comparison.isGap(seq.getCharAt(column - 1)))
     {
       /*
        * returning null allows the colour scheme to provide gap colour
@@ -303,14 +304,19 @@ public class FeatureRenderer extends FeatureRendererModel
       List<SequenceFeature> overlaps = seq.getFeatures().findFeatures(
               visiblePositions.getBegin(), visiblePositions.getEnd(), type);
 
-      filterFeaturesForDisplay(overlaps, fc);
+      if (fc.isSimpleColour())
+      {
+        filterFeaturesForDisplay(overlaps);
+      }
 
       for (SequenceFeature sf : overlaps)
       {
-        Color featureColour = fc.getColor(sf);
+        Color featureColour = getColor(sf, fc);
         if (featureColour == null)
         {
-          // score feature outwith threshold for colouring
+          /*
+           * feature excluded by visibility settings, filters, or colour threshold
+           */
           continue;
         }
 
@@ -401,27 +407,6 @@ public class FeatureRenderer extends FeatureRendererModel
   }
 
   /**
-<<<<<<< HEAD
-=======
-   * Answers true if the feature belongs to a feature group which is not
-   * currently displayed, else false
-   * 
-   * @param sequenceFeature
-   * @return
-   */
-  @Override
-  protected boolean featureGroupNotShown(
-          final SequenceFeature sequenceFeature)
-  {
-    return featureGroups != null && sequenceFeature.featureGroup != null
-            && sequenceFeature.featureGroup.length() != 0
-            && featureGroups.containsKey(sequenceFeature.featureGroup)
-            && !featureGroups.get(sequenceFeature.featureGroup)
-                    .booleanValue();
-  }
-
-  /**
->>>>>>> refs/heads/develop
    * Called when alignment in associated view has new/modified features to
    * discover and display.
    * 
index 0a01103..e1100a8 100644 (file)
@@ -1,4 +1,4 @@
-#Mon Jun 20 15:44:52 BST 2016
+#Thu Dec 14 09:10:14 GMT 2017
 jalview.schemabinding.version2.ThresholdLine=jalview.schemabinding.version2.descriptors.ThresholdLineDescriptor
 jalview.schemabinding.version2.SequenceSetProperties=jalview.schemabinding.version2.descriptors.SequenceSetPropertiesDescriptor
 jalview.schemabinding.version2.StructureState=jalview.schemabinding.version2.descriptors.StructureStateDescriptor
@@ -10,7 +10,9 @@ jalview.schemabinding.version2.OtherData=jalview.schemabinding.version2.descript
 jalview.schemabinding.version2.Setting=jalview.schemabinding.version2.descriptors.SettingDescriptor
 jalview.schemabinding.version2.AlcodonFrame=jalview.schemabinding.version2.descriptors.AlcodonFrameDescriptor
 jalview.schemabinding.version2.AnnotationElement=jalview.schemabinding.version2.descriptors.AnnotationElementDescriptor
+jalview.schemabinding.version2.FeatureMatcherSet=jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor
 jalview.schemabinding.version2.SecondaryStructure=jalview.schemabinding.version2.descriptors.SecondaryStructureDescriptor
+jalview.schemabinding.version2.MatchCondition=jalview.schemabinding.version2.descriptors.MatchConditionDescriptor
 jalview.schemabinding.version2.SequenceSet=jalview.schemabinding.version2.descriptors.SequenceSetDescriptor
 jalview.schemabinding.version2.Viewport=jalview.schemabinding.version2.descriptors.ViewportDescriptor
 jalview.schemabinding.version2.RnaViewer=jalview.schemabinding.version2.descriptors.RnaViewerDescriptor
@@ -20,31 +22,32 @@ jalview.schemabinding.version2.UserColourScheme=jalview.schemabinding.version2.d
 jalview.schemabinding.version2.DBRef=jalview.schemabinding.version2.descriptors.DBRefDescriptor
 jalview.schemabinding.version2.AlcodMap=jalview.schemabinding.version2.descriptors.AlcodMapDescriptor
 jalview.schemabinding.version2.Annotation=jalview.schemabinding.version2.descriptors.AnnotationDescriptor
-jalview.schemabinding.version2.Wsparameters=jalview.schemabinding.version2.descriptors.WsparametersDescriptor
 jalview.schemabinding.version2.JSeq=jalview.schemabinding.version2.descriptors.JSeqDescriptor
+jalview.schemabinding.version2.MatcherSet=jalview.schemabinding.version2.descriptors.MatcherSetDescriptor
 jalview.schemabinding.version2.Sequence=jalview.schemabinding.version2.descriptors.SequenceDescriptor
 jalview.schemabinding.version2.WebServiceParameterSet=jalview.schemabinding.version2.descriptors.WebServiceParameterSetDescriptor
 jalview.schemabinding.version2.Alcodon=jalview.schemabinding.version2.descriptors.AlcodonDescriptor
+jalview.schemabinding.version2.Filter=jalview.schemabinding.version2.descriptors.FilterDescriptor
 jalview.schemabinding.version2.AnnotationColours=jalview.schemabinding.version2.descriptors.AnnotationColoursDescriptor
 jalview.schemabinding.version2.Pdbids=jalview.schemabinding.version2.descriptors.PdbidsDescriptor
 jalview.schemabinding.version2.AnnotationColourScheme=jalview.schemabinding.version2.descriptors.AnnotationColourSchemeDescriptor
 jalview.schemabinding.version2.Mapping=jalview.schemabinding.version2.descriptors.MappingDescriptor
-jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor
+jalview.schemabinding.version2.CompoundMatcher=jalview.schemabinding.version2.descriptors.CompoundMatcherDescriptor
+jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor
 jalview.schemabinding.version2.Group=jalview.schemabinding.version2.descriptors.GroupDescriptor
+jalview.schemabinding.version2.MappingChoice=jalview.schemabinding.version2.descriptors.MappingChoiceDescriptor
 jalview.schemabinding.version2.Feature=jalview.schemabinding.version2.descriptors.FeatureDescriptor
-jalview.schemabinding.version2.JalviewModelSequence=jalview.schemabinding.version2.descriptors.JalviewModelSequenceDescriptor
 jalview.schemabinding.version2.UserColours=jalview.schemabinding.version2.descriptors.UserColoursDescriptor
 jalview.schemabinding.version2.Colour=jalview.schemabinding.version2.descriptors.ColourDescriptor
-jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor
 jalview.schemabinding.version2.PdbentryItem=jalview.schemabinding.version2.descriptors.PdbentryItemDescriptor
-jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor
+jalview.schemabinding.version2.MapListFrom=jalview.schemabinding.version2.descriptors.MapListFromDescriptor
 jalview.schemabinding.version2.FeatureSettings=jalview.schemabinding.version2.descriptors.FeatureSettingsDescriptor
-jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor
-jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor
+jalview.schemabinding.version2.JGroup=jalview.schemabinding.version2.descriptors.JGroupDescriptor
 jalview.schemabinding.version2.MapListTo=jalview.schemabinding.version2.descriptors.MapListToDescriptor
+jalview.schemabinding.version2.JalviewUserColours=jalview.schemabinding.version2.descriptors.JalviewUserColoursDescriptor
+jalview.schemabinding.version2.VamsasModel=jalview.schemabinding.version2.descriptors.VamsasModelDescriptor
 jalview.schemabinding.version2.Pdbentry=jalview.schemabinding.version2.descriptors.PdbentryDescriptor
 jalview.schemabinding.version2.HiddenColumns=jalview.schemabinding.version2.descriptors.HiddenColumnsDescriptor
 jalview.schemabinding.version2.Features=jalview.schemabinding.version2.descriptors.FeaturesDescriptor
-jalview.schemabinding.version2.DseqFor=jalview.schemabinding.version2.descriptors.DseqForDescriptor
 jalview.schemabinding.version2.VAMSAS=jalview.schemabinding.version2.descriptors.VAMSASDescriptor
-jalview.schemabinding.version2.MappingChoiceItem=jalview.schemabinding.version2.descriptors.MappingChoiceItemDescriptor
+jalview.schemabinding.version2.FeatureMatcher=jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor
index 9d5a916..d1c7297 100644 (file)
@@ -27,7 +27,8 @@ public class Colour implements java.io.Serializable
   // --------------------------/
 
   /**
-   * Field _name.
+   * Single letter residue code for an alignment colour scheme, or feature type
+   * for a feature colour scheme
    */
   private java.lang.String _name;
 
@@ -42,9 +43,15 @@ public class Colour implements java.io.Serializable
   private java.lang.String _minRGB;
 
   /**
-   * loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Field _noValueColour.
    */
-  private java.lang.String _threshType;
+  private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour
+          .valueOf("Min");
+
+  /**
+   * Field _threshType.
+   */
+  private jalview.schemabinding.version2.types.ColourThreshTypeType _threshType;
 
   /**
    * Field _threshold.
@@ -96,6 +103,11 @@ public class Colour implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -103,6 +115,9 @@ public class Colour implements java.io.Serializable
   public Colour()
   {
     super();
+    setNoValueColour(jalview.schemabinding.version2.types.NoValueColour
+            .valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -110,41 +125,140 @@ public class Colour implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -195,7 +309,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'name'.
+   * Returns the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @return the value of field 'Name'.
    */
@@ -205,6 +321,16 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.schemabinding.version2.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'RGB'.
    * 
    * @return the value of field 'RGB'.
@@ -215,12 +341,11 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Returns the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Returns the value of field 'threshType'.
    * 
    * @return the value of field 'ThreshType'.
    */
-  public java.lang.String getThreshType()
+  public jalview.schemabinding.version2.types.ColourThreshTypeType getThreshType()
   {
     return this._threshType;
   }
@@ -360,6 +485,76 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -419,7 +614,9 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'name'.
+   * Sets the value of field 'name'. The field 'name' has the following
+   * description: Single letter residue code for an alignment colour scheme, or
+   * feature type for a feature colour scheme
    * 
    * @param name
    *          the value of field 'name'.
@@ -430,6 +627,18 @@ public class Colour implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.schemabinding.version2.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'RGB'.
    * 
    * @param RGB
@@ -441,13 +650,13 @@ public class Colour implements java.io.Serializable
   }
 
   /**
-   * Sets the value of field 'threshType'. The field 'threshType' has the
-   * following description: loosely specified enumeration: NONE,ABOVE, or BELOW
+   * Sets the value of field 'threshType'.
    * 
    * @param threshType
    *          the value of field 'threshType'.
    */
-  public void setThreshType(final java.lang.String threshType)
+  public void setThreshType(
+          final jalview.schemabinding.version2.types.ColourThreshTypeType threshType)
   {
     this._threshType = threshType;
   }
@@ -480,8 +689,8 @@ public class Colour implements java.io.Serializable
           throws org.exolab.castor.xml.MarshalException,
           org.exolab.castor.xml.ValidationException
   {
-    return (jalview.schemabinding.version2.Colour) Unmarshaller.unmarshal(
-            jalview.schemabinding.version2.Colour.class, reader);
+    return (jalview.schemabinding.version2.Colour) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.Colour.class, reader);
   }
 
   /**
diff --git a/src/jalview/schemabinding/version2/CompoundMatcher.java b/src/jalview/schemabinding/version2/CompoundMatcher.java
new file mode 100644 (file)
index 0000000..27714e2
--- /dev/null
@@ -0,0 +1,374 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class CompoundMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * If true, matchers are AND-ed, if false they are OR-ed
+   */
+  private boolean _and;
+
+  /**
+   * keeps track of state for field: _and
+   */
+  private boolean _has_and;
+
+  /**
+   * Field _matcherSetList.
+   */
+  private java.util.Vector _matcherSetList;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcher()
+  {
+    super();
+    this._matcherSetList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.addElement(vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addMatcherSet(final int index,
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._matcherSetList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addMatcherSet has a maximum of 2");
+    }
+
+    this._matcherSetList.add(index, vMatcherSet);
+  }
+
+  /**
+   */
+  public void deleteAnd()
+  {
+    this._has_and = false;
+  }
+
+  /**
+   * Method enumerateMatcherSet.
+   * 
+   * @return an Enumeration over all jalview.schemabinding.version2.MatcherSet
+   *         elements
+   */
+  public java.util.Enumeration enumerateMatcherSet()
+  {
+    return this._matcherSetList.elements();
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean getAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method getMatcherSet.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the jalview.schemabinding.version2.MatcherSet at the
+   *         given index
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet(
+          final int index) throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    return (jalview.schemabinding.version2.MatcherSet) _matcherSetList
+            .get(index);
+  }
+
+  /**
+   * Method getMatcherSet.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public jalview.schemabinding.version2.MatcherSet[] getMatcherSet()
+  {
+    jalview.schemabinding.version2.MatcherSet[] array = new jalview.schemabinding.version2.MatcherSet[0];
+    return (jalview.schemabinding.version2.MatcherSet[]) this._matcherSetList
+            .toArray(array);
+  }
+
+  /**
+   * Method getMatcherSetCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getMatcherSetCount()
+  {
+    return this._matcherSetList.size();
+  }
+
+  /**
+   * Method hasAnd.
+   * 
+   * @return true if at least one And has been added
+   */
+  public boolean hasAnd()
+  {
+    return this._has_and;
+  }
+
+  /**
+   * Returns the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @return the value of field 'And'.
+   */
+  public boolean isAnd()
+  {
+    return this._and;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllMatcherSet()
+  {
+    this._matcherSetList.clear();
+  }
+
+  /**
+   * Method removeMatcherSet.
+   * 
+   * @param vMatcherSet
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+  {
+    boolean removed = _matcherSetList.remove(vMatcherSet);
+    return removed;
+  }
+
+  /**
+   * Method removeMatcherSetAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public jalview.schemabinding.version2.MatcherSet removeMatcherSetAt(
+          final int index)
+  {
+    java.lang.Object obj = this._matcherSetList.remove(index);
+    return (jalview.schemabinding.version2.MatcherSet) obj;
+  }
+
+  /**
+   * Sets the value of field 'and'. The field 'and' has the following
+   * description: If true, matchers are AND-ed, if false they are OR-ed
+   * 
+   * @param and
+   *          the value of field 'and'.
+   */
+  public void setAnd(final boolean and)
+  {
+    this._and = and;
+    this._has_and = true;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vMatcherSet
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setMatcherSet(final int index,
+          final jalview.schemabinding.version2.MatcherSet vMatcherSet)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._matcherSetList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setMatcherSet: Index value '" + index + "' not in range [0.."
+                      + (this._matcherSetList.size() - 1) + "]");
+    }
+
+    this._matcherSetList.set(index, vMatcherSet);
+  }
+
+  /**
+   * 
+   * 
+   * @param vMatcherSetArray
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet[] vMatcherSetArray)
+  {
+    // -- copy array
+    _matcherSetList.clear();
+
+    for (int i = 0; i < vMatcherSetArray.length; i++)
+    {
+      this._matcherSetList.add(vMatcherSetArray[i]);
+    }
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.CompoundMatcher
+   */
+  public static jalview.schemabinding.version2.CompoundMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.CompoundMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.CompoundMatcher.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/FeatureMatcher.java b/src/jalview/schemabinding/version2/FeatureMatcher.java
new file mode 100644 (file)
index 0000000..4d29cab
--- /dev/null
@@ -0,0 +1,383 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class FeatureMatcher.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcher implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _by.
+   */
+  private jalview.schemabinding.version2.types.FeatureMatcherByType _by;
+
+  /**
+   * name of feature attribute to filter on, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * Field _condition.
+   */
+  private java.lang.String _condition;
+
+  /**
+   * Field _value.
+   */
+  private java.lang.String _value;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcher()
+  {
+    super();
+    this._attributeNameList = new java.util.Vector();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
+   * Returns the value of field 'by'.
+   * 
+   * @return the value of field 'By'.
+   */
+  public jalview.schemabinding.version2.types.FeatureMatcherByType getBy()
+  {
+    return this._by;
+  }
+
+  /**
+   * Returns the value of field 'condition'.
+   * 
+   * @return the value of field 'Condition'.
+   */
+  public java.lang.String getCondition()
+  {
+    return this._condition;
+  }
+
+  /**
+   * Returns the value of field 'value'.
+   * 
+   * @return the value of field 'Value'.
+   */
+  public java.lang.String getValue()
+  {
+    return this._value;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
+   * Sets the value of field 'by'.
+   * 
+   * @param by
+   *          the value of field 'by'.
+   */
+  public void setBy(
+          final jalview.schemabinding.version2.types.FeatureMatcherByType by)
+  {
+    this._by = by;
+  }
+
+  /**
+   * Sets the value of field 'condition'.
+   * 
+   * @param condition
+   *          the value of field 'condition'.
+   */
+  public void setCondition(final java.lang.String condition)
+  {
+    this._condition = condition;
+  }
+
+  /**
+   * Sets the value of field 'value'.
+   * 
+   * @param value
+   *          the value of field 'value'.
+   */
+  public void setValue(final java.lang.String value)
+  {
+    this._value = value;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher
+   */
+  public static jalview.schemabinding.version2.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.FeatureMatcher.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/FeatureMatcherSet.java b/src/jalview/schemabinding/version2/FeatureMatcherSet.java
new file mode 100644 (file)
index 0000000..2d79a98
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * A feature match condition, which may be simple or compound
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSet implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Internal choice value storage
+   */
+  private java.lang.Object _choiceValue;
+
+  /**
+   * Field _matchCondition.
+   */
+  private MatchCondition _matchCondition;
+
+  /**
+   * Field _compoundMatcher.
+   */
+  private CompoundMatcher _compoundMatcher;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'choiceValue'. The field 'choiceValue' has the
+   * following description: Internal choice value storage
+   * 
+   * @return the value of field 'ChoiceValue'.
+   */
+  public java.lang.Object getChoiceValue()
+  {
+    return this._choiceValue;
+  }
+
+  /**
+   * Returns the value of field 'compoundMatcher'.
+   * 
+   * @return the value of field 'CompoundMatcher'.
+   */
+  public CompoundMatcher getCompoundMatcher()
+  {
+    return this._compoundMatcher;
+  }
+
+  /**
+   * Returns the value of field 'matchCondition'.
+   * 
+   * @return the value of field 'MatchCondition'.
+   */
+  public MatchCondition getMatchCondition()
+  {
+    return this._matchCondition;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'compoundMatcher'.
+   * 
+   * @param compoundMatcher
+   *          the value of field 'compoundMatcher'.
+   */
+  public void setCompoundMatcher(final CompoundMatcher compoundMatcher)
+  {
+    this._compoundMatcher = compoundMatcher;
+    this._choiceValue = compoundMatcher;
+  }
+
+  /**
+   * Sets the value of field 'matchCondition'.
+   * 
+   * @param matchCondition
+   *          the value of field 'matchCondition'.
+   */
+  public void setMatchCondition(final MatchCondition matchCondition)
+  {
+    this._matchCondition = matchCondition;
+    this._choiceValue = matchCondition;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet
+   */
+  public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller
+            .unmarshal(
+                    jalview.schemabinding.version2.FeatureMatcherSet.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/Filter.java b/src/jalview/schemabinding/version2/Filter.java
new file mode 100644 (file)
index 0000000..45323a7
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class Filter.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class Filter implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _featureType.
+   */
+  private java.lang.String _featureType;
+
+  /**
+   * Field _matcherSet.
+   */
+  private jalview.schemabinding.version2.MatcherSet _matcherSet;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public Filter()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Returns the value of field 'featureType'.
+   * 
+   * @return the value of field 'FeatureType'.
+   */
+  public java.lang.String getFeatureType()
+  {
+    return this._featureType;
+  }
+
+  /**
+   * Returns the value of field 'matcherSet'.
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Sets the value of field 'featureType'.
+   * 
+   * @param featureType
+   *          the value of field 'featureType'.
+   */
+  public void setFeatureType(final java.lang.String featureType)
+  {
+    this._featureType = featureType;
+  }
+
+  /**
+   * Sets the value of field 'matcherSet'.
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.Filter
+   */
+  public static jalview.schemabinding.version2.Filter unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.Filter) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.Filter.class, reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index 042f092..c8d52ac 100644 (file)
@@ -42,6 +42,11 @@ public class JalviewUserColours implements java.io.Serializable
    */
   private java.util.Vector _colourList;
 
+  /**
+   * Field _filterList.
+   */
+  private java.util.Vector _filterList;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -50,6 +55,7 @@ public class JalviewUserColours implements java.io.Serializable
   {
     super();
     this._colourList = new java.util.Vector();
+    this._filterList = new java.util.Vector();
   }
 
   // -----------/
@@ -84,6 +90,33 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.addElement(vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    this._filterList.add(index, vFilter);
+  }
+
+  /**
    * Method enumerateColour.
    * 
    * @return an Enumeration over all Colour elements
@@ -94,6 +127,16 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method enumerateFilter.
+   * 
+   * @return an Enumeration over all Filter elements
+   */
+  public java.util.Enumeration enumerateFilter()
+  {
+    return this._filterList.elements();
+  }
+
+  /**
    * Method getColour.
    * 
    * @param index
@@ -107,9 +150,9 @@ public class JalviewUserColours implements java.io.Serializable
     // check bounds for index
     if (index < 0 || index >= this._colourList.size())
     {
-      throw new IndexOutOfBoundsException("getColour: Index value '"
-              + index + "' not in range [0.."
-              + (this._colourList.size() - 1) + "]");
+      throw new IndexOutOfBoundsException(
+              "getColour: Index value '" + index + "' not in range [0.."
+                      + (this._colourList.size() - 1) + "]");
     }
 
     return (Colour) _colourList.get(index);
@@ -141,6 +184,53 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method getFilter.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the Filter at the given index
+   */
+  public Filter getFilter(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "getFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    return (Filter) _filterList.get(index);
+  }
+
+  /**
+   * Method getFilter.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public Filter[] getFilter()
+  {
+    Filter[] array = new Filter[0];
+    return (Filter[]) this._filterList.toArray(array);
+  }
+
+  /**
+   * Method getFilterCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getFilterCount()
+  {
+    return this._filterList.size();
+  }
+
+  /**
    * Returns the value of field 'schemeName'.
    * 
    * @return the value of field 'SchemeName'.
@@ -217,13 +307,20 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
-     */
+   */
   public void removeAllColour()
   {
     this._colourList.clear();
   }
 
   /**
+   */
+  public void removeAllFilter()
+  {
+    this._filterList.clear();
+  }
+
+  /**
    * Method removeColour.
    * 
    * @param vColour
@@ -248,6 +345,30 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * Method removeFilter.
+   * 
+   * @param vFilter
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeFilter(final Filter vFilter)
+  {
+    boolean removed = _filterList.remove(vFilter);
+    return removed;
+  }
+
+  /**
+   * Method removeFilterAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public Filter removeFilterAt(final int index)
+  {
+    java.lang.Object obj = this._filterList.remove(index);
+    return (Filter) obj;
+  }
+
+  /**
    * 
    * 
    * @param index
@@ -261,9 +382,9 @@ public class JalviewUserColours implements java.io.Serializable
     // check bounds for index
     if (index < 0 || index >= this._colourList.size())
     {
-      throw new IndexOutOfBoundsException("setColour: Index value '"
-              + index + "' not in range [0.."
-              + (this._colourList.size() - 1) + "]");
+      throw new IndexOutOfBoundsException(
+              "setColour: Index value '" + index + "' not in range [0.."
+                      + (this._colourList.size() - 1) + "]");
     }
 
     this._colourList.set(index, vColour);
@@ -286,6 +407,44 @@ public class JalviewUserColours implements java.io.Serializable
   }
 
   /**
+   * 
+   * 
+   * @param index
+   * @param vFilter
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setFilter(final int index, final Filter vFilter)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._filterList.size())
+    {
+      throw new IndexOutOfBoundsException(
+              "setFilter: Index value '" + index + "' not in range [0.."
+                      + (this._filterList.size() - 1) + "]");
+    }
+
+    this._filterList.set(index, vFilter);
+  }
+
+  /**
+   * 
+   * 
+   * @param vFilterArray
+   */
+  public void setFilter(final Filter[] vFilterArray)
+  {
+    // -- copy array
+    _filterList.clear();
+
+    for (int i = 0; i < vFilterArray.length; i++)
+    {
+      this._filterList.add(vFilterArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'schemeName'.
    * 
    * @param schemeName
diff --git a/src/jalview/schemabinding/version2/MatchCondition.java b/src/jalview/schemabinding/version2/MatchCondition.java
new file mode 100644 (file)
index 0000000..af2f3f5
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * Class MatchCondition.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchCondition extends FeatureMatcher
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchCondition()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcher
+   */
+  public static jalview.schemabinding.version2.FeatureMatcher unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcher) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.MatchCondition.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/MatcherSet.java b/src/jalview/schemabinding/version2/MatcherSet.java
new file mode 100644 (file)
index 0000000..6fde9e4
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import org.exolab.castor.xml.Marshaller;
+import org.exolab.castor.xml.Unmarshaller;
+
+/**
+ * optional filter(s) applied to the feature type
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSet extends FeatureMatcherSet
+        implements java.io.Serializable
+{
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSet()
+  {
+    super();
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method isValid.
+   * 
+   * @return true if this object is valid according to the schema
+   */
+  public boolean isValid()
+  {
+    try
+    {
+      validate();
+    } catch (org.exolab.castor.xml.ValidationException vex)
+    {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 
+   * 
+   * @param out
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void marshal(final java.io.Writer out)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, out);
+  }
+
+  /**
+   * 
+   * 
+   * @param handler
+   * @throws java.io.IOException
+   *           if an IOException occurs during marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   */
+  public void marshal(final org.xml.sax.ContentHandler handler)
+          throws java.io.IOException,
+          org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    Marshaller.marshal(this, handler);
+  }
+
+  /**
+   * Method unmarshal.
+   * 
+   * @param reader
+   * @throws org.exolab.castor.xml.MarshalException
+   *           if object is null or if any SAXException is thrown during
+   *           marshaling
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   * @return the unmarshaled jalview.schemabinding.version2.FeatureMatcherSet
+   */
+  public static jalview.schemabinding.version2.FeatureMatcherSet unmarshal(
+          final java.io.Reader reader)
+          throws org.exolab.castor.xml.MarshalException,
+          org.exolab.castor.xml.ValidationException
+  {
+    return (jalview.schemabinding.version2.FeatureMatcherSet) Unmarshaller
+            .unmarshal(jalview.schemabinding.version2.MatcherSet.class,
+                    reader);
+  }
+
+  /**
+   * 
+   * 
+   * @throws org.exolab.castor.xml.ValidationException
+   *           if this object is an invalid instance according to the schema
+   */
+  public void validate() throws org.exolab.castor.xml.ValidationException
+  {
+    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+    validator.validate(this);
+  }
+
+}
index fb6b276..31797fe 100644 (file)
@@ -7,8 +7,8 @@
 
 package jalview.schemabinding.version2;
 
-//---------------------------------/
-//- Imported classes and packages -/
+  //---------------------------------/
+ //- Imported classes and packages -/
 //---------------------------------/
 
 import org.exolab.castor.xml.Marshaller;
@@ -19,163 +19,181 @@ import org.exolab.castor.xml.Unmarshaller;
  * 
  * @version $Revision$ $Date$
  */
-public class OtherData implements java.io.Serializable
-{
-
-  // --------------------------/
-  // - Class/Member Variables -/
-  // --------------------------/
-
-  /**
-   * Field _key.
-   */
-  private java.lang.String _key;
-
-  /**
-   * Field _value.
-   */
-  private java.lang.String _value;
-
-  // ----------------/
-  // - Constructors -/
-  // ----------------/
-
-  public OtherData()
-  {
-    super();
-  }
-
-  // -----------/
-  // - Methods -/
-  // -----------/
-
-  /**
-   * Returns the value of field 'key'.
-   * 
-   * @return the value of field 'Key'.
-   */
-  public java.lang.String getKey()
-  {
-    return this._key;
-  }
-
-  /**
-   * Returns the value of field 'value'.
-   * 
-   * @return the value of field 'Value'.
-   */
-  public java.lang.String getValue()
-  {
-    return this._value;
-  }
-
-  /**
-   * Method isValid.
-   * 
-   * @return true if this object is valid according to the schema
-   */
-  public boolean isValid()
-  {
-    try
-    {
-      validate();
-    } catch (org.exolab.castor.xml.ValidationException vex)
-    {
-      return false;
+public class OtherData implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _key.
+     */
+    private java.lang.String _key;
+
+    /**
+     * key2 may be used for a sub-attribute of key
+     */
+    private java.lang.String _key2;
+
+    /**
+     * Field _value.
+     */
+    private java.lang.String _value;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public OtherData() {
+        super();
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Returns the value of field 'key'.
+     * 
+     * @return the value of field 'Key'.
+     */
+    public java.lang.String getKey(
+    ) {
+        return this._key;
+    }
+
+    /**
+     * Returns the value of field 'key2'. The field 'key2' has the
+     * following description: key2 may be used for a sub-attribute
+     * of key
+     * 
+     * @return the value of field 'Key2'.
+     */
+    public java.lang.String getKey2(
+    ) {
+        return this._key2;
+    }
+
+    /**
+     * Returns the value of field 'value'.
+     * 
+     * @return the value of field 'Value'.
+     */
+    public java.lang.String getValue(
+    ) {
+        return this._value;
+    }
+
+    /**
+     * Method isValid.
+     * 
+     * @return true if this object is valid according to the schema
+     */
+    public boolean isValid(
+    ) {
+        try {
+            validate();
+        } catch (org.exolab.castor.xml.ValidationException vex) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 
+     * 
+     * @param out
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     */
+    public void marshal(
+            final java.io.Writer out)
+    throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        Marshaller.marshal(this, out);
+    }
+
+    /**
+     * 
+     * 
+     * @param handler
+     * @throws java.io.IOException if an IOException occurs during
+     * marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     */
+    public void marshal(
+            final org.xml.sax.ContentHandler handler)
+    throws java.io.IOException, org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        Marshaller.marshal(this, handler);
+    }
+
+    /**
+     * Sets the value of field 'key'.
+     * 
+     * @param key the value of field 'key'.
+     */
+    public void setKey(
+            final java.lang.String key) {
+        this._key = key;
+    }
+
+    /**
+     * Sets the value of field 'key2'. The field 'key2' has the
+     * following description: key2 may be used for a sub-attribute
+     * of key
+     * 
+     * @param key2 the value of field 'key2'.
+     */
+    public void setKey2(
+            final java.lang.String key2) {
+        this._key2 = key2;
+    }
+
+    /**
+     * Sets the value of field 'value'.
+     * 
+     * @param value the value of field 'value'.
+     */
+    public void setValue(
+            final java.lang.String value) {
+        this._value = value;
+    }
+
+    /**
+     * Method unmarshal.
+     * 
+     * @param reader
+     * @throws org.exolab.castor.xml.MarshalException if object is
+     * null or if any SAXException is thrown during marshaling
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     * @return the unmarshaled
+     * jalview.schemabinding.version2.OtherData
+     */
+    public static jalview.schemabinding.version2.OtherData unmarshal(
+            final java.io.Reader reader)
+    throws org.exolab.castor.xml.MarshalException, org.exolab.castor.xml.ValidationException {
+        return (jalview.schemabinding.version2.OtherData) Unmarshaller.unmarshal(jalview.schemabinding.version2.OtherData.class, reader);
+    }
+
+    /**
+     * 
+     * 
+     * @throws org.exolab.castor.xml.ValidationException if this
+     * object is an invalid instance according to the schema
+     */
+    public void validate(
+    )
+    throws org.exolab.castor.xml.ValidationException {
+        org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
+        validator.validate(this);
     }
-    return true;
-  }
-
-  /**
-   * 
-   * 
-   * @param out
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   */
-  public void marshal(final java.io.Writer out)
-          throws org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    Marshaller.marshal(this, out);
-  }
-
-  /**
-   * 
-   * 
-   * @param handler
-   * @throws java.io.IOException
-   *           if an IOException occurs during marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   */
-  public void marshal(final org.xml.sax.ContentHandler handler)
-          throws java.io.IOException,
-          org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    Marshaller.marshal(this, handler);
-  }
-
-  /**
-   * Sets the value of field 'key'.
-   * 
-   * @param key
-   *          the value of field 'key'.
-   */
-  public void setKey(final java.lang.String key)
-  {
-    this._key = key;
-  }
-
-  /**
-   * Sets the value of field 'value'.
-   * 
-   * @param value
-   *          the value of field 'value'.
-   */
-  public void setValue(final java.lang.String value)
-  {
-    this._value = value;
-  }
-
-  /**
-   * Method unmarshal.
-   * 
-   * @param reader
-   * @throws org.exolab.castor.xml.MarshalException
-   *           if object is null or if any SAXException is thrown during
-   *           marshaling
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   * @return the unmarshaled jalview.schemabinding.version2.OtherData
-   */
-  public static jalview.schemabinding.version2.OtherData unmarshal(
-          final java.io.Reader reader)
-          throws org.exolab.castor.xml.MarshalException,
-          org.exolab.castor.xml.ValidationException
-  {
-    return (jalview.schemabinding.version2.OtherData) Unmarshaller
-            .unmarshal(jalview.schemabinding.version2.OtherData.class,
-                    reader);
-  }
-
-  /**
-   * 
-   * 
-   * @throws org.exolab.castor.xml.ValidationException
-   *           if this object is an invalid instance according to the schema
-   */
-  public void validate() throws org.exolab.castor.xml.ValidationException
-  {
-    org.exolab.castor.xml.Validator validator = new org.exolab.castor.xml.Validator();
-    validator.validate(this);
-  }
 
 }
index c458971..59e9522 100644 (file)
@@ -73,6 +73,12 @@ public class Setting implements java.io.Serializable
   private boolean _has_mincolour;
 
   /**
+   * Field _noValueColour.
+   */
+  private jalview.schemabinding.version2.types.NoValueColour _noValueColour = jalview.schemabinding.version2.types.NoValueColour
+          .valueOf("Min");
+
+  /**
    * threshold value for graduated feature colour
    * 
    */
@@ -134,6 +140,16 @@ public class Setting implements java.io.Serializable
    */
   private boolean _has_autoScale;
 
+  /**
+   * name of feature attribute to colour by, or attribute and sub-attribute
+   */
+  private java.util.Vector _attributeNameList;
+
+  /**
+   * optional filter(s) applied to the feature type
+   */
+  private jalview.schemabinding.version2.MatcherSet _matcherSet;
+
   // ----------------/
   // - Constructors -/
   // ----------------/
@@ -141,6 +157,9 @@ public class Setting implements java.io.Serializable
   public Setting()
   {
     super();
+    setNoValueColour(jalview.schemabinding.version2.types.NoValueColour
+            .valueOf("Min"));
+    this._attributeNameList = new java.util.Vector();
   }
 
   // -----------/
@@ -148,76 +167,175 @@ public class Setting implements java.io.Serializable
   // -----------/
 
   /**
-     */
+   * 
+   * 
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.addElement(vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void addAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check for the maximum size
+    if (this._attributeNameList.size() >= 2)
+    {
+      throw new IndexOutOfBoundsException(
+              "addAttributeName has a maximum of 2");
+    }
+
+    this._attributeNameList.add(index, vAttributeName);
+  }
+
+  /**
+   */
   public void deleteAutoScale()
   {
     this._has_autoScale = false;
   }
 
   /**
-     */
+   */
   public void deleteColour()
   {
     this._has_colour = false;
   }
 
   /**
-     */
+   */
   public void deleteColourByLabel()
   {
     this._has_colourByLabel = false;
   }
 
   /**
-     */
+   */
   public void deleteDisplay()
   {
     this._has_display = false;
   }
 
   /**
-     */
+   */
   public void deleteMax()
   {
     this._has_max = false;
   }
 
   /**
-     */
+   */
   public void deleteMin()
   {
     this._has_min = false;
   }
 
   /**
-     */
+   */
   public void deleteMincolour()
   {
     this._has_mincolour = false;
   }
 
   /**
-     */
+   */
   public void deleteOrder()
   {
     this._has_order = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshold()
   {
     this._has_threshold = false;
   }
 
   /**
-     */
+   */
   public void deleteThreshstate()
   {
     this._has_threshstate = false;
   }
 
   /**
+   * Method enumerateAttributeName.
+   * 
+   * @return an Enumeration over all java.lang.String elements
+   */
+  public java.util.Enumeration enumerateAttributeName()
+  {
+    return this._attributeNameList.elements();
+  }
+
+  /**
+   * Method getAttributeName.
+   * 
+   * @param index
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   * @return the value of the java.lang.String at the given index
+   */
+  public java.lang.String getAttributeName(final int index)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("getAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    return (java.lang.String) _attributeNameList.get(index);
+  }
+
+  /**
+   * Method getAttributeName.Returns the contents of the collection in an Array.
+   * <p>
+   * Note: Just in case the collection contents are changing in another thread,
+   * we pass a 0-length Array of the correct type into the API call. This way we
+   * <i>know</i> that the Array returned is of exactly the correct length.
+   * 
+   * @return this collection as an Array
+   */
+  public java.lang.String[] getAttributeName()
+  {
+    java.lang.String[] array = new java.lang.String[0];
+    return (java.lang.String[]) this._attributeNameList.toArray(array);
+  }
+
+  /**
+   * Method getAttributeNameCount.
+   * 
+   * @return the size of this collection
+   */
+  public int getAttributeNameCount()
+  {
+    return this._attributeNameList.size();
+  }
+
+  /**
    * Returns the value of field 'autoScale'.
    * 
    * @return the value of field 'AutoScale'.
@@ -258,6 +376,17 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'matcherSet'. The field 'matcherSet' has the
+   * following description: optional filter(s) applied to the feature type
+   * 
+   * @return the value of field 'MatcherSet'.
+   */
+  public jalview.schemabinding.version2.MatcherSet getMatcherSet()
+  {
+    return this._matcherSet;
+  }
+
+  /**
    * Returns the value of field 'max'.
    * 
    * @return the value of field 'Max'.
@@ -290,6 +419,16 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Returns the value of field 'noValueColour'.
+   * 
+   * @return the value of field 'NoValueColour'.
+   */
+  public jalview.schemabinding.version2.types.NoValueColour getNoValueColour()
+  {
+    return this._noValueColour;
+  }
+
+  /**
    * Returns the value of field 'order'.
    * 
    * @return the value of field 'Order'.
@@ -518,6 +657,76 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   */
+  public void removeAllAttributeName()
+  {
+    this._attributeNameList.clear();
+  }
+
+  /**
+   * Method removeAttributeName.
+   * 
+   * @param vAttributeName
+   * @return true if the object was removed from the collection.
+   */
+  public boolean removeAttributeName(final java.lang.String vAttributeName)
+  {
+    boolean removed = _attributeNameList.remove(vAttributeName);
+    return removed;
+  }
+
+  /**
+   * Method removeAttributeNameAt.
+   * 
+   * @param index
+   * @return the element removed from the collection
+   */
+  public java.lang.String removeAttributeNameAt(final int index)
+  {
+    java.lang.Object obj = this._attributeNameList.remove(index);
+    return (java.lang.String) obj;
+  }
+
+  /**
+   * 
+   * 
+   * @param index
+   * @param vAttributeName
+   * @throws java.lang.IndexOutOfBoundsException
+   *           if the index given is outside the bounds of the collection
+   */
+  public void setAttributeName(final int index,
+          final java.lang.String vAttributeName)
+          throws java.lang.IndexOutOfBoundsException
+  {
+    // check bounds for index
+    if (index < 0 || index >= this._attributeNameList.size())
+    {
+      throw new IndexOutOfBoundsException("setAttributeName: Index value '"
+              + index + "' not in range [0.."
+              + (this._attributeNameList.size() - 1) + "]");
+    }
+
+    this._attributeNameList.set(index, vAttributeName);
+  }
+
+  /**
+   * 
+   * 
+   * @param vAttributeNameArray
+   */
+  public void setAttributeName(final java.lang.String[] vAttributeNameArray)
+  {
+    // -- copy array
+    _attributeNameList.clear();
+
+    for (int i = 0; i < vAttributeNameArray.length; i++)
+    {
+      this._attributeNameList.add(vAttributeNameArray[i]);
+    }
+  }
+
+  /**
    * Sets the value of field 'autoScale'.
    * 
    * @param autoScale
@@ -566,6 +775,19 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'matcherSet'. The field 'matcherSet' has the
+   * following description: optional filter(s) applied to the feature type
+   * 
+   * @param matcherSet
+   *          the value of field 'matcherSet'.
+   */
+  public void setMatcherSet(
+          final jalview.schemabinding.version2.MatcherSet matcherSet)
+  {
+    this._matcherSet = matcherSet;
+  }
+
+  /**
    * Sets the value of field 'max'.
    * 
    * @param max
@@ -604,6 +826,18 @@ public class Setting implements java.io.Serializable
   }
 
   /**
+   * Sets the value of field 'noValueColour'.
+   * 
+   * @param noValueColour
+   *          the value of field 'noValueColour'.
+   */
+  public void setNoValueColour(
+          final jalview.schemabinding.version2.types.NoValueColour noValueColour)
+  {
+    this._noValueColour = noValueColour;
+  }
+
+  /**
    * Sets the value of field 'order'.
    * 
    * @param order
index 8b1ae9e..cca4ef1 100644 (file)
@@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Colour;
  * 
  * @version $Revision$ $Date$
  */
-public class ColourDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class ColourDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -55,6 +55,9 @@ public class ColourDescriptor extends
     super();
     _xmlName = "colour";
     _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
     org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
     org.exolab.castor.mapping.FieldHandler handler = null;
     org.exolab.castor.xml.FieldValidator fieldValidator = null;
@@ -197,11 +200,57 @@ public class ColourDescriptor extends
       typeValidator.setWhiteSpace("preserve");
     }
     desc.setValidator(fieldValidator);
-    // -- _threshType
+    // -- _noValueColour
     desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_threshType", "threshType",
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            "_noValueColour", "noValueColour",
             org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Colour target = (Colour) object;
+        return target.getNoValueColour();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.setNoValueColour(
+                  (jalview.schemabinding.version2.types.NoValueColour) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            handler);
     desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _noValueColour
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- _threshType
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.ColourThreshTypeType.class,
+            "_threshType", "threshType",
+            org.exolab.castor.xml.NodeType.Attribute);
     handler = new org.exolab.castor.xml.XMLFieldHandler()
     {
       public java.lang.Object getValue(java.lang.Object object)
@@ -217,7 +266,8 @@ public class ColourDescriptor extends
         try
         {
           Colour target = (Colour) object;
-          target.setThreshType((java.lang.String) value);
+          target.setThreshType(
+                  (jalview.schemabinding.version2.types.ColourThreshTypeType) value);
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -229,6 +279,10 @@ public class ColourDescriptor extends
         return null;
       }
     };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.ColourThreshTypeType.class,
+            handler);
+    desc.setImmutable(true);
     desc.setHandler(handler);
     desc.setMultivalued(false);
     addFieldDescriptor(desc);
@@ -236,10 +290,6 @@ public class ColourDescriptor extends
     // -- validation code for: _threshType
     fieldValidator = new org.exolab.castor.xml.FieldValidator();
     { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
     }
     desc.setValidator(fieldValidator);
     // -- _threshold
@@ -437,8 +487,8 @@ public class ColourDescriptor extends
             target.deleteColourByLabel();
             return;
           }
-          target.setColourByLabel(((java.lang.Boolean) value)
-                  .booleanValue());
+          target.setColourByLabel(
+                  ((java.lang.Boolean) value).booleanValue());
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -518,6 +568,66 @@ public class ColourDescriptor extends
     desc.setValidator(fieldValidator);
     // -- initialize element descriptors
 
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Colour target = (Colour) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Colour target = (Colour) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/CompoundMatcherDescriptor.java
new file mode 100644 (file)
index 0000000..2402d68
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.CompoundMatcher;
+
+/**
+ * Class CompoundMatcherDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class CompoundMatcherDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public CompoundMatcherDescriptor()
+  {
+    super();
+    _xmlName = "compoundMatcher";
+    _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _and
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.Boolean.TYPE, "_and", "and",
+            org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        CompoundMatcher target = (CompoundMatcher) object;
+        if (!target.hasAnd())
+        {
+          return null;
+        }
+        return (target.getAnd() ? java.lang.Boolean.TRUE
+                : java.lang.Boolean.FALSE);
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          // ignore null values for non optional primitives
+          if (value == null)
+          {
+            return;
+          }
+
+          target.setAnd(((java.lang.Boolean) value).booleanValue());
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _and
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.BooleanValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.BooleanValidator();
+      fieldValidator.setValidator(typeValidator);
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _matcherSetList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class,
+            "_matcherSetList", "matcherSet",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        CompoundMatcher target = (CompoundMatcher) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          target.addMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          CompoundMatcher target = (CompoundMatcher) object;
+          target.removeAllMatcherSet();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSetList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(2);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.CompoundMatcher.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherDescriptor.java
new file mode 100644 (file)
index 0000000..2df2f5b
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.FeatureMatcher;
+
+/**
+ * Class FeatureMatcherDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "FeatureMatcher";
+    _elementDefinition = false;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _by
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.FeatureMatcherByType.class,
+            "_by", "by", org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getBy();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setBy(
+                  (jalview.schemabinding.version2.types.FeatureMatcherByType) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.FeatureMatcherByType.class,
+            handler);
+    desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _by
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _condition
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_condition", "condition",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getCondition();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setCondition((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _condition
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _value
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_value", "value",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcher target = (FeatureMatcher) object;
+        return target.getValue();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcher target = (FeatureMatcher) object;
+          target.setValue((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _value
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.FeatureMatcher.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FeatureMatcherSetDescriptor.java
new file mode 100644 (file)
index 0000000..b3d19bb
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+import jalview.schemabinding.version2.CompoundMatcher;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.FeatureMatcherSet;
+import jalview.schemabinding.version2.MatchCondition;
+
+/**
+ * Class FeatureMatcherSetDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherSetDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FeatureMatcherSetDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "FeatureMatcherSet";
+    _elementDefinition = false;
+
+    // -- set grouping compositor
+    setCompositorAsChoice();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- initialize element descriptors
+
+    // -- _matchCondition
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            MatchCondition.class, "_matchCondition", "matchCondition",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcherSet target = (FeatureMatcherSet) object;
+        return target.getMatchCondition();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcherSet target = (FeatureMatcherSet) object;
+          target.setMatchCondition((MatchCondition) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new MatchCondition();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matchCondition
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+    // -- _compoundMatcher
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            CompoundMatcher.class, "_compoundMatcher", "compoundMatcher",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        FeatureMatcherSet target = (FeatureMatcherSet) object;
+        return target.getCompoundMatcher();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          FeatureMatcherSet target = (FeatureMatcherSet) object;
+          target.setCompoundMatcher((CompoundMatcher) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new CompoundMatcher();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _compoundMatcher
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  @Override
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  @Override
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  @Override
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.FeatureMatcherSet.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  @Override
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  @Override
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  @Override
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  @Override
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  @Override
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java b/src/jalview/schemabinding/version2/descriptors/FilterDescriptor.java
new file mode 100644 (file)
index 0000000..f58f9ae
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.Filter;
+
+/**
+ * Class FilterDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FilterDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public FilterDescriptor()
+  {
+    super();
+    _xmlName = "filter";
+    _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
+    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
+    org.exolab.castor.mapping.FieldHandler handler = null;
+    org.exolab.castor.xml.FieldValidator fieldValidator = null;
+    // -- initialize attribute descriptors
+
+    // -- _featureType
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_featureType", "featureType",
+            org.exolab.castor.xml.NodeType.Attribute);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Filter target = (Filter) object;
+        return target.getFeatureType();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Filter target = (Filter) object;
+          target.setFeatureType((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _featureType
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- initialize element descriptors
+
+    // -- _matcherSet
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class, "_matcherSet",
+            "matcherSet", org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Filter target = (Filter) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Filter target = (Filter) object;
+          target.setMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setRequired(true);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSet
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(1);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.Filter.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index d65de13..459d645 100644 (file)
@@ -7,11 +7,13 @@
 
 package jalview.schemabinding.version2.descriptors;
 
+import jalview.schemabinding.version2.Colour;
+import jalview.schemabinding.version2.Filter;
+
 //---------------------------------/
 //- Imported classes and packages -/
 //---------------------------------/
 
-import jalview.schemabinding.version2.Colour;
 import jalview.schemabinding.version2.JalviewUserColours;
 
 /**
@@ -19,8 +21,8 @@ import jalview.schemabinding.version2.JalviewUserColours;
  * 
  * @version $Revision$ $Date$
  */
-public class JalviewUserColoursDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class JalviewUserColoursDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -192,8 +194,8 @@ public class JalviewUserColoursDescriptor extends
       }
 
       @Override
-      public void resetValue(Object object) throws IllegalStateException,
-              IllegalArgumentException
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
       {
         try
         {
@@ -221,6 +223,64 @@ public class JalviewUserColoursDescriptor extends
     { // -- local scope
     }
     desc.setValidator(fieldValidator);
+    // -- _filterList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            Filter.class, "_filterList", "filter",
+            org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      @Override
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        JalviewUserColours target = (JalviewUserColours) object;
+        return target.getFilter();
+      }
+
+      @Override
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          JalviewUserColours target = (JalviewUserColours) object;
+          target.addFilter((Filter) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          JalviewUserColours target = (JalviewUserColours) object;
+          target.removeAllFilter();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      @Override
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new Filter();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _filterList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatchConditionDescriptor.java
new file mode 100644 (file)
index 0000000..8373421
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.MatchCondition;
+
+/**
+ * Class MatchConditionDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatchConditionDescriptor extends
+        jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatchConditionDescriptor()
+  {
+    super();
+    setExtendsWithoutFlatten(
+            new jalview.schemabinding.version2.descriptors.FeatureMatcherDescriptor());
+    _xmlName = "matchCondition";
+    _elementDefinition = true;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.MatchCondition.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java b/src/jalview/schemabinding/version2/descriptors/MatcherSetDescriptor.java
new file mode 100644 (file)
index 0000000..2807f92
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.MatcherSet;
+
+/**
+ * Class MatcherSetDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class MatcherSetDescriptor extends
+        jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public MatcherSetDescriptor()
+  {
+    super();
+    setExtendsWithoutFlatten(
+            new jalview.schemabinding.version2.descriptors.FeatureMatcherSetDescriptor());
+    _xmlName = "matcherSet";
+    _elementDefinition = true;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.MatcherSet.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index f582311..ab7a626 100644 (file)
@@ -7,8 +7,8 @@
 
 package jalview.schemabinding.version2.descriptors;
 
-//---------------------------------/
-//- Imported classes and packages -/
+  //---------------------------------/
+ //- Imported classes and packages -/
 //---------------------------------/
 
 import jalview.schemabinding.version2.OtherData;
@@ -18,231 +18,255 @@ import jalview.schemabinding.version2.OtherData;
  * 
  * @version $Revision$ $Date$
  */
-public class OtherDataDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
-{
-
-  // --------------------------/
-  // - Class/Member Variables -/
-  // --------------------------/
-
-  /**
-   * Field _elementDefinition.
-   */
-  private boolean _elementDefinition;
-
-  /**
-   * Field _nsPrefix.
-   */
-  private java.lang.String _nsPrefix;
-
-  /**
-   * Field _nsURI.
-   */
-  private java.lang.String _nsURI;
-
-  /**
-   * Field _xmlName.
-   */
-  private java.lang.String _xmlName;
-
-  // ----------------/
-  // - Constructors -/
-  // ----------------/
-
-  public OtherDataDescriptor()
-  {
-    super();
-    _nsURI = "www.jalview.org";
-    _xmlName = "otherData";
-    _elementDefinition = true;
-    org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
-    org.exolab.castor.mapping.FieldHandler handler = null;
-    org.exolab.castor.xml.FieldValidator fieldValidator = null;
-    // -- initialize attribute descriptors
-
-    // -- _key
-    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_key", "key",
-            org.exolab.castor.xml.NodeType.Attribute);
-    desc.setImmutable(true);
-    handler = new org.exolab.castor.xml.XMLFieldHandler()
-    {
-      public java.lang.Object getValue(java.lang.Object object)
-              throws IllegalStateException
-      {
-        OtherData target = (OtherData) object;
-        return target.getKey();
-      }
-
-      public void setValue(java.lang.Object object, java.lang.Object value)
-              throws IllegalStateException, IllegalArgumentException
-      {
-        try
-        {
-          OtherData target = (OtherData) object;
-          target.setKey((java.lang.String) value);
-        } catch (java.lang.Exception ex)
-        {
-          throw new IllegalStateException(ex.toString());
+public class OtherDataDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public OtherDataDescriptor() {
+        super();
+        _nsURI = "www.jalview.org";
+        _xmlName = "otherData";
+        _elementDefinition = true;
+        org.exolab.castor.xml.util.XMLFieldDescriptorImpl  desc           = null;
+        org.exolab.castor.mapping.FieldHandler             handler        = null;
+        org.exolab.castor.xml.FieldValidator               fieldValidator = null;
+        //-- initialize attribute descriptors
+        
+        //-- _key
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key", "key", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getKey();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setKey( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setRequired(true);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _key
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        fieldValidator.setMinOccurs(1);
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
+        }
+        desc.setValidator(fieldValidator);
+        //-- _key2
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_key2", "key2", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getKey2();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setKey2( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _key2
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
+        }
+        desc.setValidator(fieldValidator);
+        //-- _value
+        desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(java.lang.String.class, "_value", "value", org.exolab.castor.xml.NodeType.Attribute);
+        desc.setImmutable(true);
+        handler = new org.exolab.castor.xml.XMLFieldHandler() {
+            public java.lang.Object getValue( java.lang.Object object ) 
+                throws IllegalStateException
+            {
+                OtherData target = (OtherData) object;
+                return target.getValue();
+            }
+            public void setValue( java.lang.Object object, java.lang.Object value) 
+                throws IllegalStateException, IllegalArgumentException
+            {
+                try {
+                    OtherData target = (OtherData) object;
+                    target.setValue( (java.lang.String) value);
+                } catch (java.lang.Exception ex) {
+                    throw new IllegalStateException(ex.toString());
+                }
+            }
+            public java.lang.Object newInstance(java.lang.Object parent) {
+                return null;
+            }
+        };
+        desc.setHandler(handler);
+        desc.setRequired(true);
+        desc.setMultivalued(false);
+        addFieldDescriptor(desc);
+        
+        //-- validation code for: _value
+        fieldValidator = new org.exolab.castor.xml.FieldValidator();
+        fieldValidator.setMinOccurs(1);
+        { //-- local scope
+            org.exolab.castor.xml.validators.StringValidator typeValidator;
+            typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+            fieldValidator.setValidator(typeValidator);
+            typeValidator.setWhiteSpace("preserve");
         }
-      }
+        desc.setValidator(fieldValidator);
+        //-- initialize element descriptors
+        
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
 
-      public java.lang.Object newInstance(java.lang.Object parent)
-      {
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
         return null;
-      }
-    };
-    desc.setHandler(handler);
-    desc.setRequired(true);
-    desc.setMultivalued(false);
-    addFieldDescriptor(desc);
-
-    // -- validation code for: _key
-    fieldValidator = new org.exolab.castor.xml.FieldValidator();
-    fieldValidator.setMinOccurs(1);
-    { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
     }
-    desc.setValidator(fieldValidator);
-    // -- _value
-    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
-            java.lang.String.class, "_value", "value",
-            org.exolab.castor.xml.NodeType.Attribute);
-    desc.setImmutable(true);
-    handler = new org.exolab.castor.xml.XMLFieldHandler()
-    {
-      public java.lang.Object getValue(java.lang.Object object)
-              throws IllegalStateException
-      {
-        OtherData target = (OtherData) object;
-        return target.getValue();
-      }
-
-      public void setValue(java.lang.Object object, java.lang.Object value)
-              throws IllegalStateException, IllegalArgumentException
-      {
-        try
-        {
-          OtherData target = (OtherData) object;
-          target.setValue((java.lang.String) value);
-        } catch (java.lang.Exception ex)
-        {
-          throw new IllegalStateException(ex.toString());
-        }
-      }
 
-      public java.lang.Object newInstance(java.lang.Object parent)
-      {
-        return null;
-      }
-    };
-    desc.setHandler(handler);
-    desc.setRequired(true);
-    desc.setMultivalued(false);
-    addFieldDescriptor(desc);
-
-    // -- validation code for: _value
-    fieldValidator = new org.exolab.castor.xml.FieldValidator();
-    fieldValidator.setMinOccurs(1);
-    { // -- local scope
-      org.exolab.castor.xml.validators.StringValidator typeValidator;
-      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
-      fieldValidator.setValidator(typeValidator);
-      typeValidator.setWhiteSpace("preserve");
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.OtherData.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
     }
-    desc.setValidator(fieldValidator);
-    // -- initialize element descriptors
-
-  }
-
-  // -----------/
-  // - Methods -/
-  // -----------/
-
-  /**
-   * Method getAccessMode.
-   * 
-   * @return the access mode specified for this class.
-   */
-  public org.exolab.castor.mapping.AccessMode getAccessMode()
-  {
-    return null;
-  }
-
-  /**
-   * Method getIdentity.
-   * 
-   * @return the identity field, null if this class has no identity.
-   */
-  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
-  {
-    return super.getIdentity();
-  }
-
-  /**
-   * Method getJavaClass.
-   * 
-   * @return the Java class represented by this descriptor.
-   */
-  public java.lang.Class getJavaClass()
-  {
-    return jalview.schemabinding.version2.OtherData.class;
-  }
-
-  /**
-   * Method getNameSpacePrefix.
-   * 
-   * @return the namespace prefix to use when marshaling as XML.
-   */
-  public java.lang.String getNameSpacePrefix()
-  {
-    return _nsPrefix;
-  }
-
-  /**
-   * Method getNameSpaceURI.
-   * 
-   * @return the namespace URI used when marshaling and unmarshaling as XML.
-   */
-  public java.lang.String getNameSpaceURI()
-  {
-    return _nsURI;
-  }
-
-  /**
-   * Method getValidator.
-   * 
-   * @return a specific validator for the class described by this
-   *         ClassDescriptor.
-   */
-  public org.exolab.castor.xml.TypeValidator getValidator()
-  {
-    return this;
-  }
-
-  /**
-   * Method getXMLName.
-   * 
-   * @return the XML Name for the Class being described.
-   */
-  public java.lang.String getXMLName()
-  {
-    return _xmlName;
-  }
-
-  /**
-   * Method isElementDefinition.
-   * 
-   * @return true if XML schema definition of this Class is that of a global
-   *         element or element with anonymous type definition.
-   */
-  public boolean isElementDefinition()
-  {
-    return _elementDefinition;
-  }
 
 }
index 4703f46..c816e43 100644 (file)
@@ -18,8 +18,8 @@ import jalview.schemabinding.version2.Setting;
  * 
  * @version $Revision$ $Date$
  */
-public class SettingDescriptor extends
-        org.exolab.castor.xml.util.XMLClassDescriptorImpl
+public class SettingDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
 {
 
   // --------------------------/
@@ -56,6 +56,9 @@ public class SettingDescriptor extends
     _nsURI = "www.jalview.org";
     _xmlName = "setting";
     _elementDefinition = true;
+
+    // -- set grouping compositor
+    setCompositorAsSequence();
     org.exolab.castor.xml.util.XMLFieldDescriptorImpl desc = null;
     org.exolab.castor.mapping.FieldHandler handler = null;
     org.exolab.castor.xml.FieldValidator fieldValidator = null;
@@ -331,6 +334,52 @@ public class SettingDescriptor extends
       typeValidator.setMaxInclusive(2147483647);
     }
     desc.setValidator(fieldValidator);
+    // -- _noValueColour
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            "_noValueColour", "noValueColour",
+            org.exolab.castor.xml.NodeType.Attribute);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getNoValueColour();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.setNoValueColour(
+                  (jalview.schemabinding.version2.types.NoValueColour) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    handler = new org.exolab.castor.xml.handlers.EnumFieldHandler(
+            jalview.schemabinding.version2.types.NoValueColour.class,
+            handler);
+    desc.setImmutable(true);
+    desc.setHandler(handler);
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _noValueColour
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
     // -- _threshold
     desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
             java.lang.Float.TYPE, "_threshold", "threshold",
@@ -581,8 +630,8 @@ public class SettingDescriptor extends
             target.deleteColourByLabel();
             return;
           }
-          target.setColourByLabel(((java.lang.Boolean) value)
-                  .booleanValue());
+          target.setColourByLabel(
+                  ((java.lang.Boolean) value).booleanValue());
         } catch (java.lang.Exception ex)
         {
           throw new IllegalStateException(ex.toString());
@@ -662,6 +711,109 @@ public class SettingDescriptor extends
     desc.setValidator(fieldValidator);
     // -- initialize element descriptors
 
+    // -- _attributeNameList
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            java.lang.String.class, "_attributeNameList", "attributeName",
+            org.exolab.castor.xml.NodeType.Element);
+    desc.setImmutable(true);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getAttributeName();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.addAttributeName((java.lang.String) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public void resetValue(Object object)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.removeAllAttributeName();
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return null;
+      }
+    };
+    desc.setHandler(handler);
+    desc.setNameSpaceURI("www.jalview.org");
+    desc.setMultivalued(true);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _attributeNameList
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    fieldValidator.setMinOccurs(0);
+    fieldValidator.setMaxOccurs(2);
+    { // -- local scope
+      org.exolab.castor.xml.validators.StringValidator typeValidator;
+      typeValidator = new org.exolab.castor.xml.validators.StringValidator();
+      fieldValidator.setValidator(typeValidator);
+      typeValidator.setWhiteSpace("preserve");
+    }
+    desc.setValidator(fieldValidator);
+    // -- _matcherSet
+    desc = new org.exolab.castor.xml.util.XMLFieldDescriptorImpl(
+            jalview.schemabinding.version2.MatcherSet.class, "_matcherSet",
+            "matcherSet", org.exolab.castor.xml.NodeType.Element);
+    handler = new org.exolab.castor.xml.XMLFieldHandler()
+    {
+      public java.lang.Object getValue(java.lang.Object object)
+              throws IllegalStateException
+      {
+        Setting target = (Setting) object;
+        return target.getMatcherSet();
+      }
+
+      public void setValue(java.lang.Object object, java.lang.Object value)
+              throws IllegalStateException, IllegalArgumentException
+      {
+        try
+        {
+          Setting target = (Setting) object;
+          target.setMatcherSet(
+                  (jalview.schemabinding.version2.MatcherSet) value);
+        } catch (java.lang.Exception ex)
+        {
+          throw new IllegalStateException(ex.toString());
+        }
+      }
+
+      public java.lang.Object newInstance(java.lang.Object parent)
+      {
+        return new jalview.schemabinding.version2.MatcherSet();
+      }
+    };
+    desc.setHandler(handler);
+    desc.setNameSpaceURI("www.jalview.org");
+    desc.setMultivalued(false);
+    addFieldDescriptor(desc);
+
+    // -- validation code for: _matcherSet
+    fieldValidator = new org.exolab.castor.xml.FieldValidator();
+    { // -- local scope
+    }
+    desc.setValidator(fieldValidator);
   }
 
   // -----------/
diff --git a/src/jalview/schemabinding/version2/types/.castor.cdr b/src/jalview/schemabinding/version2/types/.castor.cdr
new file mode 100644 (file)
index 0000000..d9874b6
--- /dev/null
@@ -0,0 +1,5 @@
+#Thu Dec 14 15:28:22 GMT 2017
+jalview.schemabinding.version2.types.ColourNoValueColourType=jalview.schemabinding.version2.types.descriptors.ColourNoValueColourTypeDescriptor
+jalview.schemabinding.version2.types.FeatureMatcherByType=jalview.schemabinding.version2.types.descriptors.FeatureMatcherByTypeDescriptor
+jalview.schemabinding.version2.types.NoValueColour=jalview.schemabinding.version2.types.descriptors.NoValueColourDescriptor
+jalview.schemabinding.version2.types.ColourThreshTypeType=jalview.schemabinding.version2.types.descriptors.ColourThreshTypeTypeDescriptor
diff --git a/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java b/src/jalview/schemabinding/version2/types/ColourThreshTypeType.java
new file mode 100644 (file)
index 0000000..0330411
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class ColourThreshTypeType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The NONE type
+     */
+    public static final int NONE_TYPE = 0;
+
+    /**
+     * The instance of the NONE type
+     */
+    public static final ColourThreshTypeType NONE = new ColourThreshTypeType(NONE_TYPE, "NONE");
+
+    /**
+     * The ABOVE type
+     */
+    public static final int ABOVE_TYPE = 1;
+
+    /**
+     * The instance of the ABOVE type
+     */
+    public static final ColourThreshTypeType ABOVE = new ColourThreshTypeType(ABOVE_TYPE, "ABOVE");
+
+    /**
+     * The BELOW type
+     */
+    public static final int BELOW_TYPE = 2;
+
+    /**
+     * The instance of the BELOW type
+     */
+    public static final ColourThreshTypeType BELOW = new ColourThreshTypeType(BELOW_TYPE, "BELOW");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private ColourThreshTypeType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of ColourThreshTypeType
+     * 
+     * @return an Enumeration over all possible instances of
+     * ColourThreshTypeType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this ColourThreshTypeType
+     * 
+     * @return the type of this ColourThreshTypeType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("NONE", NONE);
+        members.put("ABOVE", ABOVE);
+        members.put("BELOW", BELOW);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * ColourThreshTypeType
+     * 
+     * @return the String representation of this ColourThreshTypeTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new ColourThreshTypeType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the ColourThreshTypeType value of parameter 'string'
+     */
+    public static jalview.schemabinding.version2.types.ColourThreshTypeType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid ColourThreshTypeType";
+            throw new IllegalArgumentException(err);
+        }
+        return (ColourThreshTypeType) obj;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java b/src/jalview/schemabinding/version2/types/FeatureMatcherByType.java
new file mode 100644 (file)
index 0000000..6e97332
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Class FeatureMatcherByType.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByType implements java.io.Serializable {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * The byLabel type
+     */
+    public static final int BYLABEL_TYPE = 0;
+
+    /**
+     * The instance of the byLabel type
+     */
+    public static final FeatureMatcherByType BYLABEL = new FeatureMatcherByType(BYLABEL_TYPE, "byLabel");
+
+    /**
+     * The byScore type
+     */
+    public static final int BYSCORE_TYPE = 1;
+
+    /**
+     * The instance of the byScore type
+     */
+    public static final FeatureMatcherByType BYSCORE = new FeatureMatcherByType(BYSCORE_TYPE, "byScore");
+
+    /**
+     * The byAttribute type
+     */
+    public static final int BYATTRIBUTE_TYPE = 2;
+
+    /**
+     * The instance of the byAttribute type
+     */
+    public static final FeatureMatcherByType BYATTRIBUTE = new FeatureMatcherByType(BYATTRIBUTE_TYPE, "byAttribute");
+
+    /**
+     * Field _memberTable.
+     */
+    private static java.util.Hashtable _memberTable = init();
+
+    /**
+     * Field type.
+     */
+    private int type = -1;
+
+    /**
+     * Field stringValue.
+     */
+    private java.lang.String stringValue = null;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    private FeatureMatcherByType(final int type, final java.lang.String value) {
+        super();
+        this.type = type;
+        this.stringValue = value;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method enumerate.Returns an enumeration of all possible
+     * instances of FeatureMatcherByType
+     * 
+     * @return an Enumeration over all possible instances of
+     * FeatureMatcherByType
+     */
+    public static java.util.Enumeration enumerate(
+    ) {
+        return _memberTable.elements();
+    }
+
+    /**
+     * Method getType.Returns the type of this FeatureMatcherByType
+     * 
+     * @return the type of this FeatureMatcherByType
+     */
+    public int getType(
+    ) {
+        return this.type;
+    }
+
+    /**
+     * Method init.
+     * 
+     * @return the initialized Hashtable for the member table
+     */
+    private static java.util.Hashtable init(
+    ) {
+        Hashtable members = new Hashtable();
+        members.put("byLabel", BYLABEL);
+        members.put("byScore", BYSCORE);
+        members.put("byAttribute", BYATTRIBUTE);
+        return members;
+    }
+
+    /**
+     * Method readResolve. will be called during deserialization to
+     * replace the deserialized object with the correct constant
+     * instance.
+     * 
+     * @return this deserialized object
+     */
+    private java.lang.Object readResolve(
+    ) {
+        return valueOf(this.stringValue);
+    }
+
+    /**
+     * Method toString.Returns the String representation of this
+     * FeatureMatcherByType
+     * 
+     * @return the String representation of this FeatureMatcherByTyp
+     */
+    public java.lang.String toString(
+    ) {
+        return this.stringValue;
+    }
+
+    /**
+     * Method valueOf.Returns a new FeatureMatcherByType based on
+     * the given String value.
+     * 
+     * @param string
+     * @return the FeatureMatcherByType value of parameter 'string'
+     */
+    public static jalview.schemabinding.version2.types.FeatureMatcherByType valueOf(
+            final java.lang.String string) {
+        java.lang.Object obj = null;
+        if (string != null) {
+            obj = _memberTable.get(string);
+        }
+        if (obj == null) {
+            String err = "" + string + " is not a valid FeatureMatcherByType";
+            throw new IllegalArgumentException(err);
+        }
+        return (FeatureMatcherByType) obj;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/NoValueColour.java b/src/jalview/schemabinding/version2/types/NoValueColour.java
new file mode 100644 (file)
index 0000000..bbef3d7
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import java.util.Hashtable;
+
+/**
+ * Graduated feature colour if no score (or attribute) value
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColour implements java.io.Serializable
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * The None type
+   */
+  public static final int NONE_TYPE = 0;
+
+  /**
+   * The instance of the None type
+   */
+  public static final NoValueColour NONE = new NoValueColour(NONE_TYPE,
+          "None");
+
+  /**
+   * The Min type
+   */
+  public static final int MIN_TYPE = 1;
+
+  /**
+   * The instance of the Min type
+   */
+  public static final NoValueColour MIN = new NoValueColour(MIN_TYPE,
+          "Min");
+
+  /**
+   * The Max type
+   */
+  public static final int MAX_TYPE = 2;
+
+  /**
+   * The instance of the Max type
+   */
+  public static final NoValueColour MAX = new NoValueColour(MAX_TYPE,
+          "Max");
+
+  /**
+   * Field _memberTable.
+   */
+  private static java.util.Hashtable _memberTable = init();
+
+  /**
+   * Field type.
+   */
+  private int type = -1;
+
+  /**
+   * Field stringValue.
+   */
+  private java.lang.String stringValue = null;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  private NoValueColour(final int type, final java.lang.String value)
+  {
+    super();
+    this.type = type;
+    this.stringValue = value;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method enumerate.Returns an enumeration of all possible instances of
+   * NoValueColour
+   * 
+   * @return an Enumeration over all possible instances of NoValueColour
+   */
+  public static java.util.Enumeration enumerate()
+  {
+    return _memberTable.elements();
+  }
+
+  /**
+   * Method getType.Returns the type of this NoValueColour
+   * 
+   * @return the type of this NoValueColour
+   */
+  public int getType()
+  {
+    return this.type;
+  }
+
+  /**
+   * Method init.
+   * 
+   * @return the initialized Hashtable for the member table
+   */
+  private static java.util.Hashtable init()
+  {
+    Hashtable members = new Hashtable();
+    members.put("None", NONE);
+    members.put("Min", MIN);
+    members.put("Max", MAX);
+    return members;
+  }
+
+  /**
+   * Method readResolve. will be called during deserialization to replace the
+   * deserialized object with the correct constant instance.
+   * 
+   * @return this deserialized object
+   */
+  private java.lang.Object readResolve()
+  {
+    return valueOf(this.stringValue);
+  }
+
+  /**
+   * Method toString.Returns the String representation of this NoValueColour
+   * 
+   * @return the String representation of this NoValueColour
+   */
+  public java.lang.String toString()
+  {
+    return this.stringValue;
+  }
+
+  /**
+   * Method valueOf.Returns a new NoValueColour based on the given String value.
+   * 
+   * @param string
+   * @return the NoValueColour value of parameter 'string'
+   */
+  public static jalview.schemabinding.version2.types.NoValueColour valueOf(
+          final java.lang.String string)
+  {
+    java.lang.Object obj = null;
+    if (string != null)
+    {
+      obj = _memberTable.get(string);
+    }
+    if (obj == null)
+    {
+      String err = "" + string + " is not a valid NoValueColour";
+      throw new IllegalArgumentException(err);
+    }
+    return (NoValueColour) obj;
+  }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/ColourThreshTypeTypeDescriptor.java
new file mode 100644 (file)
index 0000000..f978363
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.ColourThreshTypeType;
+
+/**
+ * Class ColourThreshTypeTypeDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class ColourThreshTypeTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public ColourThreshTypeTypeDescriptor() {
+        super();
+        _nsURI = "www.jalview.org/colours";
+        _xmlName = "ColourThreshTypeType";
+        _elementDefinition = false;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
+        return null;
+    }
+
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.types.ColourThreshTypeType.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/FeatureMatcherByTypeDescriptor.java
new file mode 100644 (file)
index 0000000..e392e76
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+  //---------------------------------/
+ //- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.FeatureMatcherByType;
+
+/**
+ * Class FeatureMatcherByTypeDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class FeatureMatcherByTypeDescriptor extends org.exolab.castor.xml.util.XMLClassDescriptorImpl {
+
+
+      //--------------------------/
+     //- Class/Member Variables -/
+    //--------------------------/
+
+    /**
+     * Field _elementDefinition.
+     */
+    private boolean _elementDefinition;
+
+    /**
+     * Field _nsPrefix.
+     */
+    private java.lang.String _nsPrefix;
+
+    /**
+     * Field _nsURI.
+     */
+    private java.lang.String _nsURI;
+
+    /**
+     * Field _xmlName.
+     */
+    private java.lang.String _xmlName;
+
+
+      //----------------/
+     //- Constructors -/
+    //----------------/
+
+    public FeatureMatcherByTypeDescriptor() {
+        super();
+        _nsURI = "www.jalview.org/colours";
+        _xmlName = "FeatureMatcherByType";
+        _elementDefinition = false;
+    }
+
+
+      //-----------/
+     //- Methods -/
+    //-----------/
+
+    /**
+     * Method getAccessMode.
+     * 
+     * @return the access mode specified for this class.
+     */
+    public org.exolab.castor.mapping.AccessMode getAccessMode(
+    ) {
+        return null;
+    }
+
+    /**
+     * Method getIdentity.
+     * 
+     * @return the identity field, null if this class has no
+     * identity.
+     */
+    public org.exolab.castor.mapping.FieldDescriptor getIdentity(
+    ) {
+        return super.getIdentity();
+    }
+
+    /**
+     * Method getJavaClass.
+     * 
+     * @return the Java class represented by this descriptor.
+     */
+    public java.lang.Class getJavaClass(
+    ) {
+        return jalview.schemabinding.version2.types.FeatureMatcherByType.class;
+    }
+
+    /**
+     * Method getNameSpacePrefix.
+     * 
+     * @return the namespace prefix to use when marshaling as XML.
+     */
+    public java.lang.String getNameSpacePrefix(
+    ) {
+        return _nsPrefix;
+    }
+
+    /**
+     * Method getNameSpaceURI.
+     * 
+     * @return the namespace URI used when marshaling and
+     * unmarshaling as XML.
+     */
+    public java.lang.String getNameSpaceURI(
+    ) {
+        return _nsURI;
+    }
+
+    /**
+     * Method getValidator.
+     * 
+     * @return a specific validator for the class described by this
+     * ClassDescriptor.
+     */
+    public org.exolab.castor.xml.TypeValidator getValidator(
+    ) {
+        return this;
+    }
+
+    /**
+     * Method getXMLName.
+     * 
+     * @return the XML Name for the Class being described.
+     */
+    public java.lang.String getXMLName(
+    ) {
+        return _xmlName;
+    }
+
+    /**
+     * Method isElementDefinition.
+     * 
+     * @return true if XML schema definition of this Class is that
+     * of a global
+     * element or element with anonymous type definition.
+     */
+    public boolean isElementDefinition(
+    ) {
+        return _elementDefinition;
+    }
+
+}
diff --git a/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java b/src/jalview/schemabinding/version2/types/descriptors/NoValueColourDescriptor.java
new file mode 100644 (file)
index 0000000..14c58ed
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * This class was automatically generated with 
+ * <a href="http://www.castor.org">Castor 1.1</a>, using an XML
+ * Schema.
+ * $Id$
+ */
+
+package jalview.schemabinding.version2.types.descriptors;
+
+//---------------------------------/
+//- Imported classes and packages -/
+//---------------------------------/
+
+import jalview.schemabinding.version2.types.NoValueColour;
+
+/**
+ * Class NoValueColourDescriptor.
+ * 
+ * @version $Revision$ $Date$
+ */
+public class NoValueColourDescriptor
+        extends org.exolab.castor.xml.util.XMLClassDescriptorImpl
+{
+
+  // --------------------------/
+  // - Class/Member Variables -/
+  // --------------------------/
+
+  /**
+   * Field _elementDefinition.
+   */
+  private boolean _elementDefinition;
+
+  /**
+   * Field _nsPrefix.
+   */
+  private java.lang.String _nsPrefix;
+
+  /**
+   * Field _nsURI.
+   */
+  private java.lang.String _nsURI;
+
+  /**
+   * Field _xmlName.
+   */
+  private java.lang.String _xmlName;
+
+  // ----------------/
+  // - Constructors -/
+  // ----------------/
+
+  public NoValueColourDescriptor()
+  {
+    super();
+    _nsURI = "www.jalview.org/colours";
+    _xmlName = "NoValueColour";
+    _elementDefinition = false;
+  }
+
+  // -----------/
+  // - Methods -/
+  // -----------/
+
+  /**
+   * Method getAccessMode.
+   * 
+   * @return the access mode specified for this class.
+   */
+  public org.exolab.castor.mapping.AccessMode getAccessMode()
+  {
+    return null;
+  }
+
+  /**
+   * Method getIdentity.
+   * 
+   * @return the identity field, null if this class has no identity.
+   */
+  public org.exolab.castor.mapping.FieldDescriptor getIdentity()
+  {
+    return super.getIdentity();
+  }
+
+  /**
+   * Method getJavaClass.
+   * 
+   * @return the Java class represented by this descriptor.
+   */
+  public java.lang.Class getJavaClass()
+  {
+    return jalview.schemabinding.version2.types.NoValueColour.class;
+  }
+
+  /**
+   * Method getNameSpacePrefix.
+   * 
+   * @return the namespace prefix to use when marshaling as XML.
+   */
+  public java.lang.String getNameSpacePrefix()
+  {
+    return _nsPrefix;
+  }
+
+  /**
+   * Method getNameSpaceURI.
+   * 
+   * @return the namespace URI used when marshaling and unmarshaling as XML.
+   */
+  public java.lang.String getNameSpaceURI()
+  {
+    return _nsURI;
+  }
+
+  /**
+   * Method getValidator.
+   * 
+   * @return a specific validator for the class described by this
+   *         ClassDescriptor.
+   */
+  public org.exolab.castor.xml.TypeValidator getValidator()
+  {
+    return this;
+  }
+
+  /**
+   * Method getXMLName.
+   * 
+   * @return the XML Name for the Class being described.
+   */
+  public java.lang.String getXMLName()
+  {
+    return _xmlName;
+  }
+
+  /**
+   * Method isElementDefinition.
+   * 
+   * @return true if XML schema definition of this Class is that of a global
+   *         element or element with anonymous type definition.
+   */
+  public boolean isElementDefinition()
+  {
+    return _elementDefinition;
+  }
+
+}
index 54d1c6c..7d14662 100644 (file)
@@ -22,6 +22,7 @@ package jalview.schemes;
 
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureMatcher;
 import jalview.util.ColorUtils;
 import jalview.util.Format;
 
@@ -29,10 +30,49 @@ import java.awt.Color;
 import java.util.StringTokenizer;
 
 /**
- * A class that wraps either a simple colour or a graduated colour
+ * A class that represents a colour scheme for a feature type. Options supported
+ * are currently
+ * <ul>
+ * <li>a simple colour e.g. Red</li>
+ * <li>colour by label - a colour is generated from the feature description</li>
+ * <li>graduated colour by feature score</li>
+ * <ul>
+ * <li>minimum and maximum score range must be provided</li>
+ * <li>minimum and maximum value colours should be specified</li>
+ * <li>a colour for 'no value' may optionally be provided</li>
+ * <li>colours for intermediate scores are interpolated RGB values</li>
+ * <li>there is an optional threshold above/below which to colour values</li>
+ * <li>the range may be the full value range, or may be limited by the threshold
+ * value</li>
+ * </ul>
+ * <li>colour by (text) value of a named attribute</li> <li>graduated colour by
+ * (numeric) value of a named attribute</li> </ul>
  */
 public class FeatureColour implements FeatureColourI
 {
+  private static final String ABSOLUTE = "abso";
+
+  private static final String ABOVE = "above";
+
+  private static final String BELOW = "below";
+
+  /*
+   * constants used to read or write a Jalview Features file
+   */
+  private static final String LABEL = "label";
+
+  private static final String SCORE = "score";
+
+  private static final String ATTRIBUTE = "attribute";
+
+  private static final String NO_VALUE_MIN = "noValueMin";
+
+  private static final String NO_VALUE_MAX = "noValueMax";
+
+  private static final String NO_VALUE_NONE = "noValueNone";
+
+  static final Color DEFAULT_NO_COLOUR = null;
+
   private static final String BAR = "|";
 
   final private Color colour;
@@ -41,10 +81,30 @@ public class FeatureColour implements FeatureColourI
 
   final private Color maxColour;
 
+  /*
+   * colour to use for colour by attribute when the 
+   * attribute value is absent
+   */
+  final private Color noColour;
+
+  /*
+   * if true, then colour has a gradient based on a numerical 
+   * range (either feature score, or an attribute value)
+   */
   private boolean graduatedColour;
 
+  /*
+   * if true, colour values are generated from a text string,
+   * either feature description, or an attribute value
+   */
   private boolean colourByLabel;
 
+  /*
+   * if not null, the value of [attribute, [sub-attribute] ...]
+   *  is used for colourByLabel or graduatedColour
+   */
+  private String[] attributeName;
+
   private float threshold;
 
   private float base;
@@ -55,8 +115,6 @@ public class FeatureColour implements FeatureColourI
 
   private boolean aboveThreshold;
 
-  private boolean thresholdIsMinOrMax;
-
   private boolean isHighToLow;
 
   private boolean autoScaled;
@@ -75,16 +133,29 @@ public class FeatureColour implements FeatureColourI
 
   /**
    * Parses a Jalview features file format colour descriptor
-   * [label|][mincolour|maxcolour
-   * |[absolute|]minvalue|maxvalue|thresholdtype|thresholdvalue] Examples:
+   * <p>
+   * <code>
+   * [label|score|[attribute|attributeName]|][mincolour|maxcolour|
+   * [absolute|]minvalue|maxvalue|[noValueOption|]thresholdtype|thresholdvalue]</code>
+   * <p>
+   * 'Score' is optional (default) for a graduated colour. An attribute with
+   * sub-attribute should be written as (for example) CSQ:Consequence.
+   * noValueOption is one of <code>noValueMin, noValueMax, noValueNone</code>
+   * with default noValueMin.
+   * <p>
+   * Examples:
    * <ul>
    * <li>red</li>
    * <li>a28bbb</li>
    * <li>25,125,213</li>
    * <li>label</li>
+   * <li>attribute|CSQ:PolyPhen</li>
    * <li>label|||0.0|0.0|above|12.5</li>
    * <li>label|||0.0|0.0|below|12.5</li>
    * <li>red|green|12.0|26.0|none</li>
+   * <li>score|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|12.0|26.0|none</li>
+   * <li>attribute|AF|red|green|noValueNone|12.0|26.0|none</li>
    * <li>a28bbb|3eb555|12.0|26.0|above|12.5</li>
    * <li>a28bbb|3eb555|abso|12.0|26.0|below|12.5</li>
    * </ul>
@@ -94,34 +165,71 @@ public class FeatureColour implements FeatureColourI
    * @throws IllegalArgumentException
    *           if not parseable
    */
-  public static FeatureColour parseJalviewFeatureColour(String descriptor)
+  public static FeatureColourI parseJalviewFeatureColour(String descriptor)
   {
-    StringTokenizer gcol = new StringTokenizer(descriptor, "|", true);
+    StringTokenizer gcol = new StringTokenizer(descriptor, BAR, true);
     float min = Float.MIN_VALUE;
     float max = Float.MAX_VALUE;
-    boolean labelColour = false;
+    boolean byLabel = false;
+    boolean byAttribute = false;
+    String attName = null;
+    String mincol = null;
+    String maxcol = null;
 
-    String mincol = gcol.nextToken();
-    if (mincol == "|")
+    /*
+     * first token should be 'label', or 'score', or an
+     * attribute name, or simple colour, or minimum colour
+     */
+    String nextToken = gcol.nextToken();
+    if (nextToken == BAR)
     {
       throw new IllegalArgumentException(
               "Expected either 'label' or a colour specification in the line: "
                       + descriptor);
     }
-    String maxcol = null;
-    if (mincol.toLowerCase().indexOf("label") == 0)
+    if (nextToken.toLowerCase().startsWith(LABEL))
+    {
+      byLabel = true;
+      // get the token after the next delimiter:
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else if (nextToken.toLowerCase().startsWith(SCORE))
+    {
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+    }
+    else if (nextToken.toLowerCase().startsWith(ATTRIBUTE))
     {
-      labelColour = true;
+      byAttribute = true;
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
+      attName = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
-      // skip '|'
       mincol = (gcol.hasMoreTokens() ? gcol.nextToken() : null);
     }
+    else
+    {
+      mincol = nextToken;
+    }
 
-    if (!labelColour && !gcol.hasMoreTokens())
+    /*
+     * if only one token, it can validly be label, attributeName,
+     * or a plain colour value
+     */
+    if (!gcol.hasMoreTokens())
     {
-      /*
-       * only a simple colour specification - parse it
-       */
+      if (byLabel || byAttribute)
+      {
+        FeatureColourI fc = new FeatureColour();
+        fc.setColourByLabel(true);
+        if (byAttribute)
+        {
+          fc.setAttributeName(
+                  FeatureMatcher.fromAttributeDisplayName(attName));
+        }
+        return fc;
+      }
+
       Color colour = ColorUtils.parseColourString(descriptor);
       if (colour == null)
       {
@@ -132,34 +240,64 @@ public class FeatureColour implements FeatureColourI
     }
 
     /*
+     * continue parsing for min/max/no colour (if graduated)
+     * and for threshold (colour by text or graduated)
+     */
+
+    /*
      * autoScaled == true: colours range over actual score range
      * autoScaled == false ('abso'): colours range over min/max range
      */
     boolean autoScaled = true;
     String tok = null, minval, maxval;
+    String noValueColour = NO_VALUE_MIN;
+
     if (mincol != null)
     {
       // at least four more tokens
-      if (mincol.equals("|"))
+      if (mincol.equals(BAR))
       {
-        mincol = "";
+        mincol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       maxcol = gcol.nextToken();
-      if (maxcol.equals("|"))
+      if (maxcol.equals(BAR))
       {
-        maxcol = "";
+        maxcol = null;
       }
       else
       {
         gcol.nextToken(); // skip next '|'
       }
       tok = gcol.nextToken();
+
+      /*
+       * check for specifier for colour for no attribute value
+       * (new in 2.11, defaults to minColour if not specified)
+       */
+      if (tok.equalsIgnoreCase(NO_VALUE_MIN))
+      {
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_MAX))
+      {
+        noValueColour = NO_VALUE_MAX;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+      else if (tok.equalsIgnoreCase(NO_VALUE_NONE))
+      {
+        noValueColour = NO_VALUE_NONE;
+        tok = gcol.nextToken();
+        tok = gcol.nextToken();
+      }
+
       gcol.nextToken(); // skip next '|'
-      if (tok.toLowerCase().startsWith("abso"))
+      if (tok.toLowerCase().startsWith(ABSOLUTE))
       {
         minval = gcol.nextToken();
         gcol.nextToken(); // skip next '|'
@@ -201,34 +339,45 @@ public class FeatureColour implements FeatureColourI
     }
     else
     {
-      // add in some dummy min/max colours for the label-only
-      // colourscheme.
-      mincol = "FFFFFF";
-      maxcol = "000000";
+      /*
+       * dummy min/max colours for colour by text
+       * (label or attribute value)
+       */
+      mincol = "white";
+      maxcol = "black";
+      byLabel = true;
     }
 
     /*
-     * construct the FeatureColour
+     * construct the FeatureColour!
      */
     FeatureColour featureColour;
     try
     {
       Color minColour = ColorUtils.parseColourString(mincol);
       Color maxColour = ColorUtils.parseColourString(maxcol);
-      featureColour = new FeatureColour(minColour, maxColour, min, max);
-      featureColour.setColourByLabel(labelColour);
+      Color noColour = noValueColour.equals(NO_VALUE_MAX) ? maxColour
+              : (noValueColour.equals(NO_VALUE_NONE) ? null : minColour);
+      featureColour = new FeatureColour(minColour, maxColour, noColour, min,
+              max);
+      featureColour.setColourByLabel(minColour == null);
       featureColour.setAutoScaled(autoScaled);
+      if (byAttribute)
+      {
+        featureColour.setAttributeName(
+                FeatureMatcher.fromAttributeDisplayName(attName));
+      }
       // add in any additional parameters
       String ttype = null, tval = null;
       if (gcol.hasMoreTokens())
       {
         // threshold type and possibly a threshold value
         ttype = gcol.nextToken();
-        if (ttype.toLowerCase().startsWith("below"))
+        if (ttype.toLowerCase().startsWith(BELOW))
         {
           featureColour.setBelowThreshold(true);
         }
-        else if (ttype.toLowerCase().startsWith("above"))
+        else if (ttype.toLowerCase().startsWith(ABOVE))
         {
           featureColour.setAboveThreshold(true);
         }
@@ -260,7 +409,7 @@ public class FeatureColour implements FeatureColourI
                 "Ignoring additional tokens in parameters in graduated colour specification\n");
         while (gcol.hasMoreTokens())
         {
-          System.err.println("|" + gcol.nextToken());
+          System.err.println(BAR + gcol.nextToken());
         }
         System.err.println("\n");
       }
@@ -288,6 +437,7 @@ public class FeatureColour implements FeatureColourI
   {
     minColour = Color.WHITE;
     maxColour = Color.BLACK;
+    noColour = DEFAULT_NO_COLOUR;
     minRed = 0f;
     minGreen = 0f;
     minBlue = 0f;
@@ -298,7 +448,8 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Constructor given a colour range and a score range
+   * Constructor given a colour range and a score range, defaulting 'no value
+   * colour' to be the same as minimum colour
    * 
    * @param low
    * @param high
@@ -307,36 +458,7 @@ public class FeatureColour implements FeatureColourI
    */
   public FeatureColour(Color low, Color high, float min, float max)
   {
-    if (low == null)
-    {
-      low = Color.white;
-    }
-    if (high == null)
-    {
-      high = Color.black;
-    }
-    graduatedColour = true;
-    colour = null;
-    minColour = low;
-    maxColour = high;
-    threshold = Float.NaN;
-    isHighToLow = min >= max;
-    minRed = low.getRed() / 255f;
-    minGreen = low.getGreen() / 255f;
-    minBlue = low.getBlue() / 255f;
-    deltaRed = (high.getRed() / 255f) - minRed;
-    deltaGreen = (high.getGreen() / 255f) - minGreen;
-    deltaBlue = (high.getBlue() / 255f) - minBlue;
-    if (isHighToLow)
-    {
-      base = max;
-      range = min - max;
-    }
-    else
-    {
-      base = min;
-      range = max - min;
-    }
+    this(low, high, low, min, max);
   }
 
   /**
@@ -350,6 +472,7 @@ public class FeatureColour implements FeatureColourI
     colour = fc.colour;
     minColour = fc.minColour;
     maxColour = fc.maxColour;
+    noColour = fc.noColour;
     minRed = fc.minRed;
     minGreen = fc.minGreen;
     minBlue = fc.minBlue;
@@ -359,6 +482,7 @@ public class FeatureColour implements FeatureColourI
     base = fc.base;
     range = fc.range;
     isHighToLow = fc.isHighToLow;
+    attributeName = fc.attributeName;
     setAboveThreshold(fc.isAboveThreshold());
     setBelowThreshold(fc.isBelowThreshold());
     setThreshold(fc.getThreshold());
@@ -376,10 +500,54 @@ public class FeatureColour implements FeatureColourI
   public FeatureColour(FeatureColour fc, float min, float max)
   {
     this(fc);
-    graduatedColour = true;
     updateBounds(min, max);
   }
 
+  /**
+   * Constructor for a graduated colour
+   * 
+   * @param low
+   * @param high
+   * @param noValueColour
+   * @param min
+   * @param max
+   */
+  public FeatureColour(Color low, Color high, Color noValueColour,
+          float min, float max)
+  {
+    if (low == null)
+    {
+      low = Color.white;
+    }
+    if (high == null)
+    {
+      high = Color.black;
+    }
+    graduatedColour = true;
+    colour = null;
+    minColour = low;
+    maxColour = high;
+    noColour = noValueColour;
+    threshold = Float.NaN;
+    isHighToLow = min >= max;
+    minRed = low.getRed() / 255f;
+    minGreen = low.getGreen() / 255f;
+    minBlue = low.getBlue() / 255f;
+    deltaRed = (high.getRed() / 255f) - minRed;
+    deltaGreen = (high.getGreen() / 255f) - minGreen;
+    deltaBlue = (high.getBlue() / 255f) - minBlue;
+    if (isHighToLow)
+    {
+      base = max;
+      range = min - max;
+    }
+    else
+    {
+      base = min;
+      range = max - min;
+    }
+  }
+
   @Override
   public boolean isGraduatedColour()
   {
@@ -418,6 +586,12 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
+  public Color getNoColour()
+  {
+    return noColour;
+  }
+
+  @Override
   public boolean isColourByLabel()
   {
     return colourByLabel;
@@ -470,18 +644,6 @@ public class FeatureColour implements FeatureColourI
   }
 
   @Override
-  public boolean isThresholdMinMax()
-  {
-    return thresholdIsMinOrMax;
-  }
-
-  @Override
-  public void setThresholdMinMax(boolean b)
-  {
-    thresholdIsMinOrMax = b;
-  }
-
-  @Override
   public float getThreshold()
   {
     return threshold;
@@ -506,10 +668,7 @@ public class FeatureColour implements FeatureColourI
   }
 
   /**
-   * Updates the base and range appropriately for the given minmax range
-   * 
-   * @param min
-   * @param max
+   * {@inheritDoc}
    */
   @Override
   public void updateBounds(float min, float max)
@@ -542,7 +701,10 @@ public class FeatureColour implements FeatureColourI
   {
     if (isColourByLabel())
     {
-      return ColorUtils.createColourFromName(feature.getDescription());
+      String label = attributeName == null ? feature.getDescription()
+              : feature.getValueAsString(attributeName);
+      return label == null ? noColour : ColorUtils
+              .createColourFromName(label);
     }
 
     if (!isGraduatedColour())
@@ -552,17 +714,31 @@ public class FeatureColour implements FeatureColourI
 
     /*
      * graduated colour case, optionally with threshold
-     * Float.NaN is assigned minimum visible score colour
+     * may be based on feature score on an attribute value
+     * Float.NaN, or no value, is assigned the 'no value' colour
      */
     float scr = feature.getScore();
+    if (attributeName != null)
+    {
+      try
+      {
+        String attVal = feature.getValueAsString(attributeName);
+        scr = Float.valueOf(attVal);
+      } catch (Throwable e)
+      {
+        scr = Float.NaN;
+      }
+    }
     if (Float.isNaN(scr))
     {
-      return getMinColour();
+      return noColour;
     }
+
     if (isAboveThreshold() && scr <= threshold)
     {
       return null;
     }
+
     if (isBelowThreshold() && scr >= threshold)
     {
       return null;
@@ -635,21 +811,43 @@ public class FeatureColour implements FeatureColourI
     else
     {
       StringBuilder sb = new StringBuilder(32);
-      if (isColourByLabel())
+      if (isColourByAttribute())
       {
-        sb.append("label");
-        if (hasThreshold())
-        {
-          sb.append(BAR).append(BAR).append(BAR);
-        }
+        sb.append(ATTRIBUTE).append(BAR);
+        sb.append(
+                FeatureMatcher.toAttributeDisplayName(getAttributeName()));
+      }
+      else if (isColourByLabel())
+      {
+        sb.append(LABEL);
+      }
+      else
+      {
+        sb.append(SCORE);
       }
       if (isGraduatedColour())
       {
-        sb.append(Format.getHexString(getMinColour())).append(BAR);
+        sb.append(BAR).append(Format.getHexString(getMinColour()))
+                .append(BAR);
         sb.append(Format.getHexString(getMaxColour())).append(BAR);
+        String noValue = minColour.equals(noColour) ? NO_VALUE_MIN
+                : (maxColour.equals(noColour) ? NO_VALUE_MAX
+                        : NO_VALUE_NONE);
+        sb.append(noValue).append(BAR);
         if (!isAutoScaled())
         {
-          sb.append("abso").append(BAR);
+          sb.append(ABSOLUTE).append(BAR);
+        }
+      }
+      else
+      {
+        /*
+         * colour by text with score threshold: empty fields for
+         * minColour and maxColour (not used)
+         */
+        if (hasThreshold())
+        {
+          sb.append(BAR).append(BAR).append(BAR);
         }
       }
       if (hasThreshold() || isGraduatedColour())
@@ -658,11 +856,11 @@ public class FeatureColour implements FeatureColourI
         sb.append(getMax()).append(BAR);
         if (isBelowThreshold())
         {
-          sb.append("below").append(BAR).append(getThreshold());
+          sb.append(BELOW).append(BAR).append(getThreshold());
         }
         else if (isAboveThreshold())
         {
-          sb.append("above").append(BAR).append(getThreshold());
+          sb.append(ABOVE).append(BAR).append(getThreshold());
         }
         else
         {
@@ -674,4 +872,22 @@ public class FeatureColour implements FeatureColourI
     return String.format("%s\t%s", featureType, colourString);
   }
 
+  @Override
+  public boolean isColourByAttribute()
+  {
+    return attributeName != null;
+  }
+
+  @Override
+  public String[] getAttributeName()
+  {
+    return attributeName;
+  }
+
+  @Override
+  public void setAttributeName(String... name)
+  {
+    attributeName = name;
+  }
+
 }
index 15cb157..9809fa9 100644 (file)
@@ -58,7 +58,7 @@ public class RNAHelicesColourChooser
     oldcs = av.getGlobalColourScheme();
     if (av.getAlignment().getGroups() != null)
     {
-      oldgroupColours = new Hashtable<SequenceGroup, ColourSchemeI>();
+      oldgroupColours = new Hashtable<>();
       for (SequenceGroup sg : ap.getAlignment().getGroups())
       {
         if (sg.getColourScheme() != null)
@@ -71,7 +71,7 @@ public class RNAHelicesColourChooser
     this.ap = ap;
 
     adjusting = true;
-    Vector<String> list = new Vector<String>();
+    Vector<String> list = new Vector<>();
     int index = 1;
     AlignmentAnnotation[] anns = av.getAlignment().getAlignmentAnnotation();
     if (anns != null)
@@ -105,6 +105,6 @@ public class RNAHelicesColourChooser
 
     av.setGlobalColourScheme(rhc);
 
-    ap.paintAlignment(true);
+    ap.paintAlignment(true, true);
   }
 }
index 55df1d1..a4e6480 100755 (executable)
@@ -39,14 +39,14 @@ public class ResidueProperties
 
   public static final int[] purinepyrimidineIndex;
 
-  public static final Map<String, Integer> aa3Hash = new HashMap<String, Integer>();
+  public static final Map<String, Integer> aa3Hash = new HashMap<>();
 
-  public static final Map<String, String> aa2Triplet = new HashMap<String, String>();
+  public static final Map<String, String> aa2Triplet = new HashMap<>();
 
-  public static final Map<String, String> nucleotideName = new HashMap<String, String>();
+  public static final Map<String, String> nucleotideName = new HashMap<>();
 
   // lookup from modified amino acid (e.g. MSE) to canonical form (e.g. MET)
-  public static final Map<String, String> modifications = new HashMap<String, String>();
+  public static final Map<String, String> modifications = new HashMap<>();
 
   static
   {
@@ -496,25 +496,27 @@ public class ResidueProperties
    * Color.white, // R Color.white, // Y Color.white, // N Color.white, // Gap
    */
 
-  public static List<String> STOP = Arrays.asList("TGA", "TAA", "TAG");
+  public static String STOP = "STOP";
+
+  public static List<String> STOP_CODONS = Arrays.asList("TGA", "TAA", "TAG");
 
   public static String START = "ATG";
 
   /**
    * Nucleotide Ambiguity Codes
    */
-  public static final Map<String, String[]> ambiguityCodes = new Hashtable<String, String[]>();
+  public static final Map<String, String[]> ambiguityCodes = new Hashtable<>();
 
   /**
    * Codon triplets with additional symbols for unambiguous codons that include
    * ambiguity codes
    */
-  public static final Hashtable<String, String> codonHash2 = new Hashtable<String, String>();
+  public static final Hashtable<String, String> codonHash2 = new Hashtable<>();
 
   /**
    * all ambiguity codes for a given base
    */
-  public final static Hashtable<String, List<String>> _ambiguityCodes = new Hashtable<String, List<String>>();
+  public final static Hashtable<String, List<String>> _ambiguityCodes = new Hashtable<>();
 
   static
   {
@@ -638,7 +640,7 @@ public class ResidueProperties
         List<String> codesfor = _ambiguityCodes.get(r);
         if (codesfor == null)
         {
-          _ambiguityCodes.put(r, codesfor = new ArrayList<String>());
+          _ambiguityCodes.put(r, codesfor = new ArrayList<>());
         }
         if (!codesfor.contains(acode.getKey()))
         {
@@ -755,27 +757,27 @@ public class ResidueProperties
   }
 
   // Stores residue codes/names and colours and other things
-  public static Map<String, Map<String, Integer>> propHash = new Hashtable<String, Map<String, Integer>>();
+  public static Map<String, Map<String, Integer>> propHash = new Hashtable<>();
 
-  public static Map<String, Integer> hydrophobic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> hydrophobic = new Hashtable<>();
 
-  public static Map<String, Integer> polar = new Hashtable<String, Integer>();
+  public static Map<String, Integer> polar = new Hashtable<>();
 
-  public static Map<String, Integer> small = new Hashtable<String, Integer>();
+  public static Map<String, Integer> small = new Hashtable<>();
 
-  public static Map<String, Integer> positive = new Hashtable<String, Integer>();
+  public static Map<String, Integer> positive = new Hashtable<>();
 
-  public static Map<String, Integer> negative = new Hashtable<String, Integer>();
+  public static Map<String, Integer> negative = new Hashtable<>();
 
-  public static Map<String, Integer> charged = new Hashtable<String, Integer>();
+  public static Map<String, Integer> charged = new Hashtable<>();
 
-  public static Map<String, Integer> aromatic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> aromatic = new Hashtable<>();
 
-  public static Map<String, Integer> aliphatic = new Hashtable<String, Integer>();
+  public static Map<String, Integer> aliphatic = new Hashtable<>();
 
-  public static Map<String, Integer> tiny = new Hashtable<String, Integer>();
+  public static Map<String, Integer> tiny = new Hashtable<>();
 
-  public static Map<String, Integer> proline = new Hashtable<String, Integer>();
+  public static Map<String, Integer> proline = new Hashtable<>();
 
   static
   {
@@ -1149,7 +1151,7 @@ public class ResidueProperties
     String cdn = codonHash2.get(lccodon.toUpperCase());
     if ("*".equals(cdn))
     {
-      return "STOP";
+      return STOP;
     }
     return cdn;
   }
@@ -1157,7 +1159,7 @@ public class ResidueProperties
   public static Hashtable<String, String> toDssp3State;
   static
   {
-    toDssp3State = new Hashtable<String, String>();
+    toDssp3State = new Hashtable<>();
     toDssp3State.put("H", "H");
     toDssp3State.put("E", "E");
     toDssp3State.put("C", " ");
@@ -2525,7 +2527,7 @@ public class ResidueProperties
   // / cut here
   public static void main(String[] args)
   {
-    Hashtable<String, Vector<String>> aaProps = new Hashtable<String, Vector<String>>();
+    Hashtable<String, Vector<String>> aaProps = new Hashtable<>();
     System.out.println("my %aa = {");
     // invert property hashes
     for (String pname : propHash.keySet())
@@ -2536,7 +2538,7 @@ public class ResidueProperties
         Vector<String> aprops = aaProps.get(rname);
         if (aprops == null)
         {
-          aprops = new Vector<String>();
+          aprops = new Vector<>();
           aaProps.put(rname, aprops);
         }
         Integer hasprop = phash.get(rname);
@@ -2578,7 +2580,7 @@ public class ResidueProperties
   public static List<String> getResidues(boolean forNucleotide,
           boolean includeAmbiguous)
   {
-    List<String> result = new ArrayList<String>();
+    List<String> result = new ArrayList<>();
     if (forNucleotide)
     {
       for (String nuc : nucleotideName.keySet())
index 40789ed..4174f5b 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.structure;
 
 import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.Mapping;
 import jalview.datamodel.SequenceI;
 
 import java.util.ArrayList;
@@ -29,6 +30,12 @@ import java.util.List;
 
 public class StructureMapping
 {
+  public static final int UNASSIGNED_VALUE = Integer.MIN_VALUE;
+
+  private static final int PDB_RES_NUM_INDEX = 0;
+
+  private static final int PDB_ATOM_NUM_INDEX = 1;
+
   String mappingDetails;
 
   SequenceI sequence;
@@ -39,16 +46,12 @@ public class StructureMapping
 
   String pdbchain;
 
-  public static final int UNASSIGNED_VALUE = -1;
-
-  private static final int PDB_RES_NUM_INDEX = 0;
-
-  private static final int PDB_ATOM_NUM_INDEX = 1;
-
   // Mapping key is residue index while value is an array containing PDB resNum,
   // and atomNo
   HashMap<Integer, int[]> mapping;
 
+  jalview.datamodel.Mapping seqToPdbMapping = null;
+
   /**
    * Constructor
    * 
@@ -73,6 +76,14 @@ public class StructureMapping
     this.mappingDetails = mappingDetails;
   }
 
+  public StructureMapping(SequenceI seq, String pdbFile2, String pdbId2,
+          String chain, HashMap<Integer, int[]> mapping2,
+          String mappingOutput, Mapping seqToPdbMapping)
+  {
+    this(seq, pdbFile2, pdbId2, chain, mapping2, mappingOutput);
+    this.seqToPdbMapping = seqToPdbMapping;
+  }
+
   public SequenceI getSequence()
   {
     return sequence;
@@ -109,7 +120,8 @@ public class StructureMapping
   /**
    * 
    * @param seqpos
-   * @return 0 or the corresponding residue number for the sequence position
+   * @return UNASSIGNED_VALUE or the corresponding residue number for the
+   *         sequence position
    */
   public int getPDBResNum(int seqpos)
   {
@@ -134,7 +146,7 @@ public class StructureMapping
    */
   public List<int[]> getPDBResNumRanges(int fromSeqPos, int toSeqPos)
   {
-    List<int[]> result = new ArrayList<int[]>();
+    List<int[]> result = new ArrayList<>();
     int startRes = -1;
     int endRes = -1;
 
@@ -247,4 +259,110 @@ public class StructureMapping
   {
     return mapping;
   }
+
+  public Mapping getSeqToPdbMapping()
+  {
+    return seqToPdbMapping;
+  }
+
+  /**
+   * A hash function that satisfies the contract that if two mappings are
+   * equal(), they have the same hashCode
+   */
+  @Override
+  public int hashCode()
+  {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result
+            + ((mappingDetails == null) ? 0 : mappingDetails.hashCode());
+    result = prime * result
+            + ((pdbchain == null) ? 0 : pdbchain.hashCode());
+    result = prime * result + ((pdbfile == null) ? 0 : pdbfile.hashCode());
+    result = prime * result + ((pdbid == null) ? 0 : pdbid.hashCode());
+    result = prime * result
+            + ((seqToPdbMapping == null) ? 0 : seqToPdbMapping.hashCode());
+    result = prime * result
+            + ((sequence == null) ? 0 : sequence.hashCode());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (this == obj)
+    {
+      return true;
+    }
+    if (obj == null)
+    {
+      return false;
+    }
+    if (getClass() != obj.getClass())
+    {
+      return false;
+    }
+    StructureMapping other = (StructureMapping) obj;
+    if (mappingDetails == null)
+    {
+      if (other.mappingDetails != null)
+      {
+        return false;
+      }
+    }
+    else if (!mappingDetails.equals(other.mappingDetails))
+    {
+      return false;
+    }
+    if (pdbchain == null)
+    {
+      if (other.pdbchain != null)
+      {
+        return false;
+      }
+    }
+    else if (!pdbchain.equals(other.pdbchain))
+    {
+      return false;
+    }
+    if (pdbfile == null)
+    {
+      if (other.pdbfile != null)
+      {
+        return false;
+      }
+    }
+    else if (!pdbfile.equals(other.pdbfile))
+    {
+      return false;
+    }
+    if (pdbid == null)
+    {
+      if (other.pdbid != null)
+      {
+        return false;
+      }
+    }
+    else if (!pdbid.equals(other.pdbid))
+    {
+      return false;
+    }
+    if (seqToPdbMapping == null)
+    {
+      if (other.seqToPdbMapping != null)
+      {
+        return false;
+      }
+    }
+    else if (!seqToPdbMapping.equals(other.seqToPdbMapping))
+    {
+      return false;
+    }
+    if (sequence != other.sequence)
+    {
+      return false;
+    }
+
+    return true;
+  }
 }
index b973f45..cd986c0 100644 (file)
@@ -66,7 +66,7 @@ public class StructureSelectionManager
 
   static IdentityHashMap<StructureSelectionManagerProvider, StructureSelectionManager> instances;
 
-  private List<StructureMapping> mappings = new ArrayList<StructureMapping>();
+  private List<StructureMapping> mappings = new ArrayList<>();
 
   private boolean processSecondaryStructure = false;
 
@@ -74,20 +74,14 @@ public class StructureSelectionManager
 
   private boolean addTempFacAnnot = false;
 
-  private IProgressIndicator progressIndicator;
-
-  private SiftsClient siftsClient = null;
-
-  private long progressSessionId;
-
   /*
    * Set of any registered mappings between (dataset) sequences.
    */
-  private List<AlignedCodonFrame> seqmappings = new ArrayList<AlignedCodonFrame>();
+  private List<AlignedCodonFrame> seqmappings = new ArrayList<>();
 
-  private List<CommandListener> commandListeners = new ArrayList<CommandListener>();
+  private List<CommandListener> commandListeners = new ArrayList<>();
 
-  private List<SelectionListener> sel_listeners = new ArrayList<SelectionListener>();
+  private List<SelectionListener> sel_listeners = new ArrayList<>();
 
   /**
    * @return true if will try to use external services for processing secondary
@@ -175,9 +169,9 @@ public class StructureSelectionManager
    * map between the PDB IDs (or structure identifiers) used by Jalview and the
    * absolute filenames for PDB data that corresponds to it
    */
-  Map<String, String> pdbIdFileName = new HashMap<String, String>();
+  Map<String, String> pdbIdFileName = new HashMap<>();
 
-  Map<String, String> pdbFileNameId = new HashMap<String, String>();
+  Map<String, String> pdbFileNameId = new HashMap<>();
 
   public void registerPDBFile(String idForFile, String absoluteFile)
   {
@@ -228,7 +222,7 @@ public class StructureSelectionManager
     }
     if (instances == null)
     {
-      instances = new java.util.IdentityHashMap<StructureSelectionManagerProvider, StructureSelectionManager>();
+      instances = new java.util.IdentityHashMap<>();
     }
     StructureSelectionManager instance = instances.get(context);
     if (instance == null)
@@ -291,7 +285,8 @@ public class StructureSelectionManager
   }
 
   /**
-   * Returns the file name for a mapped PDB id (or null if not mapped).
+   * Returns the filename the PDB id is already mapped to if known, or null if
+   * it is not mapped
    * 
    * @param pdbid
    * @return
@@ -300,7 +295,7 @@ public class StructureSelectionManager
   {
     for (StructureMapping sm : mappings)
     {
-      if (sm.getPdbId().equals(pdbid))
+      if (sm.getPdbId().equalsIgnoreCase(pdbid))
       {
         return sm.pdbfile;
       }
@@ -324,14 +319,43 @@ public class StructureSelectionManager
    * @return null or the structure data parsed as a pdb file
    */
   synchronized public StructureFile setMapping(SequenceI[] sequence,
-          String[] targetChains, String pdbFile, DataSourceType protocol)
+          String[] targetChains, String pdbFile, DataSourceType protocol, 
+          IProgressIndicator progress)
   {
-    return setMapping(true, sequence, targetChains, pdbFile, protocol);
+    return computeMapping(true, sequence, targetChains, pdbFile, protocol,
+            progress);
+  }
+
+  /**
+   * Import a single structure file and register sequence structure mappings for
+   * broadcasting colouring, mouseovers and selection events (convenience
+   * wrapper).
+   * 
+   * @param forStructureView
+   *          when true, record the mapping for use in mouseOvers
+   * @param sequence
+   *          - one or more sequences to be mapped to pdbFile
+   * @param targetChains
+   *          - optional chain specification for mapping each sequence to pdb
+   *          (may be nill, individual elements may be nill)
+   * @param pdbFile
+   *          - structure data resource
+   * @param protocol
+   *          - how to resolve data from resource
+   * @return null or the structure data parsed as a pdb file
+   */
+  synchronized public StructureFile setMapping(boolean forStructureView,
+          SequenceI[] sequenceArray, String[] targetChainIds,
+          String pdbFile, DataSourceType sourceType)
+  {
+    return computeMapping(forStructureView, sequenceArray, targetChainIds,
+            pdbFile, sourceType, null);
   }
 
   /**
    * create sequence structure mappings between each sequence and the given
-   * pdbFile (retrieved via the given protocol).
+   * pdbFile (retrieved via the given protocol). Either constructs a mapping
+   * using NW alignment or derives one from any available SIFTS mapping data.
    * 
    * @param forStructureView
    *          when true, record the mapping for use in mouseOvers
@@ -347,49 +371,38 @@ public class StructureSelectionManager
    *          - structure data resource
    * @param sourceType
    *          - how to resolve data from resource
+   * @param IProgressIndicator
+   *          reference to UI component that maintains a progress bar for the
+   *          mapping operation
    * @return null or the structure data parsed as a pdb file
    */
-  synchronized public StructureFile setMapping(boolean forStructureView,
-          SequenceI[] sequenceArray, String[] targetChainIds,
-          String pdbFile, DataSourceType sourceType)
+  synchronized public StructureFile computeMapping(
+          boolean forStructureView, SequenceI[] sequenceArray,
+          String[] targetChainIds, String pdbFile, DataSourceType sourceType,
+          IProgressIndicator progress)
   {
-    /*
-     * There will be better ways of doing this in the future, for now we'll use
-     * the tried and tested MCview pdb mapping
+    long progressSessionId = System.currentTimeMillis() * 3;
+
+    /**
+     * do we extract and transfer annotation from 3D data ?
      */
-    boolean parseSecStr = processSecondaryStructure;
-    if (isPDBFileRegistered(pdbFile))
-    {
-      for (SequenceI sq : sequenceArray)
-      {
-        SequenceI ds = sq;
-        while (ds.getDatasetSequence() != null)
-        {
-          ds = ds.getDatasetSequence();
-        }
-        ;
-        if (ds.getAnnotation() != null)
-        {
-          for (AlignmentAnnotation ala : ds.getAnnotation())
-          {
-            // false if any annotation present from this structure
-            // JBPNote this fails for jmol/chimera view because the *file* is
-            // passed, not the structure data ID -
-            if (PDBfile.isCalcIdForFile(ala, findIdForPDBFile(pdbFile)))
-            {
-              parseSecStr = false;
-            }
-          }
-        }
-      }
-    }
+    // FIXME: possibly should just delete
+
+    boolean parseSecStr = processSecondaryStructure
+            ? isStructureFileProcessed(pdbFile, sequenceArray)
+            : false;
+
     StructureFile pdb = null;
     boolean isMapUsingSIFTs = SiftsSettings.isMapWithSifts();
     try
     {
+      // FIXME if sourceType is not null, we've lost data here
       sourceType = AppletFormatAdapter.checkProtocol(pdbFile);
-      pdb = new JmolParser(pdbFile, sourceType);
-
+      pdb = new JmolParser(false, pdbFile, sourceType);
+      pdb.addSettings(parseSecStr && processSecondaryStructure,
+              parseSecStr && addTempFacAnnot,
+              parseSecStr && secStructServices);
+      pdb.doParse();
       if (pdb.getId() != null && pdb.getId().trim().length() > 0
               && DataSourceType.FILE == sourceType)
       {
@@ -403,7 +416,10 @@ public class StructureSelectionManager
       ex.printStackTrace();
       return null;
     }
-
+    /*
+     * sifts client - non null if SIFTS mappings are to be used 
+     */
+    SiftsClient siftsClient = null;
     try
     {
       if (isMapUsingSIFTs)
@@ -414,6 +430,7 @@ public class StructureSelectionManager
     {
       isMapUsingSIFTs = false;
       e.printStackTrace();
+      siftsClient = null;
     }
 
     String targetChainId;
@@ -500,12 +517,14 @@ public class StructureSelectionManager
         pdbFile = "INLINE" + pdb.getId();
       }
 
-      List<StructureMapping> seqToStrucMapping = new ArrayList<StructureMapping>();
+      List<StructureMapping> seqToStrucMapping = new ArrayList<>();
       if (isMapUsingSIFTs && seq.isProtein())
       {
-        setProgressBar(null);
-        setProgressBar(MessageManager
-                .getString("status.obtaining_mapping_with_sifts"));
+        if (progress!=null) {
+          progress.setProgressBar(MessageManager
+                .getString("status.obtaining_mapping_with_sifts"),
+                  progressSessionId);
+        }
         jalview.datamodel.Mapping sqmpping = maxAlignseq
                 .getMappingFromS1(false);
         if (targetChainId != null && !targetChainId.trim().isEmpty())
@@ -514,12 +533,12 @@ public class StructureSelectionManager
           try
           {
             siftsMapping = getStructureMapping(seq, pdbFile, targetChainId,
-                    pdb, maxChain, sqmpping, maxAlignseq);
+                    pdb, maxChain, sqmpping, maxAlignseq, siftsClient);
             seqToStrucMapping.add(siftsMapping);
-            maxChain.makeExactMapping(maxAlignseq, seq);
-            maxChain.transferRESNUMFeatures(seq, null);// FIXME: is this
+            maxChain.makeExactMapping(siftsMapping, seq);
+            maxChain.transferRESNUMFeatures(seq, "IEA: SIFTS");// FIXME: is this
                                                        // "IEA:SIFTS" ?
-            maxChain.transferResidueAnnotation(siftsMapping, sqmpping);
+            maxChain.transferResidueAnnotation(siftsMapping, null);
             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
 
           } catch (SiftsException e)
@@ -530,7 +549,8 @@ public class StructureSelectionManager
                     targetChainId, maxChain, pdb, maxAlignseq);
             seqToStrucMapping.add(nwMapping);
             maxChain.makeExactMapping(maxAlignseq, seq);
-            maxChain.transferRESNUMFeatures(seq, null); // FIXME: is this
+            maxChain.transferRESNUMFeatures(seq, "IEA:Jalview"); // FIXME: is
+                                                                 // this
                                                         // "IEA:Jalview" ?
             maxChain.transferResidueAnnotation(nwMapping, sqmpping);
             ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
@@ -538,27 +558,35 @@ public class StructureSelectionManager
         }
         else
         {
-          List<StructureMapping> foundSiftsMappings = new ArrayList<StructureMapping>();
+          List<StructureMapping> foundSiftsMappings = new ArrayList<>();
           for (PDBChain chain : pdb.getChains())
           {
+            StructureMapping siftsMapping = null;
             try
             {
-              StructureMapping siftsMapping = getStructureMapping(seq,
-                      pdbFile, chain.id, pdb, chain, sqmpping, maxAlignseq);
+              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
+              // "IEA:SIFTS" ?
+              chain.transferResidueAnnotation(siftsMapping, null);
             } catch (SiftsException e)
             {
               System.err.println(e.getMessage());
             }
+            catch (Exception e)
+            {
+              System.err
+                      .println(
+                              "Unexpected exception during SIFTS mapping - falling back to NW for this sequence/structure pair");
+              System.err.println(e.getMessage());
+            }
           }
           if (!foundSiftsMappings.isEmpty())
           {
             seqToStrucMapping.addAll(foundSiftsMappings);
-            maxChain.makeExactMapping(maxAlignseq, seq);
-            maxChain.transferRESNUMFeatures(seq, null);// FIXME: is this
-                                                       // "IEA:SIFTS" ?
-            maxChain.transferResidueAnnotation(foundSiftsMappings.get(0),
-                    sqmpping);
             ds.addPDBId(sqmpping.getTo().getAllPDBEntries().get(0));
           }
           else
@@ -575,27 +603,78 @@ public class StructureSelectionManager
       }
       else
       {
-        setProgressBar(null);
-        setProgressBar(MessageManager
-                .getString("status.obtaining_mapping_with_nw_alignment"));
+        if (progress != null)
+        {
+          progress.setProgressBar(MessageManager
+                                 .getString("status.obtaining_mapping_with_nw_alignment"),
+                  progressSessionId);
+        }
         StructureMapping nwMapping = getNWMappings(seq, pdbFile, maxChainId,
                 maxChain, pdb, maxAlignseq);
         seqToStrucMapping.add(nwMapping);
         ds.addPDBId(maxChain.sequence.getAllPDBEntries().get(0));
-
       }
-
       if (forStructureView)
       {
-        mappings.addAll(seqToStrucMapping);
+        for (StructureMapping sm : seqToStrucMapping)
+        {
+          addStructureMapping(sm); // not addAll!
+        }
+      }
+      if (progress != null)
+      {
+        progress.setProgressBar(null, progressSessionId);
       }
     }
     return pdb;
   }
 
+  /**
+   * check if we need to extract secondary structure from given pdbFile and
+   * transfer to sequences
+   * 
+   * @param pdbFile
+   * @param sequenceArray
+   * @return
+   */
+  private boolean isStructureFileProcessed(String pdbFile,
+          SequenceI[] sequenceArray)
+  {
+    boolean parseSecStr = true;
+    if (isPDBFileRegistered(pdbFile))
+    {
+      for (SequenceI sq : sequenceArray)
+      {
+        SequenceI ds = sq;
+        while (ds.getDatasetSequence() != null)
+        {
+          ds = ds.getDatasetSequence();
+        }
+        ;
+        if (ds.getAnnotation() != null)
+        {
+          for (AlignmentAnnotation ala : ds.getAnnotation())
+          {
+            // false if any annotation present from this structure
+            // JBPNote this fails for jmol/chimera view because the *file* is
+            // passed, not the structure data ID -
+            if (PDBfile.isCalcIdForFile(ala, findIdForPDBFile(pdbFile)))
+            {
+              parseSecStr = false;
+            }
+          }
+        }
+      }
+    }
+    return parseSecStr;
+  }
+
   public void addStructureMapping(StructureMapping sm)
   {
-    mappings.add(sm);
+    if (!mappings.contains(sm))
+    {
+      mappings.add(sm);
+    }
   }
 
   /**
@@ -609,13 +688,15 @@ public class StructureSelectionManager
    * @param maxChain
    * @param sqmpping
    * @param maxAlignseq
+   * @param siftsClient
+   *          client for retrieval of SIFTS mappings for this structure
    * @return
    * @throws SiftsException
    */
   private StructureMapping getStructureMapping(SequenceI seq,
           String pdbFile, String targetChainId, StructureFile pdb,
           PDBChain maxChain, jalview.datamodel.Mapping sqmpping,
-          AlignSeq maxAlignseq) throws SiftsException
+          AlignSeq maxAlignseq, SiftsClient siftsClient) throws SiftsException
   {
     StructureMapping curChainMapping = siftsClient
             .getSiftsStructureMapping(seq, pdbFile, targetChainId);
@@ -624,7 +705,7 @@ public class StructureSelectionManager
       PDBChain chain = pdb.findChain(targetChainId);
       if (chain != null)
       {
-        chain.transferResidueAnnotation(curChainMapping, sqmpping);
+        chain.transferResidueAnnotation(curChainMapping, null);
       }
     } catch (Exception e)
     {
@@ -683,7 +764,7 @@ public class StructureSelectionManager
             .getMappingFromS1(false);
     maxChain.transferRESNUMFeatures(seq, null);
 
-    HashMap<Integer, int[]> mapping = new HashMap<Integer, int[]>();
+    HashMap<Integer, int[]> mapping = new HashMap<>();
     int resNum = -10000;
     int index = 0;
     char insCode = ' ';
@@ -737,7 +818,7 @@ public class StructureSelectionManager
      * Remove mappings to the closed listener's PDB files, but first check if
      * another listener is still interested
      */
-    List<String> pdbs = new ArrayList<String>(Arrays.asList(pdbfiles));
+    List<String> pdbs = new ArrayList<>(Arrays.asList(pdbfiles));
 
     StructureListener sl;
     for (int i = 0; i < listeners.size(); i++)
@@ -758,7 +839,7 @@ public class StructureSelectionManager
      */
     if (pdbs.size() > 0)
     {
-      List<StructureMapping> tmp = new ArrayList<StructureMapping>();
+      List<StructureMapping> tmp = new ArrayList<>();
       for (StructureMapping sm : mappings)
       {
         if (!pdbs.contains(sm.pdbfile))
@@ -844,7 +925,7 @@ public class StructureSelectionManager
                 && sm.pdbchain.equals(atom.getChain()))
         {
           int indexpos = sm.getSeqPos(atom.getPdbResNum());
-          if (lastipos != indexpos && lastseq != sm.sequence)
+          if (lastipos != indexpos || lastseq != sm.sequence)
           {
             results.addResult(sm.sequence, indexpos, indexpos);
             lastipos = indexpos;
@@ -952,7 +1033,7 @@ public class StructureSelectionManager
       return;
     }
     int atomNo;
-    List<AtomSpec> atoms = new ArrayList<AtomSpec>();
+    List<AtomSpec> atoms = new ArrayList<>();
     for (StructureMapping sm : mappings)
     {
       if (sm.sequence == seq || sm.sequence == seq.getDatasetSequence()
@@ -1060,7 +1141,7 @@ public class StructureSelectionManager
 
   public StructureMapping[] getMapping(String pdbfile)
   {
-    List<StructureMapping> tmp = new ArrayList<StructureMapping>();
+    List<StructureMapping> tmp = new ArrayList<>();
     for (StructureMapping sm : mappings)
     {
       if (sm.pdbfile.equals(pdbfile))
@@ -1220,7 +1301,7 @@ public class StructureSelectionManager
     }
   }
 
-  Vector<AlignmentViewPanelListener> view_listeners = new Vector<AlignmentViewPanelListener>();
+  Vector<AlignmentViewPanelListener> view_listeners = new Vector<>();
 
   public synchronized void sendViewPosition(
           jalview.api.AlignmentViewPanel source, int startRes, int endRes,
@@ -1343,35 +1424,6 @@ public class StructureSelectionManager
     return null;
   }
 
-  public IProgressIndicator getProgressIndicator()
-  {
-    return progressIndicator;
-  }
-
-  public void setProgressIndicator(IProgressIndicator progressIndicator)
-  {
-    this.progressIndicator = progressIndicator;
-  }
-
-  public long getProgressSessionId()
-  {
-    return progressSessionId;
-  }
-
-  public void setProgressSessionId(long progressSessionId)
-  {
-    this.progressSessionId = progressSessionId;
-  }
-
-  public void setProgressBar(String message)
-  {
-    if (progressIndicator == null)
-    {
-      return;
-    }
-    progressIndicator.setProgressBar(message, progressSessionId);
-  }
-
   public List<AlignedCodonFrame> getSequenceMappings()
   {
     return seqmappings;
index 0d5ef99..86d5660 100644 (file)
@@ -160,10 +160,24 @@ public class CustomUrlProvider extends UrlProviderImpl
    */
   private void upgradeOldLinks(HashMap<String, UrlLink> urls)
   {
+    boolean upgrade = false;
     // upgrade old SRS link
     if (urls.containsKey(SRS_LABEL))
     {
       urls.remove(SRS_LABEL);
+      upgrade = true;
+    }
+    // upgrade old EBI link - easier just to remove and re-add than faffing
+    // around checking exact url
+    if (urls.containsKey(UrlConstants.DEFAULT_LABEL))
+    {
+      // note because this is called separately for selected and nonselected
+      // urls, the default url will not always be present
+      urls.remove(UrlConstants.DEFAULT_LABEL);
+      upgrade = true;
+    }
+    if (upgrade)
+    {
       UrlLink link = new UrlLink(UrlConstants.DEFAULT_STRING);
       link.setLabel(UrlConstants.DEFAULT_LABEL);
       urls.put(UrlConstants.DEFAULT_LABEL, link);
index d4be322..60129fb 100644 (file)
 package jalview.util;
 
 import java.awt.Color;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Random;
 
 public class ColorUtils
 {
+  private static final int MAX_CACHE_SIZE = 1729;
+  /*
+   * a cache for colours generated from text strings
+   */
+  static Map<String, Color> myColours = new HashMap<>();
 
   /**
    * Generates a random color, will mix with input color. Code taken from
@@ -260,6 +267,10 @@ public class ColorUtils
     {
       return Color.white;
     }
+    if (myColours.containsKey(name))
+    {
+      return myColours.get(name);
+    }
     int lsize = name.length();
     int start = 0;
     int end = lsize / 3;
@@ -291,6 +302,11 @@ public class ColorUtils
 
     Color color = new Color(r, g, b);
 
+    if (myColours.size() < MAX_CACHE_SIZE)
+    {
+      myColours.put(name, color);
+    }
+
     return color;
   }
 
index cb32a0e..a0a29f2 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.util;
 
 import java.util.Comparator;
index 4658724..7f1abc4 100644 (file)
@@ -77,8 +77,8 @@ public class MapList
    */
   public MapList()
   {
-    fromShifts = new ArrayList<int[]>();
-    toShifts = new ArrayList<int[]>();
+    fromShifts = new ArrayList<>();
+    toShifts = new ArrayList<>();
   }
 
   /**
@@ -116,8 +116,17 @@ public class MapList
   {
     int hashCode = 31 * fromRatio;
     hashCode = 31 * hashCode + toRatio;
-    hashCode = 31 * hashCode + fromShifts.toArray().hashCode();
-    hashCode = 31 * hashCode + toShifts.toArray().hashCode();
+    for (int[] shift : fromShifts)
+    {
+      hashCode = 31 * hashCode + shift[0];
+      hashCode = 31 * hashCode + shift[1];
+    }
+    for (int[] shift : toShifts)
+    {
+      hashCode = 31 * hashCode + shift[0];
+      hashCode = 31 * hashCode + shift[1];
+    }
+
     return hashCode;
   }
 
@@ -347,7 +356,7 @@ public class MapList
     }
 
     boolean changed = false;
-    List<int[]> merged = new ArrayList<int[]>();
+    List<int[]> merged = new ArrayList<>();
     int[] lastRange = ranges.get(0);
     int lastDirection = lastRange[1] >= lastRange[0] ? 1 : -1;
     lastRange = new int[] { lastRange[0], lastRange[1] };
@@ -803,7 +812,7 @@ public class MapList
     {
       return null;
     }
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     if (fs <= fe)
     {
       intv = fs;
@@ -1094,8 +1103,33 @@ public class MapList
    */
   public boolean isFromForwardStrand()
   {
+    return isForwardStrand(getFromRanges());
+  }
+
+  /**
+   * Returns true if mapping is to forward strand, false if to reverse strand.
+   * Result is just based on the first 'to' range that is not a single position.
+   * Default is true unless proven to be false. Behaviour is not well defined if
+   * the mapping has a mixture of forward and reverse ranges.
+   * 
+   * @return
+   */
+  public boolean isToForwardStrand()
+  {
+    return isForwardStrand(getToRanges());
+  }
+
+  /**
+   * A helper method that returns true unless at least one range has start > end.
+   * Behaviour is undefined for a mixture of forward and reverse ranges.
+   * 
+   * @param ranges
+   * @return
+   */
+  private boolean isForwardStrand(List<int[]> ranges)
+  {
     boolean forwardStrand = true;
-    for (int[] range : getFromRanges())
+    for (int[] range : ranges)
     {
       if (range[1] > range[0])
       {
@@ -1120,4 +1154,63 @@ public class MapList
             || (fromRatio == 3 && toRatio == 1);
   }
 
+  /**
+   * Returns a map which is the composite of this one and the input map. That
+   * is, the output map has the fromRanges of this map, and its toRanges are the
+   * toRanges of this map as transformed by the input map.
+   * <p>
+   * Returns null if the mappings cannot be traversed (not all toRanges of this
+   * map correspond to fromRanges of the input), or if this.toRatio does not
+   * match map.fromRatio.
+   * 
+   * <pre>
+   * Example 1:
+   *    this:   from [1-100] to [501-600]
+   *    input:  from [10-40] to [60-90]
+   *    output: from [10-40] to [560-590]
+   * Example 2 ('reverse strand exons'):
+   *    this:   from [1-100] to [2000-1951], [1000-951] // transcript to loci
+   *    input:  from [1-50]  to [41-90] // CDS to transcript
+   *    output: from [10-40] to [1960-1951], [1000-971] // CDS to gene loci
+   * </pre>
+   * 
+   * @param map
+   * @return
+   */
+  public MapList traverse(MapList map)
+  {
+    if (map == null)
+    {
+      return null;
+    }
+
+    /*
+     * compound the ratios by this rule:
+     * A:B with M:N gives A*M:B*N
+     * reduced by greatest common divisor
+     * so 1:3 with 3:3 is 3:9 or 1:3
+     * 1:3 with 3:1 is 3:3 or 1:1
+     * 1:3 with 1:3 is 1:9
+     * 2:5 with 3:7 is 6:35
+     */
+    int outFromRatio = getFromRatio() * map.getFromRatio();
+    int outToRatio = getToRatio() * map.getToRatio();
+    int gcd = MathUtils.gcd(outFromRatio, outToRatio);
+    outFromRatio /= gcd;
+    outToRatio /= gcd;
+
+    List<int[]> toRanges = new ArrayList<>();
+    for (int[] range : getToRanges())
+    {
+      int[] transferred = map.locateInTo(range[0], range[1]);
+      if (transferred == null)
+      {
+        return null;
+      }
+      toRanges.add(transferred);
+    }
+
+    return new MapList(getFromRanges(), toRanges, outFromRatio, outToRatio);
+  }
+
 }
index 3682239..b552c21 100644 (file)
@@ -542,9 +542,11 @@ public final class MappingUtils
               toSequences, fromGapChar);
     }
 
-    for (int[] hidden : hiddencols.getHiddenColumnsCopy())
+    Iterator<int[]> regions = hiddencols.iterator();
+    while (regions.hasNext())
     {
-      mapHiddenColumns(hidden, codonFrames, newHidden, fromSequences,
+      mapHiddenColumns(regions.next(), codonFrames, newHidden,
+              fromSequences,
               toSequences, fromGapChar);
     }
     return; // mappedColumns;
@@ -939,4 +941,83 @@ public final class MappingUtils
     }
     return copy;
   }
+
+  /**
+   * Answers true if range's start-end positions include those of queryRange,
+   * where either range might be in reverse direction, else false
+   * 
+   * @param range
+   *          a start-end range
+   * @param queryRange
+   *          a candidate subrange of range (start2-end2)
+   * @return
+   */
+  public static boolean rangeContains(int[] range, int[] queryRange)
+  {
+    if (range == null || queryRange == null || range.length != 2
+            || queryRange.length != 2)
+    {
+      /*
+       * invalid arguments
+       */
+      return false;
+    }
+
+    int min = Math.min(range[0], range[1]);
+    int max = Math.max(range[0], range[1]);
+  
+    return (min <= queryRange[0] && max >= queryRange[0]
+            && min <= queryRange[1] && max >= queryRange[1]);
+  }
+
+  /**
+   * Removes the specified number of positions from the given ranges. Provided
+   * to allow a stop codon to be stripped from a CDS sequence so that it matches
+   * the peptide translation length.
+   * 
+   * @param positions
+   * @param ranges
+   *          a list of (single) [start, end] ranges
+   * @return
+   */
+  public static void removeEndPositions(int positions,
+          List<int[]> ranges)
+  {
+    int toRemove = positions;
+    Iterator<int[]> it = new ReverseListIterator<>(ranges);
+    while (toRemove > 0)
+    {
+      int[] endRange = it.next();
+      if (endRange.length != 2)
+      {
+        /*
+         * not coded for [start1, end1, start2, end2, ...]
+         */
+        System.err
+                .println("MappingUtils.removeEndPositions doesn't handle multiple  ranges");
+        return;
+      }
+
+      int length = endRange[1] - endRange[0] + 1;
+      if (length <= 0)
+      {
+        /*
+         * not coded for a reverse strand range (end < start)
+         */
+        System.err
+                .println("MappingUtils.removeEndPositions doesn't handle reverse strand");
+        return;
+      }
+      if (length > toRemove)
+      {
+        endRange[1] -= toRemove;
+        toRemove = 0;
+      }
+      else
+      {
+        toRemove -= length;
+        it.remove();
+      }
+    }
+  }
 }
diff --git a/src/jalview/util/MathUtils.java b/src/jalview/util/MathUtils.java
new file mode 100644 (file)
index 0000000..72d46a2
--- /dev/null
@@ -0,0 +1,22 @@
+package jalview.util;
+
+public class MathUtils
+{
+
+  /**
+   * Returns the greatest common divisor of two integers
+   * 
+   * @param a
+   * @param b
+   * @return
+   */
+  public static int gcd(int a, int b)
+  {
+    if (b == 0)
+    {
+      return Math.abs(a);
+    }
+    return gcd(b, a % b);
+  }
+
+}
index d9f8bea..2c74609 100644 (file)
@@ -30,7 +30,7 @@ import java.awt.event.MouseEvent;
  */
 public class Platform
 {
-  private static Boolean isAMac = null;
+  private static Boolean isAMac = null, isWindows = null;
 
   private static Boolean isHeadless = null;
 
@@ -45,10 +45,29 @@ public class Platform
     {
       isAMac = System.getProperty("os.name").indexOf("Mac") > -1;
     }
+
     return isAMac.booleanValue();
 
   }
 
+  /**
+   * Check if we are on a Microsoft plaform...
+   * 
+   * @return true if we have to cope with another platform variation
+   */
+  public static boolean isWindows()
+  {
+    if (isWindows == null)
+    {
+      isWindows = System.getProperty("os.name").indexOf("Win") > -1;
+    }
+    return isWindows.booleanValue();
+  }
+
+  /**
+   * 
+   * @return true if we are running in non-interactive no UI mode
+   */
   public static boolean isHeadless()
   {
     if (isHeadless == null)
index b3456aa..2e8ace8 100644 (file)
@@ -403,4 +403,45 @@ public class StringUtils
     }
     return s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
   }
+
+  /**
+   * A helper method that strips off any leading or trailing html and body tags.
+   * If no html tag is found, then also html-encodes angle bracket characters.
+   * 
+   * @param text
+   * @return
+   */
+  public static String stripHtmlTags(String text)
+  {
+    if (text == null)
+    {
+      return null;
+    }
+    String tmp2up = text.toUpperCase();
+    int startTag = tmp2up.indexOf("<HTML>");
+    if (startTag > -1)
+    {
+      text = text.substring(startTag + 6);
+      tmp2up = tmp2up.substring(startTag + 6);
+    }
+    // is omission of "<BODY>" intentional here??
+    int endTag = tmp2up.indexOf("</BODY>");
+    if (endTag > -1)
+    {
+      text = text.substring(0, endTag);
+      tmp2up = tmp2up.substring(0, endTag);
+    }
+    endTag = tmp2up.indexOf("</HTML>");
+    if (endTag > -1)
+    {
+      text = text.substring(0, endTag);
+    }
+  
+    if (startTag == -1 && (text.contains("<") || text.contains(">")))
+    {
+      text = text.replaceAll("<", "&lt;");
+      text = text.replaceAll(">", "&gt;");
+    }
+    return text;
+  }
 }
index 3347cc7..e5cfaee 100644 (file)
@@ -55,7 +55,9 @@ public class UrlConstants
    * Default sequence URL link string for EMBL-EBI search
    */
   public static final String DEFAULT_STRING = DEFAULT_LABEL
-          + "|http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
+          + "|https://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$SEQUENCE_ID$";
+
+  private static final String COLON = ":";
 
   /*
    * not instantiable
@@ -63,4 +65,12 @@ public class UrlConstants
   private UrlConstants()
   {
   }
+
+  public static boolean isDefaultString(String link)
+  {
+    String sublink = link.substring(link.indexOf(COLON) + 1);
+    String subdefault = DEFAULT_STRING
+            .substring(DEFAULT_STRING.indexOf(COLON) + 1);
+    return sublink.equalsIgnoreCase(subdefault);
+  }
 }
diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java
new file mode 100644 (file)
index 0000000..8816a7f
--- /dev/null
@@ -0,0 +1,102 @@
+package jalview.util.matcher;
+
+import jalview.util.MessageManager;
+
+/**
+ * An enumeration for binary conditions that a user might choose from when
+ * setting filter or match conditions for values
+ */
+public enum Condition
+{
+  Contains(false, true, "Contains"),
+  NotContains(false, true, "NotContains"), Matches(false, true, "Matches"),
+  NotMatches(false, true, "NotMatches"), Present(false, false, "Present"),
+  NotPresent(false, false, "NotPresent"), EQ(true, true, "EQ"),
+  NE(true, true, "NE"), LT(true, true, "LT"), LE(true, true, "LE"),
+  GT(true, true, "GT"), GE(true, true, "GE");
+
+  private boolean numeric;
+
+  private boolean needsAPattern;
+
+  /*
+   * value used to save a Condition to the 
+   * Jalview project file or restore it from project; 
+   * it should not be changed even if enum names change in future
+   */
+  private String stableName;
+
+  /**
+   * Answers the enum value whose 'stable name' matches the argument (not case
+   * sensitive), or null if no match
+   * 
+   * @param stableName
+   * @return
+   */
+  public static Condition fromString(String stableName)
+  {
+    for (Condition c : values())
+    {
+      if (c.stableName.equalsIgnoreCase(stableName))
+      {
+        return c;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Constructor
+   * 
+   * @param isNumeric
+   * @param needsPattern
+   * @param stablename
+   */
+  Condition(boolean isNumeric, boolean needsPattern, String stablename)
+  {
+    numeric = isNumeric;
+    needsAPattern = needsPattern;
+    stableName = stablename;
+  }
+
+  /**
+   * Answers true if the condition does a numerical comparison, else false
+   * (string comparison)
+   * 
+   * @return
+   */
+  public boolean isNumeric()
+  {
+    return numeric;
+  }
+
+  /**
+   * Answers true if the condition requires a pattern to compare against, else
+   * false
+   * 
+   * @return
+   */
+  public boolean needsAPattern()
+  {
+    return needsAPattern;
+  }
+
+  public String getStableName()
+  {
+    return stableName;
+  }
+
+  /**
+   * Answers a display name for the match condition, suitable for showing in
+   * drop-down menus. The value may be internationalized using the resource key
+   * "label.matchCondition_" with the enum name appended.
+   * 
+   * @return
+   */
+  @Override
+  public String toString()
+  {
+    return MessageManager.getStringOrReturn("label.matchCondition_",
+            name());
+  }
+}
diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java
new file mode 100644 (file)
index 0000000..0792509
--- /dev/null
@@ -0,0 +1,251 @@
+package jalview.util.matcher;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A bean to describe one attribute-based filter
+ */
+public class Matcher implements MatcherI
+{
+  /*
+   * the comparison condition
+   */
+  Condition condition;
+
+  /*
+   * the string pattern as entered, or the regex, to compare to
+   * also holds the string form of float value if a numeric condition
+   */
+  String pattern;
+
+  /*
+   * the pattern in upper case, for non-case-sensitive matching
+   */
+  String uppercasePattern;
+
+  /*
+   * the compiled regex if using a pattern match condition
+   * (reserved for possible future enhancement)
+   */
+  Pattern regexPattern;
+
+  /*
+   * the value to compare to for a numerical condition
+   */
+  float value;
+
+  /**
+   * Constructor
+   * 
+   * @param cond
+   * @param compareTo
+   * @return
+   * @throws NumberFormatException
+   *           if a numerical condition is specified with a non-numeric
+   *           comparison value
+   * @throws NullPointerException
+   *           if a null condition or comparison string is specified
+   */
+  public Matcher(Condition cond, String compareTo)
+  {
+    Objects.requireNonNull(cond);
+    condition = cond;
+    if (cond.isNumeric())
+    {
+      value = Float.valueOf(compareTo);
+      pattern = String.valueOf(value);
+      uppercasePattern = pattern;
+    }
+    else
+    {
+      pattern = compareTo;
+      if (pattern != null)
+      {
+        uppercasePattern = pattern.toUpperCase();
+      }
+    }
+
+    // if we add regex conditions (e.g. matchesPattern), then
+    // pattern should hold the raw regex, and
+    // regexPattern = Pattern.compile(compareTo);
+  }
+
+  /**
+   * Constructor for a numerical match condition. Note that if a string
+   * comparison condition is specified, this will be converted to a comparison
+   * with the float value as string
+   * 
+   * @param cond
+   * @param compareTo
+   */
+  public Matcher(Condition cond, float compareTo)
+  {
+    this(cond, String.valueOf(compareTo));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("incomplete-switch")
+  @Override
+  public boolean matches(String val)
+  {
+    if (condition.isNumeric())
+    {
+      try
+      {
+        /*
+         * treat a null value (no such attribute) as
+         * failing any numerical filter condition
+         */
+        return val == null ? false : matches(Float.valueOf(val));
+      } catch (NumberFormatException e)
+      {
+        return false;
+      }
+    }
+    
+    /*
+     * a null value matches a negative condition, fails a positive test
+     */
+    if (val == null)
+    {
+      return condition == Condition.NotContains
+              || condition == Condition.NotMatches 
+              || condition == Condition.NotPresent;
+    }
+    
+    String upper = val.toUpperCase().trim();
+    boolean matched = false;
+    switch(condition) {
+    case Matches:
+      matched = upper.equals(uppercasePattern);
+      break;
+    case NotMatches:
+      matched = !upper.equals(uppercasePattern);
+      break;
+    case Contains:
+      matched = upper.indexOf(uppercasePattern) > -1;
+      break;
+    case NotContains:
+      matched = upper.indexOf(uppercasePattern) == -1;
+      break;
+    case Present:
+      matched = true;
+      break;
+    default:
+      break;
+    }
+    return matched;
+  }
+
+  /**
+   * Applies a numerical comparison match condition
+   * 
+   * @param f
+   * @return
+   */
+  @SuppressWarnings("incomplete-switch")
+  boolean matches(float f)
+  {
+    if (!condition.isNumeric())
+    {
+      return matches(String.valueOf(f));
+    }
+    
+    boolean matched = false;
+    switch (condition) {
+    case LT:
+      matched = f < value;
+      break;
+    case LE:
+      matched = f <= value;
+      break;
+    case EQ:
+      matched = f == value;
+      break;
+    case NE:
+      matched = f != value;
+      break;
+    case GT:
+      matched = f > value;
+      break;
+    case GE:
+      matched = f >= value;
+      break;
+    default:
+      break;
+    }
+
+    return matched;
+  }
+
+  /**
+   * A simple hash function that guarantees that when two objects are equal,
+   * they have the same hashcode
+   */
+  @Override
+  public int hashCode()
+  {
+    return pattern.hashCode() + condition.hashCode() + (int) value;
+  }
+
+  /**
+   * equals is overridden so that we can safely remove Matcher objects from
+   * collections (e.g. delete an attribute match condition for a feature colour)
+   */
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj == null || !(obj instanceof Matcher))
+    {
+      return false;
+    }
+    Matcher m = (Matcher) obj;
+    if (condition != m.condition || value != m.value)
+    {
+      return false;
+    }
+    if (pattern == null)
+    {
+      return m.pattern == null;
+    }
+    return uppercasePattern.equals(m.uppercasePattern);
+  }
+
+  @Override
+  public Condition getCondition()
+  {
+    return condition;
+  }
+
+  @Override
+  public String getPattern()
+  {
+    return pattern;
+  }
+
+  @Override
+  public float getFloatValue()
+  {
+    return value;
+  }
+
+  @Override
+  public String toString()
+  {
+    StringBuilder sb = new StringBuilder();
+    sb.append(condition.toString()).append(" ");
+    if (condition.isNumeric())
+    {
+      sb.append(pattern);
+    }
+    else
+    {
+      sb.append("'").append(pattern).append("'");
+    }
+
+    return sb.toString();
+  }
+}
diff --git a/src/jalview/util/matcher/MatcherI.java b/src/jalview/util/matcher/MatcherI.java
new file mode 100644 (file)
index 0000000..ca6d44c
--- /dev/null
@@ -0,0 +1,18 @@
+package jalview.util.matcher;
+
+public interface MatcherI
+{
+  /**
+   * Answers true if the given value is matched, else false
+   * 
+   * @param s
+   * @return
+   */
+  boolean matches(String s);
+
+  Condition getCondition();
+
+  String getPattern();
+
+  float getFloatValue();
+}
index 3702cd0..1366ada 100644 (file)
@@ -22,6 +22,7 @@ package jalview.viewmodel;
 
 import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
 import jalview.analysis.Conservation;
+import jalview.analysis.TreeModel;
 import jalview.api.AlignCalcManagerI;
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
@@ -33,7 +34,6 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.AlignmentView;
 import jalview.datamodel.Annotation;
-import jalview.datamodel.CigarArray;
 import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.HiddenSequences;
@@ -67,6 +67,7 @@ import java.util.BitSet;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -80,7 +81,7 @@ import java.util.Map;
 public abstract class AlignmentViewport
         implements AlignViewportI, CommandListener, VamsasSource
 {
-  final protected ViewportRanges ranges;
+  protected ViewportRanges ranges;
 
   protected ViewStyleI viewStyle = new ViewStyle();
 
@@ -948,11 +949,15 @@ public abstract class AlignmentViewport
     groupConsensus = null;
     groupConservation = null;
     hconsensus = null;
+    hconservation = null;
     hcomplementConsensus = null;
-    // colour scheme may hold reference to consensus
-    residueShading = null;
-    // TODO remove listeners from changeSupport?
+    gapcounts = null;
+    calculator = null;
+    residueShading = null; // may hold a reference to Consensus
     changeSupport = null;
+    ranges = null;
+    currentTree = null;
+    selectionGroup = null;
     setAlignment(null);
   }
 
@@ -1334,7 +1339,10 @@ public abstract class AlignmentViewport
   public void removePropertyChangeListener(
           java.beans.PropertyChangeListener listener)
   {
-    changeSupport.removePropertyChangeListener(listener);
+    if (changeSupport != null)
+    {
+      changeSupport.removePropertyChangeListener(listener);
+    }
   }
 
   /**
@@ -1673,13 +1681,6 @@ public abstract class AlignmentViewport
   }
 
   @Override
-  public CigarArray getViewAsCigars(boolean selectedRegionOnly)
-  {
-    return new CigarArray(alignment, alignment.getHiddenColumns(),
-            (selectedRegionOnly ? selectionGroup : null));
-  }
-
-  @Override
   public jalview.datamodel.AlignmentView getAlignmentView(
           boolean selectedOnly)
   {
@@ -1740,8 +1741,12 @@ public abstract class AlignmentViewport
     if (alignment.getHiddenColumns() != null
             && alignment.getHiddenColumns().hasHiddenColumns())
     {
-      selection = alignment.getHiddenColumns()
-              .getVisibleSequenceStrings(start, end, seqs);
+      for (i = 0; i < iSize; i++)
+      {
+        Iterator<int[]> blocks = alignment.getHiddenColumns()
+                .getVisContigsIterator(start, end + 1, false);
+        selection[i] = seqs[i].getSequenceStringFromIterator(blocks);
+      }
     }
     else
     {
@@ -1768,10 +1773,10 @@ public abstract class AlignmentViewport
       {
         if (start == 0)
         {
-          start = hidden.adjustForHiddenColumns(start);
+          start = hidden.visibleToAbsoluteColumn(start);
         }
 
-        end = hidden.getHiddenBoundaryRight(start);
+        end = hidden.getNextHiddenBoundary(false, start);
         if (start == end)
         {
           end = max;
@@ -1786,8 +1791,8 @@ public abstract class AlignmentViewport
 
       if (hidden != null && hidden.hasHiddenColumns())
       {
-        start = hidden.adjustForHiddenColumns(end);
-        start = hidden.getHiddenBoundaryLeft(start) + 1;
+        start = hidden.visibleToAbsoluteColumn(end);
+        start = hidden.getNextHiddenBoundary(true, start) + 1;
       }
     } while (end < max);
 
@@ -1809,13 +1814,13 @@ public abstract class AlignmentViewport
         AlignmentAnnotation clone = new AlignmentAnnotation(annot);
         if (selectedOnly && selectionGroup != null)
         {
-          alignment.getHiddenColumns().makeVisibleAnnotation(
+          clone.makeVisibleAnnotation(
                   selectionGroup.getStartRes(), selectionGroup.getEndRes(),
-                  clone);
+                  alignment.getHiddenColumns());
         }
         else
         {
-          alignment.getHiddenColumns().makeVisibleAnnotation(clone);
+          clone.makeVisibleAnnotation(alignment.getHiddenColumns());
         }
         ala.add(clone);
       }
@@ -2781,7 +2786,7 @@ public abstract class AlignmentViewport
     int lastSeq = alignment.getHeight() - 1;
     List<AlignedCodonFrame> seqMappings = null;
     for (int seqNo = ranges
-            .getStartSeq(); seqNo < lastSeq; seqNo++, seqOffset++)
+            .getStartSeq(); seqNo <= lastSeq; seqNo++, seqOffset++)
     {
       sequence = getAlignment().getSequenceAt(seqNo);
       if (hiddenSequences != null && hiddenSequences.isHidden(sequence))
@@ -2877,6 +2882,8 @@ public abstract class AlignmentViewport
    */
   private SearchResultsI searchResults = null;
 
+  protected TreeModel currentTree = null;
+
   @Override
   public boolean hasSearchResults()
   {
@@ -2935,4 +2942,16 @@ public abstract class AlignmentViewport
             + ((ignoreGapsInConsensusCalculation) ? " without gaps" : ""));
     return sq;
   }
+
+  @Override
+  public void setCurrentTree(TreeModel tree)
+  {
+    currentTree = tree;
+  }
+
+  @Override
+  public TreeModel getCurrentTree()
+  {
+    return currentTree;
+  }
 }
index 170f4e9..0235081 100644 (file)
@@ -58,6 +58,10 @@ public abstract class OverviewDimensions
 
   protected int alheight;
 
+  protected float widthRatio;
+
+  protected float heightRatio;
+
   /**
    * Create an OverviewDimensions object
    * 
@@ -157,23 +161,25 @@ public abstract class OverviewDimensions
   public float getPixelsPerCol()
   {
     resetAlignmentDims();
-    return (float) width / alwidth;
+    return 1 / widthRatio;
   }
 
   public float getPixelsPerSeq()
   {
     resetAlignmentDims();
-    return (float) sequencesHeight / alheight;
+    return 1 / heightRatio;
   }
 
   public void setWidth(int w)
   {
     width = w;
+    widthRatio = (float) alwidth / width;
   }
 
   public void setHeight(int h)
   {
     sequencesHeight = h - graphHeight;
+    heightRatio = (float) alheight / sequencesHeight;
   }
 
   /**
@@ -273,14 +279,14 @@ public abstract class OverviewDimensions
 
     // boxX, boxY is the x,y location equivalent to startRes, startSeq
     int xPos = Math.min(startRes, alwidth - vpwidth + 1);
-    boxX = Math.round((float) xPos * width / alwidth);
-    boxY = Math.round((float) startSeq * sequencesHeight / alheight);
+    boxX = Math.round(xPos / widthRatio);
+    boxY = Math.round(startSeq / heightRatio);
 
     // boxWidth is the width in residues translated to pixels
-    boxWidth = Math.round((float) vpwidth * width / alwidth);
+    boxWidth = Math.round(vpwidth / widthRatio);
 
     // boxHeight is the height in sequences translated to pixels
-    boxHeight = Math.round((float) vpheight * sequencesHeight / alheight);
+    boxHeight = Math.round(vpheight / heightRatio);
   }
 
   /**
index c525bc6..de90a21 100644 (file)
@@ -50,6 +50,8 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
   public void updateViewportFromMouse(int mousex, int mousey,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     int xAsRes = getLeftXFromCentreX(mousex, hiddenCols);
     int yAsSeq = getTopYFromCentreY(mousey, hiddenSeqs);
 
@@ -61,24 +63,29 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
   public void adjustViewportFromMouse(int mousex, int mousey,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     // calculate translation in pixel terms:
     // get mouse location in viewport coords, add translation in viewport
     // coords, and update viewport as usual
-    int vpx = Math.round((float) mousex * alwidth / width);
-    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
+    int vpx = Math.round(mousex * widthRatio);
+    int vpy = Math.round(mousey * heightRatio);
 
     updateViewportFromTopLeft(vpx + xdiff, vpy + ydiff, hiddenSeqs,
             hiddenCols);
 
   }
 
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh alwidth, alheight and width/height ratios
+   */
   @Override
   protected void updateViewportFromTopLeft(int leftx, int topy,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
     int xAsRes = leftx;
     int yAsSeq = topy;
-    resetAlignmentDims();
 
     if (xAsRes < 0)
     {
@@ -132,9 +139,7 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
       }
     }
 
-    // update viewport
-    ranges.setStartRes(xAsRes);
-    ranges.setStartSeq(yAsSeq);
+    ranges.setStartResAndSeq(xAsRes, yAsSeq);
   }
 
   @Override
@@ -149,7 +154,7 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
   public AlignmentColsCollectionI getColumns(AlignmentI al)
   {
     return new VisibleColsCollection(0,
-            ranges.getAbsoluteAlignmentWidth() - 1, al);
+            ranges.getAbsoluteAlignmentWidth() - 1, al.getHiddenColumns());
   }
 
   @Override
@@ -164,19 +169,30 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
   {
     alwidth = ranges.getVisibleAlignmentWidth();
     alheight = ranges.getVisibleAlignmentHeight();
+
+    widthRatio = (float) alwidth / width;
+    heightRatio = (float) alheight / sequencesHeight;
   }
 
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh widthRatio
+   */
   @Override
   protected int getLeftXFromCentreX(int mousex, HiddenColumns hidden)
   {
-    int vpx = Math.round((float) mousex * alwidth / width);
+    int vpx = Math.round(mousex * widthRatio);
     return vpx - ranges.getViewportWidth() / 2;
   }
 
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh heightRatio
+   */
   @Override
   protected int getTopYFromCentreY(int mousey, HiddenSequences hidden)
   {
-    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
+    int vpy = Math.round(mousey * heightRatio);
     return vpy - ranges.getViewportHeight() / 2;
   }
 
@@ -184,10 +200,12 @@ public class OverviewDimensionsHideHidden extends OverviewDimensions
   public void setDragPoint(int x, int y, HiddenSequences hiddenSeqs,
           HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     // get alignment position of x and box (can get directly from vpranges) and
     // calculate difference between the positions
-    int vpx = Math.round((float) x * alwidth / width);
-    int vpy = Math.round((float) y * alheight / sequencesHeight);
+    int vpx = Math.round(x * widthRatio);
+    int vpy = Math.round(y * heightRatio);
 
     xdiff = ranges.getStartRes() - vpx;
     ydiff = ranges.getStartSeq() - vpy;
index 0bda56e..3aa6e1b 100644 (file)
@@ -72,13 +72,15 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
   public void updateViewportFromMouse(int mousex, int mousey,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     // convert mousex and mousey to alignment units as well as
     // translating to top left corner of viewport - this is an absolute position
     int xAsRes = getLeftXFromCentreX(mousex, hiddenCols);
     int yAsSeq = getTopYFromCentreY(mousey, hiddenSeqs);
 
     // convert to visible positions
-    int visXAsRes = hiddenCols.findColumnPosition(xAsRes);
+    int visXAsRes = hiddenCols.absoluteToVisibleColumn(xAsRes);
     yAsSeq = hiddenSeqs.adjustForHiddenSeqs(
             hiddenSeqs.findIndexWithoutHiddenSeqs(yAsSeq));
     yAsSeq = Math.max(yAsSeq, 0); // -1 if before first visible sequence
@@ -93,27 +95,32 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
   public void adjustViewportFromMouse(int mousex, int mousey,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     // calculate translation in pixel terms:
     // get mouse location in viewport coords, add translation in viewport
     // coords,
     // convert back to pixel coords
     int vpx = Math.round((float) mousex * alwidth / width);
-    int visXAsRes = hiddenCols.findColumnPosition(vpx) + xdiff;
+    int visXAsRes = hiddenCols.absoluteToVisibleColumn(vpx) + xdiff;
 
-    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
+    int vpy = Math.round(mousey * heightRatio);
     int visYAsRes = hiddenSeqs.findIndexWithoutHiddenSeqs(vpy) + ydiff;
 
     // update viewport accordingly
     updateViewportFromTopLeft(visXAsRes, visYAsRes, hiddenSeqs, hiddenCols);
   }
 
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh alwidth, alheight and width/height ratios
+   */
   @Override
   protected void updateViewportFromTopLeft(int leftx, int topy,
           HiddenSequences hiddenSeqs, HiddenColumns hiddenCols)
   {
     int visXAsRes = leftx;
     int visYAsSeq = topy;
-    resetAlignmentDims();
 
     if (visXAsRes < 0)
     {
@@ -136,7 +143,7 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
     int vpwidth = ranges.getViewportWidth();
 
     // check in case we went off the edge of the alignment
-    int visAlignWidth = hiddenCols.findColumnPosition(alwidth - 1);
+    int visAlignWidth = hiddenCols.absoluteToVisibleColumn(alwidth - 1);
     if (visXAsRes + vpwidth - 1 > visAlignWidth)
     {
       // went past the end of the alignment, adjust backwards
@@ -144,8 +151,8 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
       // if last position was before the end of the alignment, need to update
       if (ranges.getEndRes() < visAlignWidth)
       {
-        visXAsRes = hiddenCols.findColumnPosition(hiddenCols
-                .subtractVisibleColumns(vpwidth - 1, alwidth - 1));
+        visXAsRes = hiddenCols.absoluteToVisibleColumn(hiddenCols
+                .offsetByVisibleColumns(-(vpwidth - 1), alwidth - 1));
       }
       else
       {
@@ -176,8 +183,7 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
     }
 
     // update viewport
-    ranges.setStartRes(visXAsRes);
-    ranges.setStartSeq(visYAsSeq);
+    ranges.setStartResAndSeq(visXAsRes, visYAsSeq);
   }
 
   /**
@@ -196,8 +202,8 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
           HiddenColumns hiddenCols)
   {
     // work with absolute values of startRes and endRes
-    int startRes = hiddenCols.adjustForHiddenColumns(ranges.getStartRes());
-    int endRes = hiddenCols.adjustForHiddenColumns(ranges.getEndRes());
+    int startRes = hiddenCols.visibleToAbsoluteColumn(ranges.getStartRes());
+    int endRes = hiddenCols.visibleToAbsoluteColumn(ranges.getEndRes());
 
     // work with absolute values of startSeq and endSeq
     int startSeq = hiddenSeqs.adjustForHiddenSeqs(ranges.getStartSeq());
@@ -226,20 +232,32 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
   {
     alwidth = ranges.getAbsoluteAlignmentWidth();
     alheight = ranges.getAbsoluteAlignmentHeight();
+
+    widthRatio = (float) alwidth / width;
+    heightRatio = (float) alheight / sequencesHeight;
   }
 
+
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh widthRatio
+   */
   @Override
   protected int getLeftXFromCentreX(int mousex, HiddenColumns hidden)
   {
     int vpx = Math.round((float) mousex * alwidth / width);
-    return hidden.subtractVisibleColumns(ranges.getViewportWidth() / 2,
+    return hidden.offsetByVisibleColumns(-ranges.getViewportWidth() / 2,
             vpx);
   }
 
+  /**
+   * {@inheritDoc} Callers should have already called resetAlignmentDims to
+   * refresh heightRatio
+   */
   @Override
   protected int getTopYFromCentreY(int mousey, HiddenSequences hidden)
   {
-    int vpy = Math.round((float) mousey * alheight / sequencesHeight);
+    int vpy = Math.round(mousey * heightRatio);
     return hidden.subtractVisibleRows(ranges.getViewportHeight() / 2, vpy);
   }
 
@@ -247,12 +265,14 @@ public class OverviewDimensionsShowHidden extends OverviewDimensions
   public void setDragPoint(int x, int y, HiddenSequences hiddenSeqs,
           HiddenColumns hiddenCols)
   {
+    resetAlignmentDims();
+
     // get alignment position of x and box (can get directly from vpranges) and
     // calculate difference between the positions
-    int vpx = Math.round((float) x * alwidth / width);
-    int vpy = Math.round((float) y * alheight / sequencesHeight);
+    int vpx = Math.round(x * widthRatio);
+    int vpy = Math.round(y * heightRatio);
 
-    xdiff = ranges.getStartRes() - hiddenCols.findColumnPosition(vpx);
+    xdiff = ranges.getStartRes() - hiddenCols.absoluteToVisibleColumn(vpx);
     ydiff = ranges.getStartSeq()
             - hiddenSeqs.findIndexWithoutHiddenSeqs(vpy);
   }
index 42d490e..691e492 100644 (file)
@@ -24,11 +24,10 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.HiddenColumns;
 
 /**
- * Slightly less embryonic class which: Supplies and updates viewport properties
- * relating to position such as: start and end residues and sequences; ideally
- * will serve hidden columns/rows too. Intention also to support calculations
- * for positioning, scrolling etc. such as finding the middle of the viewport,
- * checking for scrolls off screen
+ * Supplies and updates viewport properties relating to position such as: start
+ * and end residues and sequences; ideally will serve hidden columns/rows too.
+ * Intention also to support calculations for positioning, scrolling etc. such
+ * as finding the middle of the viewport, checking for scrolls off screen
  */
 public class ViewportRanges extends ViewportProperties
 {
@@ -40,6 +39,10 @@ public class ViewportRanges extends ViewportProperties
 
   public static final String ENDSEQ = "endseq";
 
+  public static final String STARTRESANDSEQ = "startresandseq";
+
+  public static final String MOVE_VIEWPORT = "move_viewport";
+
   private boolean wrappedMode = false;
 
   // start residue of viewport
@@ -130,6 +133,31 @@ public class ViewportRanges extends ViewportProperties
    */
   public void setStartEndRes(int start, int end)
   {
+    int[] oldvalues = updateStartEndRes(start, end);
+    int oldstartres = oldvalues[0];
+    int oldendres = oldvalues[1];
+
+    changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
+    if (oldstartres == startRes)
+    {
+      // event won't be fired if start positions are same
+      // fire an event for the end positions in case they changed
+      changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
+    }
+  }
+
+  /**
+   * Update start and end residue values, adjusting for width constraints if
+   * necessary
+   * 
+   * @param start
+   *          start residue
+   * @param end
+   *          end residue
+   * @return array containing old start and end residue values
+   */
+  private int[] updateStartEndRes(int start, int end)
+  {
     int oldstartres = this.startRes;
 
     /*
@@ -162,14 +190,7 @@ public class ViewportRanges extends ViewportProperties
     {
       endRes = end;
     }
-
-    changeSupport.firePropertyChange(STARTRES, oldstartres, startRes);
-    if (oldstartres == startRes)
-    {
-      // event won't be fired if start positions are same
-      // fire an event for the end positions in case they changed
-      changeSupport.firePropertyChange(ENDRES, oldendres, endRes);
-    }
+    return new int[] { oldstartres, oldendres };
   }
 
   /**
@@ -203,6 +224,31 @@ public class ViewportRanges extends ViewportProperties
    */
   public void setStartEndSeq(int start, int end)
   {
+    int[] oldvalues = updateStartEndSeq(start, end);
+    int oldstartseq = oldvalues[0];
+    int oldendseq = oldvalues[1];
+
+    changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
+    if (oldstartseq == startSeq)
+    {
+      // event won't be fired if start positions are the same
+      // fire in case the end positions changed
+      changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
+    }
+  }
+
+  /**
+   * Update start and end sequence values, adjusting for height constraints if
+   * necessary
+   * 
+   * @param start
+   *          start sequence
+   * @param end
+   *          end sequence
+   * @return array containing old start and end sequence values
+   */
+  private int[] updateStartEndSeq(int start, int end)
+  {
     int oldstartseq = this.startSeq;
     int visibleHeight = getVisibleAlignmentHeight();
     if (start > visibleHeight - 1)
@@ -231,14 +277,7 @@ public class ViewportRanges extends ViewportProperties
     {
       endSeq = end;
     }
-
-    changeSupport.firePropertyChange(STARTSEQ, oldstartseq, startSeq);
-    if (oldstartseq == startSeq)
-    {
-      // event won't be fired if start positions are the same
-      // fire in case the end positions changed
-      changeSupport.firePropertyChange(ENDSEQ, oldendseq, endSeq);
-    }
+    return new int[] { oldstartseq, oldendseq };
   }
 
   /**
@@ -255,6 +294,34 @@ public class ViewportRanges extends ViewportProperties
   }
 
   /**
+   * Set start residue and start sequence together (fires single event). The
+   * event supplies a pair of old values and a pair of new values: [old start
+   * residue, old start sequence] and [new start residue, new start sequence]
+   * 
+   * @param res
+   *          the start residue
+   * @param seq
+   *          the start sequence
+   */
+  public void setStartResAndSeq(int res, int seq)
+  {
+    int width = getViewportWidth();
+    int[] oldresvalues = updateStartEndRes(res, res + width - 1);
+
+    int startseq = seq;
+    int height = getViewportHeight();
+    if (startseq + height - 1 > getVisibleAlignmentHeight() - 1)
+    {
+      startseq = getVisibleAlignmentHeight() - height;
+    }
+    int[] oldseqvalues = updateStartEndSeq(startseq, startseq + height - 1);
+
+    int[] old = new int[] { oldresvalues[0], oldseqvalues[0] };
+    int[] newresseq = new int[] { startRes, startSeq };
+    changeSupport.firePropertyChange(STARTRESANDSEQ, old, newresseq);
+  }
+
+  /**
    * Get start residue of viewport
    */
   public int getStartRes()
@@ -402,23 +469,39 @@ public class ViewportRanges extends ViewportProperties
    */
   public boolean scrollUp(boolean up)
   {
+    /*
+     * if in unwrapped mode, scroll up or down one sequence row;
+     * if in wrapped mode, scroll by one visible width of columns
+     */
     if (up)
     {
-      if (startSeq < 1)
+      if (wrappedMode)
       {
-        return false;
+        pageUp();
+      }
+      else
+      {
+        if (startSeq < 1)
+        {
+          return false;
+        }
+        setStartSeq(startSeq - 1);
       }
-
-      setStartSeq(startSeq - 1);
     }
     else
     {
-      if (endSeq >= getVisibleAlignmentHeight() - 1)
+      if (wrappedMode)
       {
-        return false;
+        pageDown();
+      }
+      else
+      {
+        if (endSeq >= getVisibleAlignmentHeight() - 1)
+        {
+          return false;
+        }
+        setStartSeq(startSeq + 1);
       }
-
-      setStartSeq(startSeq + 1);
     }
     return true;
   }
@@ -461,18 +544,33 @@ public class ViewportRanges extends ViewportProperties
    * the startRes changed, else false.
    * 
    * @param res
-   *          residue position to scroll to
+   *          residue position to scroll to NB visible position not absolute
+   *          alignment position
    * @return
    */
   public boolean scrollToWrappedVisible(int res)
   {
-    int oldStartRes = startRes;
-    int width = getViewportWidth();
-
-    if (res >= oldStartRes && res < oldStartRes + width)
+    int newStartRes = calcWrappedStartResidue(res);
+    if (newStartRes == startRes)
     {
       return false;
     }
+    setStartRes(newStartRes);
+
+    return true;
+  }
+
+  /**
+   * Calculate wrapped start residue from visible start residue
+   * 
+   * @param res
+   *          visible start residue
+   * @return left column of panel res will be located in
+   */
+  private int calcWrappedStartResidue(int res)
+  {
+    int oldStartRes = startRes;
+    int width = getViewportWidth();
 
     boolean up = res < oldStartRes;
     int widthsToScroll = Math.abs((res - oldStartRes) / width);
@@ -488,19 +586,16 @@ public class ViewportRanges extends ViewportProperties
     {
       newStartRes = 0;
     }
-
-    setStartRes(newStartRes);
-
-    return true;
+    return newStartRes;
   }
 
   /**
    * Scroll so that (x,y) is visible. Fires a property change event.
    * 
    * @param x
-   *          x position in alignment
+   *          x position in alignment (absolute position)
    * @param y
-   *          y position in alignment
+   *          y position in alignment (absolute position)
    */
   public void scrollToVisible(int x, int y)
   {
@@ -512,16 +607,16 @@ public class ViewportRanges extends ViewportProperties
     {
       scrollUp(false);
     }
-
+    
     HiddenColumns hidden = al.getHiddenColumns();
-    while (x < hidden.adjustForHiddenColumns(startRes))
+    while (x < hidden.visibleToAbsoluteColumn(startRes))
     {
       if (!scrollRight(false))
       {
         break;
       }
     }
-    while (x > hidden.adjustForHiddenColumns(endRes))
+    while (x > hidden.visibleToAbsoluteColumn(endRes))
     {
       if (!scrollRight(true))
       {
@@ -531,6 +626,62 @@ public class ViewportRanges extends ViewportProperties
   }
 
   /**
+   * Set the viewport location so that a position is visible
+   * 
+   * @param x
+   *          column to be visible: absolute position in alignment
+   * @param y
+   *          row to be visible: absolute position in alignment
+   */
+  public boolean setViewportLocation(int x, int y)
+  {
+    boolean changedLocation = false;
+
+    // convert the x,y location to visible coordinates
+    int visX = al.getHiddenColumns().absoluteToVisibleColumn(x);
+    int visY = al.getHiddenSequences().findIndexWithoutHiddenSeqs(y);
+
+    // if (vis_x,vis_y) is already visible don't do anything
+    if (startRes > visX || visX > endRes
+            || startSeq > visY && visY > endSeq)
+    {
+      int[] old = new int[] { startRes, startSeq };
+      int[] newresseq;
+      if (wrappedMode)
+      {
+        int newstartres = calcWrappedStartResidue(visX);
+        setStartRes(newstartres);
+        newresseq = new int[] { startRes, startSeq };
+      }
+      else
+      {
+        // set the viewport x location to contain vis_x
+        int newstartres = visX;
+        int width = getViewportWidth();
+        if (newstartres + width - 1 > getVisibleAlignmentWidth() - 1)
+        {
+          newstartres = getVisibleAlignmentWidth() - width;
+        }
+        updateStartEndRes(newstartres, newstartres + width - 1);
+
+        // set the viewport y location to contain vis_y
+        int newstartseq = visY;
+        int height = getViewportHeight();
+        if (newstartseq + height - 1 > getVisibleAlignmentHeight() - 1)
+        {
+          newstartseq = getVisibleAlignmentHeight() - height;
+        }
+        updateStartEndSeq(newstartseq, newstartseq + height - 1);
+
+        newresseq = new int[] { startRes, startSeq };
+      }
+      changedLocation = true;
+      changeSupport.firePropertyChange(MOVE_VIEWPORT, old, newresseq);
+    }
+    return changedLocation;
+  }
+
+  /**
    * Adjust sequence position for page up. Fires a property change event.
    */
   public void pageUp()
index 2f30e94..553f813 100644 (file)
@@ -26,6 +26,7 @@ import jalview.api.FeaturesDisplayedI;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.renderer.seqfeatures.FeatureRenderer;
 import jalview.schemes.FeatureColour;
@@ -48,15 +49,48 @@ import java.util.concurrent.ConcurrentHashMap;
 public abstract class FeatureRendererModel
         implements jalview.api.FeatureRenderer
 {
+  /*
+   * a data bean to hold one row of feature settings from the gui
+   */
+  public static class FeatureSettingsBean
+  {
+    public final String featureType;
 
-  /**
+    public final FeatureColourI featureColour;
+
+    public final FeatureMatcherSetI filter;
+
+    public final Boolean show;
+
+    public FeatureSettingsBean(String type, FeatureColourI colour,
+            FeatureMatcherSetI theFilter, Boolean isShown)
+    {
+      featureType = type;
+      featureColour = colour;
+      filter = theFilter;
+      show = isShown;
+    }
+  }
+
+  /*
    * global transparency for feature
    */
   protected float transparency = 1.0f;
 
-  protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+  /*
+   * colour scheme for each feature type
+   */
+  protected Map<String, FeatureColourI> featureColours = new ConcurrentHashMap<>();
 
-  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<String, Boolean>();
+  /*
+   * visibility flag for each feature group
+   */
+  protected Map<String, Boolean> featureGroups = new ConcurrentHashMap<>();
+
+  /*
+   * filters for each feature type
+   */
+  protected Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
 
   protected String[] renderOrder;
 
@@ -100,6 +134,7 @@ public abstract class FeatureRendererModel
     this.renderOrder = frs.renderOrder;
     this.featureGroups = frs.featureGroups;
     this.featureColours = frs.featureColours;
+    this.featureFilters = frs.featureFilters;
     this.transparency = frs.transparency;
     this.featureOrder = frs.featureOrder;
     if (av != null && av != fr.getViewport())
@@ -156,7 +191,7 @@ public abstract class FeatureRendererModel
     {
       av.setFeaturesDisplayed(fdi = new FeaturesDisplayed());
     }
-    List<String> nft = new ArrayList<String>();
+    List<String> nft = new ArrayList<>();
     for (String featureType : featureTypes)
     {
       if (!fdi.isRegistered(featureType))
@@ -192,7 +227,7 @@ public abstract class FeatureRendererModel
     renderOrder = neworder;
   }
 
-  protected Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+  protected Map<String, float[][]> minmax = new Hashtable<>();
 
   public Map<String, float[][]> getMinMax()
   {
@@ -271,7 +306,7 @@ public abstract class FeatureRendererModel
      * include features at the position provided their feature type is 
      * displayed, and feature group is null or marked for display
      */
-    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> result = new ArrayList<>();
     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
     {
       return result;
@@ -284,9 +319,13 @@ public abstract class FeatureRendererModel
     List<SequenceFeature> features = sequence.findFeatures(column, column,
             visibleTypes);
 
+    /*
+     * include features unless their feature group is not displayed, or
+     * they are hidden (have no colour) based on a filter or colour threshold
+     */
     for (SequenceFeature sf : features)
     {
-      if (!featureGroupNotShown(sf))
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
         result.add(sf);
       }
@@ -320,7 +359,7 @@ public abstract class FeatureRendererModel
     }
     FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed();
 
-    Set<String> oldfeatures = new HashSet<String>();
+    Set<String> oldfeatures = new HashSet<>();
     if (renderOrder != null)
     {
       for (int i = 0; i < renderOrder.length; i++)
@@ -333,7 +372,7 @@ public abstract class FeatureRendererModel
     }
 
     AlignmentI alignment = av.getAlignment();
-    List<String> allfeatures = new ArrayList<String>();
+    List<String> allfeatures = new ArrayList<>();
 
     for (int i = 0; i < alignment.getHeight(); i++)
     {
@@ -413,7 +452,7 @@ public abstract class FeatureRendererModel
      */
     if (minmax == null)
     {
-      minmax = new Hashtable<String, float[][]>();
+      minmax = new Hashtable<>();
     }
     synchronized (minmax)
     {
@@ -450,7 +489,7 @@ public abstract class FeatureRendererModel
    */
   private void updateRenderOrder(List<String> allFeatures)
   {
-    List<String> allfeatures = new ArrayList<String>(allFeatures);
+    List<String> allfeatures = new ArrayList<>(allFeatures);
     String[] oldRender = renderOrder;
     renderOrder = new String[allfeatures.size()];
     boolean initOrders = (featureOrder == null);
@@ -477,7 +516,8 @@ public abstract class FeatureRendererModel
               if (mmrange != null)
               {
                 FeatureColourI fc = featureColours.get(oldRender[j]);
-                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+                if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                        && !fc.isColourByAttribute())
                 {
                   fc.updateBounds(mmrange[0][0], mmrange[0][1]);
                 }
@@ -507,7 +547,8 @@ public abstract class FeatureRendererModel
         if (mmrange != null)
         {
           FeatureColourI fc = featureColours.get(newf[i]);
-          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled())
+          if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()
+                  && !fc.isColourByAttribute())
           {
             fc.updateBounds(mmrange[0][0], mmrange[0][1]);
           }
@@ -557,20 +598,11 @@ public abstract class FeatureRendererModel
     return fc;
   }
 
-  /**
-   * Returns the configured colour for a particular feature instance. This
-   * includes calculation of 'colour by label', or of a graduated score colour,
-   * if applicable. It does not take into account feature visibility or colour
-   * transparency. Returns null for a score feature whose score value lies
-   * outside any colour threshold.
-   * 
-   * @param feature
-   * @return
-   */
+  @Override
   public Color getColour(SequenceFeature feature)
   {
     FeatureColourI fc = getFeatureStyle(feature.getType());
-    return fc.getColor(feature);
+    return getColor(feature, fc);
   }
 
   /**
@@ -582,7 +614,8 @@ public abstract class FeatureRendererModel
    */
   protected boolean showFeatureOfType(String type)
   {
-    return type == null ? false : av.getFeaturesDisplayed().isVisible(type);
+    return type == null ? false : (av.getFeaturesDisplayed() == null ? true
+            : av.getFeaturesDisplayed().isVisible(type));
   }
 
   @Override
@@ -617,7 +650,7 @@ public abstract class FeatureRendererModel
   {
     if (featureOrder == null)
     {
-      featureOrder = new Hashtable<String, Float>();
+      featureOrder = new Hashtable<>();
     }
     featureOrder.put(type, new Float(position));
     return position;
@@ -651,32 +684,33 @@ public abstract class FeatureRendererModel
    * Replace current ordering with new ordering
    * 
    * @param data
-   *          { String(Type), Colour(Type), Boolean(Displayed) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @return true if any visible features have been reordered, else false
    */
-  public boolean setFeaturePriority(Object[][] data)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data)
   {
     return setFeaturePriority(data, true);
   }
 
   /**
-   * Sets the priority order for features, with the highest priority (displayed
-   * on top) at the start of the data array
+   * Sets the priority order for features, with the highest priority (displayed on
+   * top) at the start of the data array
    * 
    * @param data
-   *          { String(Type), Colour(Type), Boolean(Displayed) }
+   *          an array of { Type, Colour, Filter, Boolean }
    * @param visibleNew
    *          when true current featureDisplay list will be cleared
-   * @return true if any visible features have been reordered or recoloured,
-   *         else false (i.e. no need to repaint)
+   * @return true if any visible features have been reordered or recoloured, else
+   *         false (i.e. no need to repaint)
    */
-  public boolean setFeaturePriority(Object[][] data, boolean visibleNew)
+  public boolean setFeaturePriority(FeatureSettingsBean[] data,
+          boolean visibleNew)
   {
     /*
      * note visible feature ordering and colours before update
      */
     List<String> visibleFeatures = getDisplayedFeatureTypes();
-    Map<String, FeatureColourI> visibleColours = new HashMap<String, FeatureColourI>(
+    Map<String, FeatureColourI> visibleColours = new HashMap<>(
             getFeatureColours());
 
     FeaturesDisplayedI av_featuresdisplayed = null;
@@ -709,9 +743,9 @@ public abstract class FeatureRendererModel
     {
       for (int i = 0; i < data.length; i++)
       {
-        String type = data[i][0].toString();
-        setColour(type, (FeatureColourI) data[i][1]);
-        if (((Boolean) data[i][2]).booleanValue())
+        String type = data[i].featureType;
+        setColour(type, data[i].featureColour);
+        if (data[i].show)
         {
           av_featuresdisplayed.setVisible(type);
         }
@@ -836,7 +870,7 @@ public abstract class FeatureRendererModel
   {
     if (featureGroups != null)
     {
-      List<String> gp = new ArrayList<String>();
+      List<String> gp = new ArrayList<>();
 
       for (String grp : featureGroups.keySet())
       {
@@ -882,7 +916,7 @@ public abstract class FeatureRendererModel
   @Override
   public Map<String, FeatureColourI> getDisplayedFeatureCols()
   {
-    Map<String, FeatureColourI> fcols = new Hashtable<String, FeatureColourI>();
+    Map<String, FeatureColourI> fcols = new Hashtable<>();
     if (getViewport().getFeaturesDisplayed() == null)
     {
       return fcols;
@@ -910,7 +944,7 @@ public abstract class FeatureRendererModel
   public List<String> getDisplayedFeatureTypes()
   {
     List<String> typ = getRenderOrder();
-    List<String> displayed = new ArrayList<String>();
+    List<String> displayed = new ArrayList<>();
     FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed();
     if (feature_disp != null)
     {
@@ -931,7 +965,7 @@ public abstract class FeatureRendererModel
   @Override
   public List<String> getDisplayedFeatureGroups()
   {
-    List<String> _gps = new ArrayList<String>();
+    List<String> _gps = new ArrayList<>();
     for (String gp : getFeatureGroups())
     {
       if (checkGroupVisibility(gp, false))
@@ -966,7 +1000,7 @@ public abstract class FeatureRendererModel
   public List<SequenceFeature> findFeaturesAtResidue(SequenceI sequence,
           int resNo)
   {
-    List<SequenceFeature> result = new ArrayList<SequenceFeature>();
+    List<SequenceFeature> result = new ArrayList<>();
     if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null)
     {
       return result;
@@ -986,7 +1020,7 @@ public abstract class FeatureRendererModel
   
     for (SequenceFeature sf : features)
     {
-      if (!featureGroupNotShown(sf))
+      if (!featureGroupNotShown(sf) && getColour(sf) != null)
       {
         result.add(sf);
       }
@@ -995,35 +1029,34 @@ public abstract class FeatureRendererModel
   }
 
   /**
-   * Removes from the list of features any that have a feature group that is not
-   * displayed, or duplicate the location of a feature of the same type (unless
-   * a graduated colour scheme or colour by label is applied). Should be used
-   * only for features of the same feature colour (which normally implies the
-   * same feature type).
+   * Removes from the list of features any that duplicate the location of a
+   * feature of the same type. Should be used only for features of the same,
+   * simple, feature colour (which normally implies the same feature type). Does
+   * not check visibility settings for feature type or feature group. No
+   * filtering is done if transparency, or any feature filters, are in force.
    * 
    * @param features
-   * @param fc
    */
-  public void filterFeaturesForDisplay(List<SequenceFeature> features,
-          FeatureColourI fc)
+  public void filterFeaturesForDisplay(List<SequenceFeature> features)
   {
-    if (features.isEmpty())
+    /*
+     * don't remove 'redundant' features if 
+     * - transparency is applied (feature count affects depth of feature colour)
+     * - filters are applied (not all features may be displayable)
+     */
+    if (features.isEmpty() || transparency != 1f
+            || !featureFilters.isEmpty())
     {
       return;
     }
+
     SequenceFeatures.sortFeatures(features, true);
-    boolean simpleColour = fc == null || fc.isSimpleColour();
     SequenceFeature lastFeature = null;
 
     Iterator<SequenceFeature> it = features.iterator();
     while (it.hasNext())
     {
       SequenceFeature sf = it.next();
-      if (featureGroupNotShown(sf))
-      {
-        it.remove();
-        continue;
-      }
 
       /*
        * a feature is redundant for rendering purposes if it has the
@@ -1031,18 +1064,90 @@ public abstract class FeatureRendererModel
        * (checking type and isContactFeature as a fail-safe here, although
        * currently they are guaranteed to match in this context)
        */
-      if (simpleColour)
+      if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
+              && sf.getEnd() == lastFeature.getEnd()
+              && sf.isContactFeature() == lastFeature.isContactFeature()
+              && sf.getType().equals(lastFeature.getType()))
       {
-        if (lastFeature != null && sf.getBegin() == lastFeature.getBegin()
-                && sf.getEnd() == lastFeature.getEnd()
-                && sf.isContactFeature() == lastFeature.isContactFeature()
-                && sf.getType().equals(lastFeature.getType()))
-        {
-          it.remove();
-        }
+        it.remove();
       }
       lastFeature = sf;
     }
   }
 
+  @Override
+  public Map<String, FeatureMatcherSetI> getFeatureFilters()
+  {
+    return featureFilters;
+  }
+
+  @Override
+  public void setFeatureFilters(Map<String, FeatureMatcherSetI> filters)
+  {
+    featureFilters = filters;
+  }
+
+  @Override
+  public FeatureMatcherSetI getFeatureFilter(String featureType)
+  {
+    return featureFilters.get(featureType);
+  }
+
+  @Override
+  public void setFeatureFilter(String featureType, FeatureMatcherSetI filter)
+  {
+    if (filter == null || filter.isEmpty())
+    {
+      featureFilters.remove(featureType);
+    }
+    else
+    {
+      featureFilters.put(featureType, filter);
+    }
+  }
+
+  /**
+   * Answers the colour for the feature, or null if the feature is excluded by
+   * feature group visibility, by filters, or by colour threshold settings. This
+   * method does not take feature visibility into account.
+   * 
+   * @param sf
+   * @param fc
+   * @return
+   */
+  public Color getColor(SequenceFeature sf, FeatureColourI fc)
+  {
+    /*
+     * is the feature group displayed?
+     */
+    if (featureGroupNotShown(sf))
+    {
+      return null;
+    }
+
+    /*
+     * does the feature pass filters?
+     */
+    if (!featureMatchesFilters(sf))
+    {
+      return null;
+    }
+  
+    return fc.getColor(sf);
+  }
+
+  /**
+   * Answers true if there no are filters defined for the feature type, or this
+   * feature matches the filters. Answers false if the feature fails to match
+   * filters.
+   * 
+   * @param sf
+   * @return
+   */
+  protected boolean featureMatchesFilters(SequenceFeature sf)
+  {
+    FeatureMatcherSetI filter = featureFilters.get(sf.getType());
+    return filter == null ? true : filter.matches(sf);
+  }
+
 }
index dc2ae11..f594453 100644 (file)
 package jalview.viewmodel.seqfeatures;
 
 import jalview.api.FeatureColourI;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.schemes.FeatureColour;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -42,6 +44,11 @@ public class FeatureRendererSettings implements Cloneable
    */
   Map<String, FeatureColourI> featureColours;
 
+  /*
+   * map of {featureType, filters}
+   */
+  Map<String, FeatureMatcherSetI> featureFilters;
+
   float transparency;
 
   Map<String, Float> featureOrder;
@@ -72,7 +79,9 @@ public class FeatureRendererSettings implements Cloneable
     renderOrder = null;
     featureGroups = new ConcurrentHashMap<String, Boolean>();
     featureColours = new ConcurrentHashMap<String, FeatureColourI>();
+    featureFilters = new HashMap<>();
     featureOrder = new ConcurrentHashMap<String, Float>();
+
     if (fr.renderOrder != null)
     {
       this.renderOrder = new String[fr.renderOrder.length];
@@ -100,6 +109,12 @@ public class FeatureRendererSettings implements Cloneable
         featureColours.put(next, new FeatureColour((FeatureColour) val));
       }
     }
+
+    if (fr.featureFilters != null)
+    {
+      this.featureFilters.putAll(fr.featureFilters);
+    }
+
     this.transparency = fr.transparency;
     if (fr.featureOrder != null)
     {
index 8569039..8f37f15 100644 (file)
@@ -51,7 +51,7 @@ class AnnotationWorker extends AlignCalcWorker
           AnnotationProviderI counter)
   {
     super(viewport, panel);
-    ourAnnots = new ArrayList<AlignmentAnnotation>();
+    ourAnnots = new ArrayList<>();
     this.counter = counter;
     calcMan.registerWorker(this);
   }
@@ -121,7 +121,10 @@ class AnnotationWorker extends AlignCalcWorker
     if (ap != null)
     {
       ap.adjustAnnotationHeight();
-      ap.paintAlignment(true);
+      // TODO: only need to update colour and geometry if panel height changes
+      // and view is coloured by annotation, and the annotation is actually
+      // changed!
+      ap.paintAlignment(true, true);
     }
   }
 
index 24cb717..74695fe 100644 (file)
@@ -57,7 +57,7 @@ class ColumnCounterSetWorker extends AlignCalcWorker
           AlignmentViewPanel panel, FeatureSetCounterI counter)
   {
     super(viewport, panel);
-    ourAnnots = new ArrayList<AlignmentAnnotation>();
+    ourAnnots = new ArrayList<>();
     this.counter = counter;
     calcMan.registerWorker(this);
   }
@@ -116,7 +116,7 @@ class ColumnCounterSetWorker extends AlignCalcWorker
       {
         ap.adjustAnnotationHeight();
       }
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
 
   }
index 4242b2a..335529c 100644 (file)
@@ -64,7 +64,7 @@ public class ConsensusThread extends AlignCalcWorker
         {
           if (ap != null)
           {
-            ap.paintAlignment(false);
+            ap.paintAlignment(false, false);
           }
           Thread.sleep(200);
         } catch (Exception ex)
@@ -93,7 +93,7 @@ public class ConsensusThread extends AlignCalcWorker
 
       if (ap != null)
       {
-        ap.paintAlignment(true);
+        ap.paintAlignment(true, true);
       }
     } catch (OutOfMemoryError error)
     {
index 571234c..54b0191 100644 (file)
@@ -75,7 +75,7 @@ public class ConservationThread extends AlignCalcWorker
         abortAndDestroy();
         return;
       }
-      List<AlignmentAnnotation> ourAnnot = new ArrayList<AlignmentAnnotation>();
+      List<AlignmentAnnotation> ourAnnot = new ArrayList<>();
       AlignmentI alignment = alignViewport.getAlignment();
       conservation = alignViewport.getAlignmentConservationAnnotation();
       quality = alignViewport.getAlignmentQualityAnnot();
@@ -123,7 +123,7 @@ public class ConservationThread extends AlignCalcWorker
     }
     if (ap != null)
     {
-      ap.paintAlignment(true);
+      ap.paintAlignment(true, true);
     }
 
   }
index 5ed2885..61ec3d0 100644 (file)
@@ -139,7 +139,7 @@ public class StrucConsensusThread extends AlignCalcWorker
       calcMan.workerComplete(this);
       if (ap != null)
       {
-        ap.paintAlignment(true);
+        ap.paintAlignment(true, true);
       }
     }
 
index fb8864d..1677eca 100644 (file)
@@ -61,6 +61,8 @@ public class DBRefFetcher implements Runnable
 {
   private static final String NEWLINE = System.lineSeparator();
 
+  public static final String TRIM_RETRIEVED_SEQUENCES = "TRIM_FETCHED_DATASET_SEQS";
+
   public interface FetchFinishedListenerI
   {
     void finished();
@@ -139,7 +141,7 @@ public class DBRefFetcher implements Runnable
             .getSequenceFetcherSingleton(progressIndicatorFrame);
     // set default behaviour for transferring excess sequence data to the
     // dataset
-    trimDsSeqs = Cache.getDefault("TRIM_FETCHED_DATASET_SEQS", true);
+    trimDsSeqs = Cache.getDefault(TRIM_RETRIEVED_SEQUENCES, true);
     if (sources == null)
     {
       setDatabaseSources(featureSettings, isNucleotide);
index 0a61dff..c661e2c 100644 (file)
@@ -135,7 +135,7 @@ public class DasSequenceFeatureFetcher
           boolean useJDasMultiThread)
   {
     this.useJDASMultiThread = useJDasMultiThread;
-    this.selectedSources = new ArrayList<jalviewSourceI>();
+    this.selectedSources = new ArrayList<>();
     // filter both sequences and sources to eliminate duplicates
     for (jalviewSourceI src : selectedSources2)
     {
@@ -316,17 +316,17 @@ public class DasSequenceFeatureFetcher
     FeaturesClientMultipleSources fc = new FeaturesClientMultipleSources();
     fc.setConnProps(sourceRegistry.getSessionHandler());
     // Now sending requests one at a time to each server
-    ArrayList<jalviewSourceI> srcobj = new ArrayList<jalviewSourceI>();
-    ArrayList<String> src = new ArrayList<String>();
-    List<List<String>> ids = new ArrayList<List<String>>();
-    List<List<DBRefEntry>> idobj = new ArrayList<List<DBRefEntry>>();
-    List<Map<String, SequenceI>> sqset = new ArrayList<Map<String, SequenceI>>();
+    ArrayList<jalviewSourceI> srcobj = new ArrayList<>();
+    ArrayList<String> src = new ArrayList<>();
+    List<List<String>> ids = new ArrayList<>();
+    List<List<DBRefEntry>> idobj = new ArrayList<>();
+    List<Map<String, SequenceI>> sqset = new ArrayList<>();
     for (jalviewSourceI _sr : selectedSources)
     {
 
-      Map<String, SequenceI> slist = new HashMap<String, SequenceI>();
-      List<DBRefEntry> idob = new ArrayList<DBRefEntry>();
-      List<String> qset = new ArrayList<String>();
+      Map<String, SequenceI> slist = new HashMap<>();
+      List<DBRefEntry> idob = new ArrayList<>();
+      List<String> qset = new ArrayList<>();
 
       for (SequenceI seq : sequences)
       {
@@ -368,8 +368,8 @@ public class DasSequenceFeatureFetcher
         sqset.add(slist);
       }
     }
-    Map<String, Map<List<String>, Exception>> errors = new HashMap<String, Map<List<String>, Exception>>();
-    Map<String, Map<List<String>, DasGFFAdapter>> results = new HashMap<String, Map<List<String>, DasGFFAdapter>>();
+    Map<String, Map<List<String>, Exception>> errors = new HashMap<>();
+    Map<String, Map<List<String>, DasGFFAdapter>> results = new HashMap<>();
     if (!useJDASMultiThread)
     {
       Iterator<String> sources = src.iterator();
@@ -390,7 +390,7 @@ public class DasSequenceFeatureFetcher
             if (ers == null)
             {
               results.put(source,
-                      ers = new HashMap<List<String>, DasGFFAdapter>());
+                      ers = new HashMap<>());
             }
             ers.put(qid, dga);
           } catch (Exception ex)
@@ -399,7 +399,7 @@ public class DasSequenceFeatureFetcher
             if (ers == null)
             {
               errors.put(source,
-                      ers = new HashMap<List<String>, Exception>());
+                      ers = new HashMap<>());
             }
             ers.put(qid, ex);
           }
@@ -438,7 +438,7 @@ public class DasSequenceFeatureFetcher
           Map<List<String>, DasGFFAdapter> results,
           Map<List<String>, Exception> errors)
   {
-    Set<SequenceI> sequences = new HashSet<SequenceI>();
+    Set<SequenceI> sequences = new HashSet<>();
     String source = jvsource.getSourceURL();
     // process features
     DasGFFAdapter result = (results == null) ? null : results.get(ids);
@@ -622,7 +622,7 @@ public class DasSequenceFeatureFetcher
         if (seq == af.getViewport().getAlignment().getSequenceAt(index)
                 .getDatasetSequence())
         {
-          af.alignPanel.paintAlignment(true);
+          af.alignPanel.paintAlignment(true, true);
           index = end;
           break;
         }
@@ -647,8 +647,8 @@ public class DasSequenceFeatureFetcher
     // TODO: minimal list of DAS queries to make by querying with untyped ID if
     // distinct from any typed IDs
 
-    List<DBRefEntry> ids = new ArrayList<DBRefEntry>();
-    List<String> qstring = new ArrayList<String>();
+    List<DBRefEntry> ids = new ArrayList<>();
+    List<String> qstring = new ArrayList<>();
     boolean dasCoordSysFound = false;
 
     if (uprefs != null)
index 0227e35..8877c34 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.ws.dbsources;
 
+import jalview.bin.Cache;
 import jalview.datamodel.DBRefSource;
 
 import com.stevesoft.pat.Regex;
@@ -34,6 +35,9 @@ import com.stevesoft.pat.Regex;
  */
 abstract public class Pfam extends Xfam
 {
+  static final String PFAM_BASEURL_KEY = "PFAM_BASEURL";
+
+  private static final String DEFAULT_PFAM_BASEURL = "https://pfam.xfam.org";
 
   public Pfam()
   {
@@ -48,7 +52,6 @@ abstract public class Pfam extends Xfam
   @Override
   public String getAccessionSeparator()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
@@ -60,7 +63,6 @@ abstract public class Pfam extends Xfam
   @Override
   public Regex getAccessionValidator()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
@@ -91,17 +93,14 @@ abstract public class Pfam extends Xfam
   @Override
   public String getDbVersion()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
-  /**
-   * Returns base URL for selected Pfam alignment type
-   * 
-   * @return PFAM URL stub for this DbSource
-   */
   @Override
-  protected abstract String getXFAMURL();
+  protected String getURLPrefix()
+  {
+    return Cache.getDefault(PFAM_BASEURL_KEY, DEFAULT_PFAM_BASEURL);
+  }
 
   /*
    * (non-Javadoc)
index ec9fcbb..0600427 100644 (file)
@@ -31,20 +31,8 @@ public class PfamFull extends Pfam
     super();
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.ws.dbsources.Pfam#getPFAMURL()
-   */
-  @Override
-  protected String getXFAMURL()
-  {
-    return "http://pfam.xfam.org/family/";
-
-  }
-
   @Override
-  public String getXFAMURLSUFFIX()
+  public String getURLSuffix()
   {
     return "/alignment/full";
   }
index 33c39b1..dff8a17 100644 (file)
@@ -33,19 +33,8 @@ public class PfamSeed extends Pfam
     super();
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.ws.dbsources.Pfam#getPFAMURL()
-   */
-  @Override
-  protected String getXFAMURL()
-  {
-    return "http://pfam.xfam.org/family/";
-  }
-
   @Override
-  public String getXFAMURLSUFFIX()
+  public String getURLSuffix()
   {
     return "/alignment/seed";
   }
index 97f73d0..1d9d99a 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.ws.dbsources;
 
+import jalview.bin.Cache;
 import jalview.datamodel.DBRefSource;
 
 import com.stevesoft.pat.Regex;
@@ -31,6 +32,15 @@ import com.stevesoft.pat.Regex;
  */
 abstract public class Rfam extends Xfam
 {
+  static final String RFAM_BASEURL_KEY = "RFAM_BASEURL";
+
+  private static final String DEFAULT_RFAM_BASEURL = "https://rfam.xfam.org";
+
+  @Override
+  protected String getURLPrefix()
+  {
+    return Cache.getDefault(RFAM_BASEURL_KEY, DEFAULT_RFAM_BASEURL);
+  }
 
   public Rfam()
   {
@@ -46,7 +56,6 @@ abstract public class Rfam extends Xfam
   @Override
   public String getAccessionSeparator()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
@@ -58,7 +67,6 @@ abstract public class Rfam extends Xfam
   @Override
   public Regex getAccessionValidator()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
@@ -82,18 +90,9 @@ abstract public class Rfam extends Xfam
   @Override
   public String getDbVersion()
   {
-    // TODO Auto-generated method stub
     return null;
   }
 
-  /**
-   * Returns base URL for selected Rfam alignment type
-   * 
-   * @return RFAM URL stub for this DbSource
-   */
-  @Override
-  protected abstract String getXFAMURL();
-
   /*
    * (non-Javadoc)
    * 
index b2ca31a..d815336 100644 (file)
@@ -33,20 +33,8 @@ public class RfamFull extends Rfam
     super();
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.ws.dbsources.Rfam#getXFAMURL()
-   */
-  @Override
-  protected String getXFAMURL()
-  {
-    return "http://rfam.xfam.org/family/";
-
-  }
-
   @Override
-  public String getXFAMURLSUFFIX()
+  public String getURLSuffix()
   {
     return "/alignment/full";
   }
index f714547..a74e829 100644 (file)
@@ -33,19 +33,8 @@ public class RfamSeed extends Rfam
     super();
   }
 
-  /*
-   * (non-Javadoc)
-   * 
-   * @see jalview.ws.dbsources.Rfam#getRFAMURL()
-   */
-  @Override
-  protected String getXFAMURL()
-  {
-    return "http://rfam.xfam.org/family/";
-  }
-
   @Override
-  public String getXFAMURLSUFFIX()
+  public String getURLSuffix()
   {
     // to download gzipped file add '?gzip=1'
     return "/alignment/stockholm";
index c868576..6b09eb6 100644 (file)
@@ -20,6 +20,7 @@
  */
 package jalview.ws.dbsources;
 
+import jalview.bin.Cache;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.DBRefEntry;
@@ -31,13 +32,13 @@ import jalview.datamodel.SequenceI;
 import jalview.datamodel.xdb.uniprot.UniprotEntry;
 import jalview.datamodel.xdb.uniprot.UniprotFeature;
 import jalview.datamodel.xdb.uniprot.UniprotFile;
-import jalview.ws.ebi.EBIFetchClient;
 import jalview.ws.seqfetcher.DbSourceProxyImpl;
 
-import java.io.File;
-import java.io.FileReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.Reader;
 import java.net.URL;
+import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.Vector;
 
@@ -52,6 +53,8 @@ import com.stevesoft.pat.Regex;
  */
 public class Uniprot extends DbSourceProxyImpl
 {
+  private static final String DEFAULT_UNIPROT_DOMAIN = "https://www.uniprot.org";
+
   private static final String BAR_DELIMITER = "|";
 
   /*
@@ -67,6 +70,11 @@ public class Uniprot extends DbSourceProxyImpl
     super();
   }
 
+  private String getDomain()
+  {
+    return Cache.getDefault("UNIPROT_DOMAIN", DEFAULT_UNIPROT_DOMAIN);
+  }
+
   /*
    * (non-Javadoc)
    * 
@@ -162,17 +170,21 @@ public class Uniprot extends DbSourceProxyImpl
       queries = queries.toUpperCase().replaceAll(
               "(UNIPROT\\|?|UNIPROT_|UNIREF\\d+_|UNIREF\\d+\\|?)", "");
       AlignmentI al = null;
-      EBIFetchClient ebi = new EBIFetchClient();
-      // uniprotxml parameter required since december 2007
-      // uniprotkb dbname changed introduced december 2008
-      File file = ebi.fetchDataAsFile("uniprotkb:" + queries, "uniprotxml",
-              "xml");
+
+      String downloadstring = getDomain() + "/uniprot/" + queries
+              + ".xml";
+      URL url = null;
+      URLConnection urlconn = null;
+
+      url = new URL(downloadstring);
+      urlconn = url.openConnection();
+      InputStream istr = urlconn.getInputStream();
       Vector<UniprotEntry> entries = getUniprotEntries(
-              new FileReader(file));
+              new InputStreamReader(istr, "UTF-8"));
 
       if (entries != null)
       {
-        ArrayList<SequenceI> seqs = new ArrayList<SequenceI>();
+        ArrayList<SequenceI> seqs = new ArrayList<>();
         for (UniprotEntry entry : entries)
         {
           seqs.add(uniprotEntryToSequenceI(entry));
@@ -184,8 +196,10 @@ public class Uniprot extends DbSourceProxyImpl
       return al;
     } catch (Exception e)
     {
-      stopQuery();
       throw (e);
+    } finally
+    {
+      stopQuery();
     }
   }
 
@@ -203,7 +217,7 @@ public class Uniprot extends DbSourceProxyImpl
     sequence.setDescription(getUniprotEntryDescription(entry));
 
     final String dbVersion = getDbVersion();
-    ArrayList<DBRefEntry> dbRefs = new ArrayList<DBRefEntry>();
+    ArrayList<DBRefEntry> dbRefs = new ArrayList<>();
     for (String accessionId : entry.getAccession())
     {
       DBRefEntry dbRef = new DBRefEntry(DBRefSource.UNIPROT, dbVersion,
@@ -213,7 +227,7 @@ public class Uniprot extends DbSourceProxyImpl
       dbRefs.add(dbRef);
     }
 
-    Vector<PDBEntry> onlyPdbEntries = new Vector<PDBEntry>();
+    Vector<PDBEntry> onlyPdbEntries = new Vector<>();
     for (PDBEntry pdb : entry.getDbReference())
     {
       DBRefEntry dbr = new DBRefEntry();
@@ -304,23 +318,18 @@ public class Uniprot extends DbSourceProxyImpl
   /**
    *
    * @param entry
-   *          UniportEntry
+   *          UniprotEntry
    * @return The accession id(s) and name(s) delimited by '|'.
    */
   public static String getUniprotEntryId(UniprotEntry entry)
   {
     StringBuilder name = new StringBuilder(32);
-    // name.append("UniProt/Swiss-Prot");
-    // use 'canonicalised' name for optimal id matching
-    name.append(DBRefSource.UNIPROT);
-    for (String accessionId : entry.getAccession())
-    {
-      name.append(BAR_DELIMITER);
-      name.append(accessionId);
-    }
     for (String n : entry.getName())
     {
-      name.append(BAR_DELIMITER);
+      if (name.length() > 0)
+      {
+        name.append(BAR_DELIMITER);
+      }
       name.append(n);
     }
     return name.toString();
index 26291eb..b83f558 100644 (file)
@@ -42,7 +42,12 @@ public abstract class Xfam extends DbSourceProxyImpl
     super();
   }
 
-  protected abstract String getXFAMURL();
+  /**
+   * the base URL for this Xfam-like service
+   * 
+   * @return
+   */
+  protected abstract String getURLPrefix();
 
   @Override
   public abstract String getDbVersion();
@@ -57,8 +62,7 @@ public abstract class Xfam extends DbSourceProxyImpl
     // retrieved.
     startQuery();
     // TODO: trap HTTP 404 exceptions and return null
-    String xfamUrl = getXFAMURL() + queries.trim().toUpperCase()
-            + getXFAMURLSUFFIX();
+    String xfamUrl = getURL(queries);
 
     if (Cache.log != null)
     {
@@ -83,6 +87,12 @@ public abstract class Xfam extends DbSourceProxyImpl
     return rcds;
   }
 
+  String getURL(String queries)
+  {
+    return getURLPrefix() + "/family/" + queries.trim().toUpperCase()
+            + getURLSuffix();
+  }
+
   /**
    * Pfam and Rfam provide alignments
    */
@@ -97,7 +107,7 @@ public abstract class Xfam extends DbSourceProxyImpl
    * 
    * @return "" for most Xfam sources
    */
-  public String getXFAMURLSUFFIX()
+  public String getURLSuffix()
   {
     return "";
   }
index 3e8c55e..07a9df4 100644 (file)
@@ -29,6 +29,7 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
@@ -205,7 +206,14 @@ public class EBIFetchClient
     {
       URL rcall = new URL(url);
 
-      InputStream is = new BufferedInputStream(rcall.openStream());
+      HttpURLConnection conn = (HttpURLConnection) rcall.openConnection();
+      int responseCode = conn.getResponseCode();
+      if (responseCode != 200)
+      {
+        System.err.println("Warning: response code " + responseCode
+                + " for " + url);
+      }
+      InputStream is = new BufferedInputStream(conn.getInputStream());
       if (outFile != null)
       {
         FileOutputStream fio = new FileOutputStream(outFile);
@@ -267,12 +275,12 @@ public class EBIFetchClient
     if (database.equalsIgnoreCase(DBRefSource.EMBL)
             || database.equalsIgnoreCase(DBRefSource.EMBLCDS))
     {
-      url = "http://www.ebi.ac.uk/ena/data/view/" + ids.toLowerCase()
+      url = "https://www.ebi.ac.uk/ena/data/view/" + ids.toLowerCase()
               + (format != null ? "&" + format : "");
     }
     else
     {
-      url = "http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/"
+      url = "https://www.ebi.ac.uk/Tools/dbfetch/dbfetch/"
               + database.toLowerCase() + "/" + ids.toLowerCase()
               + (format != null ? "/" + format : "");
     }
index a239625..23d9eb0 100644 (file)
@@ -235,8 +235,7 @@ class JPredThread extends JWS1Thread implements WSClientI
           {
             // Adjust input view for gaps
             // propagate insertions into profile
-            alhidden = HiddenColumns.propagateInsertions(profileseq, al,
-                    input);
+            alhidden = al.propagateInsertions(profileseq, input);
           }
         }
       }
index c24ea05..9a2316c 100644 (file)
@@ -99,22 +99,22 @@ public class AADisorderClient extends JabawsCalcWorker
   {
     // TODO: turn this into some kind of configuration file that's a bit easier
     // to edit
-    featureMap = new HashMap<String, Map<String, String[]>>();
+    featureMap = new HashMap<>();
     Map<String, String[]> fmap;
     featureMap.put(compbio.ws.client.Services.IUPredWS.toString(),
-            fmap = new HashMap<String, String[]>());
+            fmap = new HashMap<>());
     fmap.put("Glob",
             new String[]
             { "Globular Domain", "Predicted globular domain" });
     featureMap.put(compbio.ws.client.Services.JronnWS.toString(),
-            fmap = new HashMap<String, String[]>());
+            fmap = new HashMap<>());
     featureMap.put(compbio.ws.client.Services.DisemblWS.toString(),
-            fmap = new HashMap<String, String[]>());
+            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<String, String[]>());
+            fmap = new HashMap<>());
     fmap.put("GlobDoms",
             new String[]
             { "Globular Domain", "Predicted globular domain" });
@@ -122,9 +122,9 @@ public class AADisorderClient extends JabawsCalcWorker
             new String[]
             { "Protein Disorder", "Probable unstructured peptide region" });
     Map<String, Map<String, Object>> amap;
-    annotMap = new HashMap<String, Map<String, Map<String, Object>>>();
+    annotMap = new HashMap<>();
     annotMap.put(compbio.ws.client.Services.GlobPlotWS.toString(),
-            amap = new HashMap<String, Map<String, Object>>());
+            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 });
@@ -135,7 +135,7 @@ public class AADisorderClient extends JabawsCalcWorker
     amap.put("RawScore", new HashMap<String, Object>());
     amap.get("RawScore").put(INVISIBLE, INVISIBLE);
     annotMap.put(compbio.ws.client.Services.DisemblWS.toString(),
-            amap = new HashMap<String, Map<String, Object>>());
+            amap = new HashMap<>());
     amap.put("COILS", new HashMap<String, Object>());
     amap.put("HOTLOOPS", new HashMap<String, Object>());
     amap.put("REM465", new HashMap<String, Object>());
@@ -148,7 +148,7 @@ public class AADisorderClient extends JabawsCalcWorker
     amap.get("REM465").put(RANGE, new float[] { 0, 1 });
 
     annotMap.put(compbio.ws.client.Services.IUPredWS.toString(),
-            amap = new HashMap<String, Map<String, Object>>());
+            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 });
@@ -156,7 +156,7 @@ public class AADisorderClient extends JabawsCalcWorker
     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<String, Map<String, Object>>());
+            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 });
@@ -173,8 +173,8 @@ public class AADisorderClient extends JabawsCalcWorker
       Map<String, Map<String, Object>> annotTypeMap = annotMap
               .get(service.serviceType);
       boolean dispFeatures = false;
-      Map<String, Object> fc = new Hashtable<String, Object>();
-      List<AlignmentAnnotation> ourAnnot = new ArrayList<AlignmentAnnotation>();
+      Map<String, Object> fc = new Hashtable<>();
+      List<AlignmentAnnotation> ourAnnot = new ArrayList<>();
       /**
        * grouping for any annotation rows created
        */
@@ -358,7 +358,6 @@ public class AADisorderClient extends JabawsCalcWorker
             // only do this if the alignFrame is currently showing this view.
             af.setShowSeqFeatures(true);
           }
-          ap.paintAlignment(true);
         }
         if (ourAnnot.size() > 0)
         {
@@ -366,6 +365,7 @@ public class AADisorderClient extends JabawsCalcWorker
           // new alignment annotation rows created.
           updateOurAnnots(ourAnnot);
           ap.adjustAnnotationHeight();
+          ap.paintAlignment(true, true);
         }
       }
     }
index 26fe0a2..dd64e77 100644 (file)
@@ -30,6 +30,7 @@ import jalview.datamodel.AnnotatedCollectionI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.IProgressIndicator;
+import jalview.gui.IProgressIndicatorHandler;
 import jalview.schemes.ResidueProperties;
 import jalview.workers.AlignCalcWorker;
 import jalview.ws.jws2.dm.AAConSettings;
@@ -220,7 +221,26 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
                 progressId = System.currentTimeMillis());
       }
       rslt = submitToService(seqs);
+      if (guiProgress != null)
+      {
+        guiProgress.registerHandler(progressId,
+                new IProgressIndicatorHandler()
+                {
 
+                  @Override
+                  public boolean cancelActivity(long id)
+                  {
+                    cancelCurrentJob();
+                    return true;
+                  }
+
+                  @Override
+                  public boolean canCancel()
+                  {
+                    return true;
+                  }
+                });
+      }
       boolean finished = false;
       long rpos = 0;
       do
@@ -372,7 +392,8 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
         {
           guiProgress.setProgressBar("", progressId);
         }
-        ap.paintAlignment(true);
+        // TODO: may not need to paintAlignment again !
+        ap.paintAlignment(false, false);
       }
       if (msg.length() > 0)
       {
@@ -582,7 +603,7 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
   protected boolean checkDone()
   {
     calcMan.notifyStart(this);
-    ap.paintAlignment(false);
+    ap.paintAlignment(false, false);
     while (!calcMan.notifyWorking(this))
     {
       if (calcMan.isWorking(this))
@@ -593,7 +614,7 @@ public abstract class AbstractJabaCalcWorker extends AlignCalcWorker
       {
         if (ap != null)
         {
-          ap.paintAlignment(false);
+          ap.paintAlignment(false, false);
         }
 
         Thread.sleep(200);
index cb8f75a..2f3c298 100644 (file)
@@ -170,13 +170,11 @@ public class Jws2Instance
     {
       try
       {
-        Closeable svc = (Closeable) service;
-        service = null;
-        svc.close();
-      } catch (Exception e)
+        ((Closeable) service).close();
+      } catch (Throwable t)
       {
+        // ignore
       }
-      ;
     }
     super.finalize();
   }
index 68af7c3..b5f9653 100644 (file)
@@ -92,14 +92,24 @@ public class SiftsClient implements SiftsClientI
 
   private CoordinateSys seqCoordSys = CoordinateSys.UNIPROT;
 
+  /**
+   * PDB sequence position to sequence coordinate mapping as derived from SIFTS
+   * record for the identified SeqCoordSys Used for lift-over from sequence
+   * derived from PDB (with first extracted PDBRESNUM as 'start' to the sequence
+   * being annotated with PDB data
+   */
+  private jalview.datamodel.Mapping seqFromPdbMapping;
+
   private static final int BUFFER_SIZE = 4096;
 
-  public static final int UNASSIGNED = -1;
+  public static final int UNASSIGNED = Integer.MIN_VALUE;
 
   private static final int PDB_RES_POS = 0;
 
   private static final int PDB_ATOM_POS = 1;
 
+  private static final int PDBE_POS = 2;
+
   private static final String NOT_OBSERVED = "Not_Observed";
 
   private static final String SIFTS_FTP_BASE_URL = "http://ftp.ebi.ac.uk/pub/databases/msd/sifts/xml/";
@@ -413,6 +423,11 @@ public class SiftsClient implements SiftsClientI
   public StructureMapping getSiftsStructureMapping(SequenceI seq,
           String pdbFile, String chain) throws SiftsException
   {
+    SequenceI aseq = seq;
+    while (seq.getDatasetSequence() != null)
+    {
+      seq = seq.getDatasetSequence();
+    }
     structId = (chain == null) ? pdbId : pdbId + "|" + chain;
     System.out.println("Getting SIFTS mapping for " + structId + ": seq "
             + seq.getName());
@@ -435,8 +450,9 @@ public class SiftsClient implements SiftsClientI
     HashMap<Integer, int[]> mapping = getGreedyMapping(chain, seq, ps);
 
     String mappingOutput = mappingDetails.toString();
-    StructureMapping siftsMapping = new StructureMapping(seq, pdbFile,
-            pdbId, chain, mapping, mappingOutput);
+    StructureMapping siftsMapping = new StructureMapping(aseq, pdbFile,
+            pdbId, chain, mapping, mappingOutput, seqFromPdbMapping);
+
     return siftsMapping;
   }
 
@@ -444,8 +460,8 @@ public class SiftsClient implements SiftsClientI
   public HashMap<Integer, int[]> getGreedyMapping(String entityId,
           SequenceI seq, java.io.PrintStream os) throws SiftsException
   {
-    List<Integer> omitNonObserved = new ArrayList<Integer>();
-    int nonObservedShiftIndex = 0;
+    List<Integer> omitNonObserved = new ArrayList<>();
+    int nonObservedShiftIndex = 0,pdbeNonObserved=0;
     // System.out.println("Generating mappings for : " + entityId);
     Entity entity = null;
     entity = getEntityById(entityId);
@@ -476,7 +492,7 @@ public class SiftsClient implements SiftsClientI
     TreeMap<Integer, String> resNumMap = new TreeMap<Integer, String>();
     List<Segment> segments = entity.getSegment();
     SegmentHelperPojo shp = new SegmentHelperPojo(seq, mapping, resNumMap,
-            omitNonObserved, nonObservedShiftIndex);
+            omitNonObserved, nonObservedShiftIndex,pdbeNonObserved);
     processSegments(segments, shp);
     try
     {
@@ -498,15 +514,61 @@ public class SiftsClient implements SiftsClientI
     {
       throw new SiftsException("SIFTS mapping failed");
     }
+    // also construct a mapping object between the seq-coord sys and the PDB seq's coord sys
 
     Integer[] keys = mapping.keySet().toArray(new Integer[0]);
     Arrays.sort(keys);
     seqStart = keys[0];
     seqEnd = keys[keys.length - 1];
-
+    List<int[]> from=new ArrayList<>(),to=new ArrayList<>();
+    int[]_cfrom=null,_cto=null;
     String matchedSeq = originalSeq;
-    if (seqStart != UNASSIGNED)
+    if (seqStart != UNASSIGNED) // fixme! seqStart can map to -1 for a pdb sequence that starts <-1
     {
+      for (int seqps:keys)
+      {
+        int pdbpos = mapping.get(seqps)[PDBE_POS];
+        if (pdbpos == UNASSIGNED)
+        {
+          // not correct - pdbpos might be -1, but leave it for now
+          continue;
+        }
+        if (_cfrom==null || seqps!=_cfrom[1]+1)
+        {
+          _cfrom = new int[] { seqps,seqps};
+          from.add(_cfrom);
+          _cto = null; // discontinuity
+        } else {
+          _cfrom[1]= seqps;
+        }
+        if (_cto==null || pdbpos!=1+_cto[1])
+        {
+          _cto = new int[] { pdbpos,pdbpos};
+          to.add(_cto);
+        } else {
+          _cto[1] = pdbpos;
+        }
+      }
+      _cfrom = new int[from.size() * 2];
+      _cto = new int[to.size() * 2];
+      int p = 0;
+      for (int[] range : from)
+      {
+        _cfrom[p++] = range[0];
+        _cfrom[p++] = range[1];
+      }
+      ;
+      p = 0;
+      for (int[] range : to)
+      {
+        _cto[p++] = range[0];
+        _cto[p++] = range[1];
+      }
+      ;
+
+      seqFromPdbMapping = new jalview.datamodel.Mapping(null, _cto, _cfrom,
+              1,
+              1);
       pdbStart = mapping.get(seqStart)[PDB_RES_POS];
       pdbEnd = mapping.get(seqEnd)[PDB_RES_POS];
       int orignalSeqStart = seq.getStart();
@@ -559,6 +621,8 @@ public class SiftsClient implements SiftsClientI
     TreeMap<Integer, String> resNumMap = shp.getResNumMap();
     List<Integer> omitNonObserved = shp.getOmitNonObserved();
     int nonObservedShiftIndex = shp.getNonObservedShiftIndex();
+    int pdbeNonObservedCount = shp.getPdbeNonObserved();
+    int firstPDBResNum = UNASSIGNED;
     for (Segment segment : segments)
     {
       // System.out.println("Mapping segments : " + segment.getSegId() + "\\"s
@@ -566,6 +630,9 @@ public class SiftsClient implements SiftsClientI
       List<Residue> residues = segment.getListResidue().getResidue();
       for (Residue residue : residues)
       {
+        boolean isObserved = isResidueObserved(residue);
+        int pdbeIndex = getLeadingIntegerValue(residue.getDbResNum(),
+                UNASSIGNED);
         int currSeqIndex = UNASSIGNED;
         List<CrossRefDb> cRefDbs = residue.getCrossRefDb();
         CrossRefDb pdbRefDb = null;
@@ -574,6 +641,19 @@ public class SiftsClient implements SiftsClientI
           if (cRefDb.getDbSource().equalsIgnoreCase(DBRefSource.PDB))
           {
             pdbRefDb = cRefDb;
+            if (firstPDBResNum == UNASSIGNED)
+            {
+              firstPDBResNum = getLeadingIntegerValue(cRefDb.getDbResNum(),
+                      UNASSIGNED);
+            }
+            else
+            {
+              if (isObserved)
+              {
+                // after we find the first observed residue we just increment
+                firstPDBResNum++;
+              }
+            }
           }
           if (cRefDb.getDbCoordSys().equalsIgnoreCase(seqCoordSys.getName())
                   && isAccessionMatched(cRefDb.getDbAccessionId()))
@@ -586,11 +666,45 @@ public class SiftsClient implements SiftsClientI
             }
           }
         }
+        if (!isObserved)
+        {
+          ++pdbeNonObservedCount;
+        }
+        if (seqCoordSys == seqCoordSys.PDB) // FIXME: is seqCoordSys ever PDBe
+                                            // ???
+        {
+          // if the sequence has a primary reference to the PDB, then we are
+          // dealing with a sequence extracted directly from the PDB. In that
+          // case, numbering is PDBe - non-observed residues
+          currSeqIndex = seq.getStart() - 1 + pdbeIndex;
+        }
+        if (!isObserved)
+        {
+          if (seqCoordSys != CoordinateSys.UNIPROT) // FIXME: PDB or PDBe only
+                                                    // here
+          {
+            // mapping to PDB or PDBe so we need to bookkeep for the
+            // non-observed
+            // SEQRES positions
+            omitNonObserved.add(currSeqIndex);
+            ++nonObservedShiftIndex;
+          }
+        }
         if (currSeqIndex == UNASSIGNED)
         {
+          // change in logic - unobserved residues with no currSeqIndex
+          // corresponding are still counted in both nonObservedShiftIndex and
+          // pdbeIndex...
           continue;
         }
-        if (currSeqIndex >= seq.getStart() && currSeqIndex <= seq.getEnd())
+        // if (currSeqIndex >= seq.getStart() && currSeqIndex <= seqlength) //
+        // true
+                                                                         // numbering
+                                                                         // is
+                                                                         // not
+                                                                         // up
+                                                                         // to
+                                                                         // seq.getEnd()
         {
 
           int resNum = (pdbRefDb == null)
@@ -599,22 +713,18 @@ public class SiftsClient implements SiftsClientI
                   : getLeadingIntegerValue(pdbRefDb.getDbResNum(),
                           UNASSIGNED);
 
-          if (isResidueObserved(residue)
-                  || seqCoordSys == CoordinateSys.UNIPROT)
+          if (isObserved)
           {
             char resCharCode = ResidueProperties
                     .getSingleCharacterCode(ResidueProperties
                             .getCanonicalAminoAcid(residue.getDbResName()));
             resNumMap.put(currSeqIndex, String.valueOf(resCharCode));
+
+            int[] mappingcols = new int[] { Integer.valueOf(resNum),
+                UNASSIGNED, isObserved ? firstPDBResNum : UNASSIGNED };
+
+            mapping.put(currSeqIndex - nonObservedShiftIndex, mappingcols);
           }
-          else
-          {
-            omitNonObserved.add(currSeqIndex);
-            ++nonObservedShiftIndex;
-          }
-          mapping.put(currSeqIndex - nonObservedShiftIndex,
-                  new int[]
-                  { Integer.valueOf(resNum), UNASSIGNED });
         }
       }
     }
@@ -904,17 +1014,36 @@ public class SiftsClient implements SiftsClientI
 
     private int nonObservedShiftIndex;
 
+    /**
+     * count of number of 'not observed' positions in the PDB record's SEQRES
+     * (total number of residues with coordinates == length(SEQRES) -
+     * pdbeNonObserved
+     */
+    private int pdbeNonObserved;
+
     public SegmentHelperPojo(SequenceI seq, HashMap<Integer, int[]> mapping,
             TreeMap<Integer, String> resNumMap,
-            List<Integer> omitNonObserved, int nonObservedShiftIndex)
+            List<Integer> omitNonObserved, int nonObservedShiftIndex,
+            int pdbeNonObserved)
     {
       setSeq(seq);
       setMapping(mapping);
       setResNumMap(resNumMap);
       setOmitNonObserved(omitNonObserved);
       setNonObservedShiftIndex(nonObservedShiftIndex);
+      setPdbeNonObserved(pdbeNonObserved);
+
     }
 
+    public void setPdbeNonObserved(int pdbeNonObserved2)
+    {
+      this.pdbeNonObserved = pdbeNonObserved2;
+    }
+
+    public int getPdbeNonObserved()
+    {
+      return pdbeNonObserved;
+    }
     public SequenceI getSeq()
     {
       return seq;
@@ -964,6 +1093,7 @@ public class SiftsClient implements SiftsClientI
     {
       this.nonObservedShiftIndex = nonObservedShiftIndex;
     }
+
   }
 
   @Override
diff --git a/src/org/stackoverflowusers/file/WindowsShortcut.java b/src/org/stackoverflowusers/file/WindowsShortcut.java
new file mode 100644 (file)
index 0000000..671e002
--- /dev/null
@@ -0,0 +1,215 @@
+package org.stackoverflowusers.file;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+
+/**
+ * Represents a Windows shortcut (typically visible to Java only as a '.lnk' file).
+ *
+ * Retrieved 2011-09-23 from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java/672775#672775
+ * Originally called LnkParser
+ *
+ * Written by: (the stack overflow users, obviously!)
+ *   Apache Commons VFS dependency removed by crysxd (why were we using that!?) https://github.com/crysxd
+ *   Headerified, refactored and commented by Code Bling http://stackoverflow.com/users/675721/code-bling
+ *   Network file support added by Stefan Cordes http://stackoverflow.com/users/81330/stefan-cordes
+ *   Adapted by Sam Brightman http://stackoverflow.com/users/2492/sam-brightman
+ *   Based on information in 'The Windows Shortcut File Format' by Jesse Hager &lt;jessehager@iname.com&gt;
+ *   And somewhat based on code from the book 'Swing Hacks: Tips and Tools for Killer GUIs'
+ *     by Joshua Marinacci and Chris Adamson
+ *     ISBN: 0-596-00907-0
+ *     http://www.oreilly.com/catalog/swinghks/
+ */
+public class WindowsShortcut
+{
+    private boolean isDirectory;
+    private boolean isLocal;
+    private String real_file;
+
+    /**
+     * Provides a quick test to see if this could be a valid link !
+     * If you try to instantiate a new WindowShortcut and the link is not valid,
+     * Exceptions may be thrown and Exceptions are extremely slow to generate,
+     * therefore any code needing to loop through several files should first check this.
+     *
+     * @param file the potential link
+     * @return true if may be a link, false otherwise
+     * @throws IOException if an IOException is thrown while reading from the file
+     */
+    public static boolean isPotentialValidLink(File file) throws IOException {
+        final int minimum_length = 0x64;
+        InputStream fis = new FileInputStream(file);
+        boolean isPotentiallyValid = false;
+        try {
+            isPotentiallyValid = file.isFile()
+                && file.getName().toLowerCase().endsWith(".lnk")
+                && fis.available() >= minimum_length
+                && isMagicPresent(getBytes(fis, 32));
+        } finally {
+            fis.close();
+        }
+        return isPotentiallyValid;
+    }
+
+    public WindowsShortcut(File file) throws IOException, ParseException {
+        InputStream in = new FileInputStream(file);
+        try {
+            parseLink(getBytes(in));
+        } finally {
+            in.close();
+        }
+    }
+
+    /**
+     * @return the name of the filesystem object pointed to by this shortcut
+     */
+    public String getRealFilename() {
+        return real_file;
+    }
+
+    /**
+     * Tests if the shortcut points to a local resource.
+     * @return true if the 'local' bit is set in this shortcut, false otherwise
+     */
+    public boolean isLocal() {
+        return isLocal;
+    }
+
+    /**
+     * Tests if the shortcut points to a directory.
+     * @return true if the 'directory' bit is set in this shortcut, false otherwise
+     */
+    public boolean isDirectory() {
+        return isDirectory;
+    }
+
+    /**
+     * Gets all the bytes from an InputStream
+     * @param in the InputStream from which to read bytes
+     * @return array of all the bytes contained in 'in'
+     * @throws IOException if an IOException is encountered while reading the data from the InputStream
+     */
+    private static byte[] getBytes(InputStream in) throws IOException {
+        return getBytes(in, null);
+    }
+    
+    /**
+     * Gets up to max bytes from an InputStream
+     * @param in the InputStream from which to read bytes
+     * @param max maximum number of bytes to read
+     * @return array of all the bytes contained in 'in'
+     * @throws IOException if an IOException is encountered while reading the data from the InputStream
+     */
+    private static byte[] getBytes(InputStream in, Integer max) throws IOException {
+        // read the entire file into a byte buffer
+        ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        byte[] buff = new byte[256];
+        while (max == null || max > 0) {
+            int n = in.read(buff);
+            if (n == -1) {
+                break;
+            }
+            bout.write(buff, 0, n);
+            if (max != null)
+                max -= n;
+        }
+        in.close();
+        return bout.toByteArray();
+    }
+
+    private static boolean isMagicPresent(byte[] link) {
+        final int magic = 0x0000004C;
+        final int magic_offset = 0x00;
+        return link.length >= 32 && bytesToDword(link, magic_offset) == magic;
+    }
+
+    /**
+     * Gobbles up link data by parsing it and storing info in member fields
+     * @param link all the bytes from the .lnk file
+     */
+    private void parseLink(byte[] link) throws ParseException {
+        try {
+            if (!isMagicPresent(link))
+                throw new ParseException("Invalid shortcut; magic is missing", 0);
+
+            // get the flags byte
+            byte flags = link[0x14];
+
+            // get the file attributes byte
+            final int file_atts_offset = 0x18;
+            byte file_atts = link[file_atts_offset];
+            byte is_dir_mask = (byte)0x10;
+            if ((file_atts & is_dir_mask) > 0) {
+                isDirectory = true;
+            } else {
+                isDirectory = false;
+            }
+
+            // if the shell settings are present, skip them
+            final int shell_offset = 0x4c;
+            final byte has_shell_mask = (byte)0x01;
+            int shell_len = 0;
+            if ((flags & has_shell_mask) > 0) {
+                // the plus 2 accounts for the length marker itself
+                shell_len = bytesToWord(link, shell_offset) + 2;
+            }
+
+            // get to the file settings
+            int file_start = 0x4c + shell_len;
+
+            final int file_location_info_flag_offset_offset = 0x08;
+            int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset];
+            isLocal = (file_location_info_flag & 2) == 0;
+            // get the local volume and local system values
+            //final int localVolumeTable_offset_offset = 0x0C;
+            final int basename_offset_offset = 0x10;
+            final int networkVolumeTable_offset_offset = 0x14;
+            final int finalname_offset_offset = 0x18;
+            int finalname_offset = link[file_start + finalname_offset_offset] + file_start;
+            String finalname = getNullDelimitedString(link, finalname_offset);
+            if (isLocal) {
+                int basename_offset = link[file_start + basename_offset_offset] + file_start;
+                String basename = getNullDelimitedString(link, basename_offset);
+                real_file = basename + finalname;
+            } else {
+                int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start;
+                int shareName_offset_offset = 0x08;
+                int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset]
+                    + networkVolumeTable_offset;
+                String shareName = getNullDelimitedString(link, shareName_offset);
+                real_file = shareName + "\\" + finalname;
+            }
+        } catch (ArrayIndexOutOfBoundsException e) {
+            throw new ParseException("Could not be parsed, probably not a valid WindowsShortcut", 0);
+        }
+    }
+
+    private static String getNullDelimitedString(byte[] bytes, int off) {
+        int len = 0;
+        // count bytes until the null character (0)
+        while (true) {
+            if (bytes[off + len] == 0) {
+                break;
+            }
+            len++;
+        }
+        return new String(bytes, off, len);
+    }
+
+    /*
+     * convert two bytes into a short note, this is little endian because it's
+     * for an Intel only OS.
+     */
+    private static int bytesToWord(byte[] bytes, int off) {
+        return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff);
+    }
+
+    private static int bytesToDword(byte[] bytes, int off) {
+        return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off);
+    }
+
+}
index defcdbc..533c0af 100644 (file)
@@ -91,7 +91,7 @@ public class PDBChainTest
     a3.resName = "ASP";
     a3.resNumber = 41;
 
-    Vector<Bond> v = new Vector<Bond>();
+    Vector<Bond> v = new Vector<>();
     v.add(new Bond(a1, a2));
     v.add(new Bond(a2, a3));
     v.add(new Bond(a3, a1));
@@ -234,7 +234,7 @@ public class PDBChainTest
   @Test(groups = { "Functional" })
   public void testMakeResidueList_noAnnotation()
   {
-    Vector<Atom> atoms = new Vector<Atom>();
+    Vector<Atom> atoms = new Vector<>();
     c.atoms = atoms;
     c.isNa = true;
     atoms.add(makeAtom(4, "N", "MET"));
@@ -292,7 +292,7 @@ public class PDBChainTest
   @Test(groups = { "Functional" })
   public void testMakeResidueList_withTempFactor()
   {
-    Vector<Atom> atoms = new Vector<Atom>();
+    Vector<Atom> atoms = new Vector<>();
     c.atoms = atoms;
     atoms.add(makeAtom(4, "N", "MET"));
     atoms.get(atoms.size() - 1).tfactor = 1f;
@@ -307,7 +307,7 @@ public class PDBChainTest
     atoms.add(makeAtom(5, "CA", "LYS"));
     atoms.get(atoms.size() - 1).tfactor = 9f;
     atoms.add(makeAtom(6, "O", "LEU"));
-    atoms.get(atoms.size() - 1).tfactor = 4f;
+    atoms.get(atoms.size() - 1).tfactor = -4f;
     atoms.add(makeAtom(6, "N", "LEU"));
     atoms.get(atoms.size() - 1).tfactor = 5f;
     atoms.add(makeAtom(6, "CA", "LEU"));
@@ -320,7 +320,7 @@ public class PDBChainTest
 
     /*
      * Verify annotations; note the tempFactor is read from the first atom in
-     * each residue i.e. we expect values 1, 7, 4 for the residues
+     * each residue i.e. we expect values 1, 7, -4 for the residues
      */
     AlignmentAnnotation[] ann = c.sequence.getAnnotation();
     assertEquals(1, ann.length);
@@ -328,12 +328,12 @@ public class PDBChainTest
     assertEquals("Temperature Factor for 1gaqA", ann[0].description);
     assertSame(c.sequence, ann[0].sequenceRef);
     assertEquals(AlignmentAnnotation.LINE_GRAPH, ann[0].graph);
-    assertEquals(0f, ann[0].graphMin, 0.001f);
+    assertEquals(-4f, ann[0].graphMin, 0.001f);
     assertEquals(7f, ann[0].graphMax, 0.001f);
     assertEquals(3, ann[0].annotations.length);
     assertEquals(1f, ann[0].annotations[0].value, 0.001f);
     assertEquals(7f, ann[0].annotations[1].value, 0.001f);
-    assertEquals(4f, ann[0].annotations[2].value, 0.001f);
+    assertEquals(-4f, ann[0].annotations[2].value, 0.001f);
   }
 
   /**
@@ -344,7 +344,7 @@ public class PDBChainTest
   public void testMakeCaBondList()
   {
     c.isNa = true;
-    Vector<Atom> atoms = new Vector<Atom>();
+    Vector<Atom> atoms = new Vector<>();
     c.atoms = atoms;
     atoms.add(makeAtom(4, "N", "MET"));
     atoms.add(makeAtom(4, "CA", "MET"));
@@ -375,7 +375,7 @@ public class PDBChainTest
   public void testMakeCaBondList_nucleotide()
   {
     c.isNa = false;
-    Vector<Atom> atoms = new Vector<Atom>();
+    Vector<Atom> atoms = new Vector<>();
     c.atoms = atoms;
     atoms.add(makeAtom(4, "N", "G"));
     atoms.add(makeAtom(4, "P", "G"));
@@ -406,7 +406,7 @@ public class PDBChainTest
   @Test(groups = { "Functional" })
   public void testMakeExactMapping()
   {
-    Vector<Atom> atoms = new Vector<Atom>();
+    Vector<Atom> atoms = new Vector<>();
     c.atoms = atoms;
     atoms.add(makeAtom(4, "N", "MET"));
     atoms.add(makeAtom(4, "CA", "MET"));
index 3187fd9..9d3877c 100644 (file)
@@ -27,39 +27,25 @@ import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 import jalview.io.FastaFile;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
 import java.util.Arrays;
 import java.util.Random;
 
 import org.testng.annotations.BeforeClass;
 
 /**
- * Generates, and outputs in Fasta format, a random DNA alignment for given
+ * 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.
  * 
- * Arguments:
- * <ul>
- * <li>length (number of bases in each sequence)</li>
- * <li>height (number of sequences)</li>
- * <li>a whole number random seed</li>
- * <li>percentage of gaps to include (0-100)</li>
- * <li>percentage chance of variation of each position (0-100)</li>
- * </ul>
- * 
  * @author gmcarstairs
- *
  */
 public class AlignmentGenerator
 {
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
   private static final char GAP = '-';
 
   private static final char ZERO = '0';
@@ -72,51 +58,76 @@ public class AlignmentGenerator
 
   private Random random;
 
+  private PrintStream ps;
 
   /**
-   * Outputs a DNA 'alignment' where each position is a random choice from
-   * 'GTCA-'.
+   * Outputs a pseudo-randomly generated nucleotide or peptide alignment
+   * Arguments:
+   * <ul>
+   * <li>n (for nucleotide) or p (for peptide)</li>
+   * <li>length (number of bases in each sequence)</li>
+   * <li>height (number of sequences)</li>
+   * <li>a whole number random seed</li>
+   * <li>percentage of gaps to include (0-100)</li>
+   * <li>percentage chance of variation of each position (0-100)</li>
+   * <li>(optional) path to a file to write the alignment to</li>
+   * </ul>
+   * 
    * 
    * @param args
+   * @throws FileNotFoundException
    */
-  public static void main(String[] args)
+  public static void main(String[] args) throws FileNotFoundException
   {
-    if (args.length != 6)
+    if (args.length != 6 && args.length != 7)
     {
       usage();
       return;
     }
+
+    PrintStream ps = System.out;
+    if (args.length == 7)
+    {
+      ps = new PrintStream(new File(args[6]));
+    }
+
     boolean nucleotide = args[0].toLowerCase().startsWith("n");
     int width = Integer.parseInt(args[1]);
     int height = Integer.parseInt(args[2]);
     long randomSeed = Long.valueOf(args[3]);
     int gapPercentage = Integer.valueOf(args[4]);
     int changePercentage = Integer.valueOf(args[5]);
-    AlignmentI al = new AlignmentGenerator(nucleotide).generate(width,
-            height,
-            randomSeed, gapPercentage, changePercentage);
 
-    System.out.println("; " + height + " sequences of " + width
+    ps.println("; " + height + " sequences of " + width
             + " bases with " + gapPercentage + "% gaps and "
             + changePercentage + "% mutations (random seed = " + randomSeed
             + ")");
-    System.out.println(new FastaFile().print(al.getSequencesArray(), true));
+
+    new AlignmentGenerator(nucleotide, ps).generate(width, height,
+            randomSeed, gapPercentage, changePercentage);
+
+    if (ps != System.out)
+    {
+      ps.close();
+    }
   }
 
   /**
-   * Print parameter help.
+   * Prints parameter help
    */
   private static void usage()
   {
     System.out.println("Usage:");
     System.out.println("arg0: n (for nucleotide) or p (for peptide)");
     System.out.println("arg1: number of (non-gap) bases per sequence");
-    System.out.println("arg2: number sequences");
+    System.out.println("arg2: number of sequences");
     System.out
             .println("arg3: an integer as random seed (same seed = same results)");
     System.out.println("arg4: percentage of gaps to (randomly) generate");
     System.out
             .println("arg5: percentage of 'mutations' to (randomly) generate");
+    System.out
+            .println("arg6: (optional) path to output file (default is sysout)");
     System.out.println("Example: AlignmentGenerator n 12 15 387 10 5");
     System.out
             .println("- 15 nucleotide sequences of 12 bases each, approx 10% gaps and 5% mutations, random seed = 387");
@@ -124,16 +135,28 @@ public class AlignmentGenerator
   }
 
   /**
-   * Constructor that sets nucleotide or peptide symbol set
+   * Constructor that sets nucleotide or peptide symbol set, and also writes the
+   * generated alignment to sysout
    */
   public AlignmentGenerator(boolean nuc)
   {
-    BASES = nuc ? NUCS : PEPS;
+    this(nuc, System.out);
+  }
+
+  /**
+   * Constructor that sets nucleotide or peptide symbol set, and also writes the
+   * generated alignment to the specified output stream (if not null). This can
+   * be used to write the alignment to a file or sysout.
+   */
+  public AlignmentGenerator(boolean nucleotide, PrintStream printStream)
+  {
+    BASES = nucleotide ? NUCS : PEPS;
+    ps = printStream;
   }
 
   /**
-   * Outputs a DNA 'alignment' of given width and height, where each position is
-   * a random choice from 'GTCA-'.
+   * Outputs an 'alignment' of given width and height, where each position is a
+   * random choice from the symbol alphabet, or - for gap
    * 
    * @param width
    * @param height
@@ -153,6 +176,12 @@ public class AlignmentGenerator
               seqno + 1, width, changePercentage);
     }
     AlignmentI al = new Alignment(seqs);
+
+    if (ps != null)
+    {
+      ps.println(new FastaFile().print(al.getSequencesArray(), true));
+    }
+
     return al;
   }
 
index 4439bb9..92bf0ce 100644 (file)
@@ -34,6 +34,7 @@ import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
 import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.GeneLociI;
 import jalview.datamodel.Mapping;
 import jalview.datamodel.SearchResultMatchI;
 import jalview.datamodel.SearchResultsI;
@@ -47,6 +48,7 @@ import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FileFormatI;
 import jalview.io.FormatAdapter;
+import jalview.io.gff.SequenceOntologyI;
 import jalview.util.MapList;
 import jalview.util.MappingUtils;
 
@@ -63,6 +65,8 @@ import org.testng.annotations.Test;
 
 public class AlignmentUtilsTests
 {
+  private static Sequence ts = new Sequence("short",
+          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
 
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
@@ -71,9 +75,6 @@ public class AlignmentUtilsTests
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
-  public static Sequence ts = new Sequence("short",
-          "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm");
-
   @Test(groups = { "Functional" })
   public void testExpandContext()
   {
@@ -263,14 +264,14 @@ public class AlignmentUtilsTests
   @Test(groups = { "Functional" })
   public void testMapProteinAlignmentToCdna_noXrefs() throws IOException
   {
-    List<SequenceI> protseqs = new ArrayList<SequenceI>();
+    List<SequenceI> protseqs = new ArrayList<>();
     protseqs.add(new Sequence("UNIPROT|V12345", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12346", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12347", "SAR"));
     AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3]));
     protein.setDataset(null);
 
-    List<SequenceI> dnaseqs = new ArrayList<SequenceI>();
+    List<SequenceI> dnaseqs = new ArrayList<>();
     dnaseqs.add(new Sequence("EMBL|A11111", "TCAGCACGC")); // = SAR
     dnaseqs.add(new Sequence("EMBL|A22222", "GAGATACAA")); // = EIQ
     dnaseqs.add(new Sequence("EMBL|A33333", "GAAATCCAG")); // = EIQ
@@ -507,7 +508,7 @@ public class AlignmentUtilsTests
     acf.addMap(dna1.getDatasetSequence(), prot1.getDatasetSequence(), map);
     acf.addMap(dna2.getDatasetSequence(), prot2.getDatasetSequence(), map);
     acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map);
-    ArrayList<AlignedCodonFrame> acfs = new ArrayList<AlignedCodonFrame>();
+    ArrayList<AlignedCodonFrame> acfs = new ArrayList<>();
     acfs.add(acf);
     protein.setCodonFrames(acfs);
 
@@ -605,14 +606,14 @@ public class AlignmentUtilsTests
   public void testMapProteinAlignmentToCdna_withStartAndStopCodons()
           throws IOException
   {
-    List<SequenceI> protseqs = new ArrayList<SequenceI>();
+    List<SequenceI> protseqs = new ArrayList<>();
     protseqs.add(new Sequence("UNIPROT|V12345", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12346", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12347", "SAR"));
     AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3]));
     protein.setDataset(null);
 
-    List<SequenceI> dnaseqs = new ArrayList<SequenceI>();
+    List<SequenceI> dnaseqs = new ArrayList<>();
     // start + SAR:
     dnaseqs.add(new Sequence("EMBL|A11111", "ATGTCAGCACGC"));
     // = EIQ + stop
@@ -697,14 +698,14 @@ public class AlignmentUtilsTests
   @Test(groups = { "Functional" })
   public void testMapProteinAlignmentToCdna_withXrefs() throws IOException
   {
-    List<SequenceI> protseqs = new ArrayList<SequenceI>();
+    List<SequenceI> protseqs = new ArrayList<>();
     protseqs.add(new Sequence("UNIPROT|V12345", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12346", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12347", "SAR"));
     AlignmentI protein = new Alignment(protseqs.toArray(new SequenceI[3]));
     protein.setDataset(null);
 
-    List<SequenceI> dnaseqs = new ArrayList<SequenceI>();
+    List<SequenceI> dnaseqs = new ArrayList<>();
     dnaseqs.add(new Sequence("EMBL|A11111", "TCAGCACGC")); // = SAR
     dnaseqs.add(new Sequence("EMBL|A22222", "ATGGAGATACAA")); // = start + EIQ
     dnaseqs.add(new Sequence("EMBL|A33333", "GAAATCCAG")); // = EIQ
@@ -774,14 +775,14 @@ public class AlignmentUtilsTests
   public void testMapProteinAlignmentToCdna_prioritiseXrefs()
           throws IOException
   {
-    List<SequenceI> protseqs = new ArrayList<SequenceI>();
+    List<SequenceI> protseqs = new ArrayList<>();
     protseqs.add(new Sequence("UNIPROT|V12345", "EIQ"));
     protseqs.add(new Sequence("UNIPROT|V12346", "EIQ"));
     AlignmentI protein = new Alignment(
             protseqs.toArray(new SequenceI[protseqs.size()]));
     protein.setDataset(null);
 
-    List<SequenceI> dnaseqs = new ArrayList<SequenceI>();
+    List<SequenceI> dnaseqs = new ArrayList<>();
     dnaseqs.add(new Sequence("EMBL|A11111", "GAAATCCAG")); // = EIQ
     dnaseqs.add(new Sequence("EMBL|A22222", "GAAATTCAG")); // = EIQ
     AlignmentI cdna = new Alignment(dnaseqs.toArray(new SequenceI[dnaseqs
@@ -848,8 +849,8 @@ public class AlignmentUtilsTests
     al.addAnnotation(ann4); // Temp for seq1
     al.addAnnotation(ann5); // Temp for seq2
     al.addAnnotation(ann6); // Temp for no sequence
-    List<String> types = new ArrayList<String>();
-    List<SequenceI> scope = new ArrayList<SequenceI>();
+    List<String> types = new ArrayList<>();
+    List<SequenceI> scope = new ArrayList<>();
 
     /*
      * Set all sequence related Structure to hidden (ann1, ann2)
@@ -1044,14 +1045,18 @@ public class AlignmentUtilsTests
     dna.addCodonFrame(acf);
 
     /*
-     * In this case, mappings originally came from matching Uniprot accessions - so need an xref on dna involving those regions. These are normally constructed from CDS annotation
+     * In this case, mappings originally came from matching Uniprot accessions 
+     * - so need an xref on dna involving those regions. 
+     * These are normally constructed from CDS annotation
      */
     DBRefEntry dna1xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep1",
             new Mapping(mapfordna1));
-    dna1.getDatasetSequence().addDBRef(dna1xref);
+    dna1.addDBRef(dna1xref);
+    assertEquals(2, dna1.getDBRefs().length); // to self and to pep1
     DBRefEntry dna2xref = new DBRefEntry("UNIPROT", "ENSEMBL", "pep2",
             new Mapping(mapfordna2));
-    dna2.getDatasetSequence().addDBRef(dna2xref);
+    dna2.addDBRef(dna2xref);
+    assertEquals(2, dna2.getDBRefs().length); // to self and to pep2
 
     /*
      * execute method under test:
@@ -1106,6 +1111,38 @@ public class AlignmentUtilsTests
     assertEquals(cdsMapping.getInverse(), dbref.getMap().getMap());
 
     /*
+     * verify cDNA has added a dbref with mapping to CDS
+     */
+    assertEquals(3, dna1.getDBRefs().length);
+    DBRefEntry dbRefEntry = dna1.getDBRefs()[2];
+    assertSame(cds1Dss, dbRefEntry.getMap().getTo());
+    MapList dnaToCdsMapping = new MapList(new int[] { 4, 6, 10, 12 },
+            new int[] { 1, 6 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+    assertEquals(3, dna2.getDBRefs().length);
+    dbRefEntry = dna2.getDBRefs()[2];
+    assertSame(cds2Dss, dbRefEntry.getMap().getTo());
+    dnaToCdsMapping = new MapList(new int[] { 1, 3, 7, 9, 13, 15 },
+            new int[] { 1, 9 }, 1, 1);
+    assertEquals(dnaToCdsMapping, dbRefEntry.getMap().getMap());
+
+    /*
+     * verify CDS has added a dbref with mapping to cDNA
+     */
+    assertEquals(2, cds1Dss.getDBRefs().length);
+    dbRefEntry = cds1Dss.getDBRefs()[1];
+    assertSame(dna1.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    MapList cdsToDnaMapping = new MapList(new int[] { 1, 6 }, new int[] {
+        4, 6, 10, 12 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+    assertEquals(2, cds2Dss.getDBRefs().length);
+    dbRefEntry = cds2Dss.getDBRefs()[1];
+    assertSame(dna2.getDatasetSequence(), dbRefEntry.getMap().getTo());
+    cdsToDnaMapping = new MapList(new int[] { 1, 9 }, new int[] { 1, 3, 7,
+        9, 13, 15 }, 1, 1);
+    assertEquals(cdsToDnaMapping, dbRefEntry.getMap().getMap());
+
+    /*
      * Verify mappings from CDS to peptide, cDNA to CDS, and cDNA to peptide
      * the mappings are on the shared alignment dataset
      * 6 mappings, 2*(DNA->CDS), 2*(DNA->Pep), 2*(CDS->Pep) 
@@ -1747,7 +1784,7 @@ public class AlignmentUtilsTests
     map = new MapList(new int[] { 9, 11 }, new int[] { 2, 2 }, 3, 1);
     acf.addMap(dna3.getDatasetSequence(), prot3.getDatasetSequence(), map);
 
-    ArrayList<AlignedCodonFrame> acfs = new ArrayList<AlignedCodonFrame>();
+    ArrayList<AlignedCodonFrame> acfs = new ArrayList<>();
     acfs.add(acf);
     protein.setCodonFrames(acfs);
 
@@ -1896,6 +1933,7 @@ public class AlignmentUtilsTests
     sf6.setValue("alleles", "g, a"); // should force to upper-case
     sf6.setValue("ID", "sequence_variant:rs758803216");
     dna.addSequenceFeature(sf6);
+
     SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 15,
             15, 0f, null);
     sf7.setValue("alleles", "A, T");
@@ -1985,6 +2023,7 @@ public class AlignmentUtilsTests
      * variants:
      *           GAA -> E             source: Ensembl
      *           CAA -> Q             source: dbSNP
+     *           TAA -> STOP          source: dnSNP
      *           AAG synonymous       source: COSMIC
      *           AAT -> N             source: Ensembl
      *           ...TTC synonymous    source: dbSNP
@@ -2000,39 +2039,51 @@ public class AlignmentUtilsTests
     String ensembl = "Ensembl";
     String dbSnp = "dbSNP";
     String cosmic = "COSMIC";
+
     SequenceFeature sf1 = new SequenceFeature("sequence_variant", "", 1, 1,
             0f, ensembl);
-    sf1.setValue("alleles", "A,G"); // GAA -> E
+    sf1.setValue("alleles", "A,G"); // AAA -> GAA -> K/E
     sf1.setValue("ID", "var1.125A>G");
+
     SequenceFeature sf2 = new SequenceFeature("sequence_variant", "", 1, 1,
             0f, dbSnp);
-    sf2.setValue("alleles", "A,C"); // CAA -> Q
+    sf2.setValue("alleles", "A,C"); // AAA -> CAA -> K/Q
     sf2.setValue("ID", "var2");
     sf2.setValue("clinical_significance", "Dodgy");
-    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 3, 3,
-            0f, cosmic);
-    sf3.setValue("alleles", "A,G"); // synonymous
+
+    SequenceFeature sf3 = new SequenceFeature("sequence_variant", "", 1, 1,
+            0f, dbSnp);
+    sf3.setValue("alleles", "A,T"); // AAA -> TAA -> stop codon
     sf3.setValue("ID", "var3");
-    sf3.setValue("clinical_significance", "None");
+    sf3.setValue("clinical_significance", "Bad");
+
     SequenceFeature sf4 = new SequenceFeature("sequence_variant", "", 3, 3,
+            0f, cosmic);
+    sf4.setValue("alleles", "A,G"); // AAA -> AAG synonymous
+    sf4.setValue("ID", "var4");
+    sf4.setValue("clinical_significance", "None");
+
+    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 3, 3,
             0f, ensembl);
-    sf4.setValue("alleles", "A,T"); // AAT -> N
-    sf4.setValue("ID", "sequence_variant:var4"); // prefix gets stripped off
-    sf4.setValue("clinical_significance", "Benign");
-    SequenceFeature sf5 = new SequenceFeature("sequence_variant", "", 6, 6,
+    sf5.setValue("alleles", "A,T"); // AAA -> AAT -> K/N
+    sf5.setValue("ID", "sequence_variant:var5"); // prefix gets stripped off
+    sf5.setValue("clinical_significance", "Benign");
+
+    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 6, 6,
             0f, dbSnp);
-    sf5.setValue("alleles", "T,C"); // synonymous
-    sf5.setValue("ID", "var5");
-    sf5.setValue("clinical_significance", "Bad");
-    SequenceFeature sf6 = new SequenceFeature("sequence_variant", "", 8, 8,
-            0f, cosmic);
-    sf6.setValue("alleles", "C,A,G"); // CAC,CGC -> H,R
+    sf6.setValue("alleles", "T,C"); // TTT -> TTC synonymous
     sf6.setValue("ID", "var6");
-    sf6.setValue("clinical_significance", "Good");
 
-    List<DnaVariant> codon1Variants = new ArrayList<DnaVariant>();
-    List<DnaVariant> codon2Variants = new ArrayList<DnaVariant>();
-    List<DnaVariant> codon3Variants = new ArrayList<DnaVariant>();
+    SequenceFeature sf7 = new SequenceFeature("sequence_variant", "", 8, 8,
+            0f, cosmic);
+    sf7.setValue("alleles", "C,A,G"); // CCC -> CAC,CGC -> P/H/R
+    sf7.setValue("ID", "var7");
+    sf7.setValue("clinical_significance", "Good");
+
+    List<DnaVariant> codon1Variants = new ArrayList<>();
+    List<DnaVariant> codon2Variants = new ArrayList<>();
+    List<DnaVariant> codon3Variants = new ArrayList<>();
+
     List<DnaVariant> codonVariants[] = new ArrayList[3];
     codonVariants[0] = codon1Variants;
     codonVariants[1] = codon2Variants;
@@ -2043,10 +2094,11 @@ public class AlignmentUtilsTests
      */
     codon1Variants.add(new DnaVariant("A", sf1));
     codon1Variants.add(new DnaVariant("A", sf2));
+    codon1Variants.add(new DnaVariant("A", sf3));
     codon2Variants.add(new DnaVariant("A"));
-    codon2Variants.add(new DnaVariant("A"));
-    codon3Variants.add(new DnaVariant("A", sf3));
+    // codon2Variants.add(new DnaVariant("A"));
     codon3Variants.add(new DnaVariant("A", sf4));
+    codon3Variants.add(new DnaVariant("A", sf5));
     AlignmentUtils.computePeptideVariants(peptide, 1, codonVariants);
 
     /*
@@ -2057,7 +2109,7 @@ public class AlignmentUtilsTests
     codon3Variants.clear();
     codon1Variants.add(new DnaVariant("T"));
     codon2Variants.add(new DnaVariant("T"));
-    codon3Variants.add(new DnaVariant("T", sf5));
+    codon3Variants.add(new DnaVariant("T", sf6));
     AlignmentUtils.computePeptideVariants(peptide, 2, codonVariants);
 
     /*
@@ -2067,7 +2119,7 @@ public class AlignmentUtilsTests
     codon2Variants.clear();
     codon3Variants.clear();
     codon1Variants.add(new DnaVariant("C"));
-    codon2Variants.add(new DnaVariant("C", sf6));
+    codon2Variants.add(new DnaVariant("C", sf7));
     codon3Variants.add(new DnaVariant("C"));
     AlignmentUtils.computePeptideVariants(peptide, 3, codonVariants);
 
@@ -2075,35 +2127,43 @@ public class AlignmentUtilsTests
      * verify added sequence features for
      * var1 K -> E Ensembl
      * var2 K -> Q dbSNP
-     * var4 K -> N Ensembl
-     * var6 P -> H COSMIC
-     * var6 P -> R COSMIC
+     * var3 K -> stop
+     * var4 synonymous
+     * var5 K -> N Ensembl
+     * var6 synonymous
+     * var7 P -> H COSMIC
+     * var8 P -> R COSMIC
      */
     List<SequenceFeature> sfs = peptide.getSequenceFeatures();
     SequenceFeatures.sortFeatures(sfs, true);
-    assertEquals(5, sfs.size());
+    assertEquals(8, sfs.size());
 
     /*
      * features are sorted by start position ascending, but in no
      * particular order where start positions match; asserts here
      * simply match the data returned (the order is not important)
      */
+    // AAA -> AAT -> K/N
     SequenceFeature sf = sfs.get(0);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Asn", sf.getDescription());
-    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("var5", sf.getValue("ID"));
     assertEquals("Benign", sf.getValue("clinical_significance"));
-    assertEquals("ID=var4;clinical_significance=Benign", sf.getAttributes());
+    assertEquals("ID=var5;clinical_significance=Benign",
+            sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Lys1Asn var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            "p.Lys1Asn var5|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var5",
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
+    // AAA -> CAA -> K/Q
     sf = sfs.get(1);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Gln", sf.getDescription());
     assertEquals("var2", sf.getValue("ID"));
     assertEquals("Dodgy", sf.getValue("clinical_significance"));
@@ -2114,9 +2174,11 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(dbSnp, sf.getFeatureGroup());
 
+    // AAA -> GAA -> K/E
     sf = sfs.get(2);
     assertEquals(1, sf.getBegin());
     assertEquals(1, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Lys1Glu", sf.getDescription());
     assertEquals("var1.125A>G", sf.getValue("ID"));
     assertNull(sf.getValue("clinical_significance"));
@@ -2128,30 +2190,79 @@ public class AlignmentUtilsTests
             sf.links.get(0));
     assertEquals(ensembl, sf.getFeatureGroup());
 
+    // AAA -> TAA -> stop codon
     sf = sfs.get(3);
+    assertEquals(1, sf.getBegin());
+    assertEquals(1, sf.getEnd());
+    assertEquals("stop_gained", sf.getType());
+    assertEquals("Aaa/Taa", sf.getDescription());
+    assertEquals("var3", sf.getValue("ID"));
+    assertEquals("Bad", sf.getValue("clinical_significance"));
+    assertEquals("ID=var3;clinical_significance=Bad", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "Aaa/Taa var3|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var3",
+            sf.links.get(0));
+    assertEquals(dbSnp, sf.getFeatureGroup());
+
+    // AAA -> AAG synonymous
+    sf = sfs.get(4);
+    assertEquals(1, sf.getBegin());
+    assertEquals(1, sf.getEnd());
+    assertEquals("synonymous_variant", sf.getType());
+    assertEquals("aaA/aaG", sf.getDescription());
+    assertEquals("var4", sf.getValue("ID"));
+    assertEquals("None", sf.getValue("clinical_significance"));
+    assertEquals("ID=var4;clinical_significance=None", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "aaA/aaG var4|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var4",
+            sf.links.get(0));
+    assertEquals(cosmic, sf.getFeatureGroup());
+
+    // TTT -> TTC synonymous
+    sf = sfs.get(5);
+    assertEquals(2, sf.getBegin());
+    assertEquals(2, sf.getEnd());
+    assertEquals("synonymous_variant", sf.getType());
+    assertEquals("ttT/ttC", sf.getDescription());
+    assertEquals("var6", sf.getValue("ID"));
+    assertNull(sf.getValue("clinical_significance"));
+    assertEquals("ID=var6", sf.getAttributes());
+    assertEquals(1, sf.links.size());
+    assertEquals(
+            "ttT/ttC var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            sf.links.get(0));
+    assertEquals(dbSnp, sf.getFeatureGroup());
+
+    // var7 generates two distinct protein variant features (two alleles)
+    // CCC -> CGC -> P/R
+    sf = sfs.get(6);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Pro3Arg", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("var7", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
+    assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3Arg var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            "p.Pro3Arg var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
 
-    // var5 generates two distinct protein variant features
-    sf = sfs.get(4);
+    // CCC -> CAC -> P/H
+    sf = sfs.get(7);
     assertEquals(3, sf.getBegin());
     assertEquals(3, sf.getEnd());
+    assertEquals("nonsynonymous_variant", sf.getType());
     assertEquals("p.Pro3His", sf.getDescription());
-    assertEquals("var6", sf.getValue("ID"));
+    assertEquals("var7", sf.getValue("ID"));
     assertEquals("Good", sf.getValue("clinical_significance"));
-    assertEquals("ID=var6;clinical_significance=Good", sf.getAttributes());
+    assertEquals("ID=var7;clinical_significance=Good", sf.getAttributes());
     assertEquals(1, sf.links.size());
     assertEquals(
-            "p.Pro3His var6|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var6",
+            "p.Pro3His var7|http://www.ensembl.org/Homo_sapiens/Variation/Summary?v=var7",
             sf.links.get(0));
     assertEquals(cosmic, sf.getFeatureGroup());
   }
@@ -2272,7 +2383,7 @@ public class AlignmentUtilsTests
     seq1.createDatasetSequence();
     Mapping mapping = new Mapping(seq1, new MapList(
             new int[] { 3, 6, 9, 10 }, new int[] { 1, 6 }, 1, 1));
-    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
+    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<>();
     AlignmentUtils.addMappedPositions(seq1, from, mapping, map);
 
     /*
@@ -2304,7 +2415,7 @@ public class AlignmentUtilsTests
     seq1.createDatasetSequence();
     Mapping mapping = new Mapping(seq1, new MapList(
             new int[] { 3, 6, 9, 10 }, new int[] { 1, 6 }, 1, 1));
-    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<Integer, Map<SequenceI, Character>>();
+    Map<Integer, Map<SequenceI, Character>> map = new TreeMap<>();
     AlignmentUtils.addMappedPositions(seq1, from, mapping, map);
 
     /*
@@ -2533,4 +2644,307 @@ public class AlignmentUtilsTests
     assertEquals(s_as3, uas3.getSequenceAsString());
   }
 
+  @Test(groups = { "Functional" })
+  public void testTransferGeneLoci()
+  {
+    SequenceI from = new Sequence("transcript",
+            "aaacccgggTTTAAACCCGGGtttaaacccgggttt");
+    SequenceI to = new Sequence("CDS", "TTTAAACCCGGG");
+    MapList map = new MapList(new int[] { 1, 12 }, new int[] { 10, 21 }, 1,
+            1);
+
+    /*
+     * first with nothing to transfer
+     */
+    AlignmentUtils.transferGeneLoci(from, map, to);
+    assertNull(to.getGeneLoci());
+
+    /*
+     * next with gene loci set on 'from' sequence
+     */
+    int[] exons = new int[] { 100, 105, 155, 164, 210, 229 };
+    MapList geneMap = new MapList(new int[] { 1, 36 }, exons, 1, 1);
+    from.setGeneLoci("human", "GRCh38", "7", geneMap);
+    AlignmentUtils.transferGeneLoci(from, map, to);
+
+    GeneLociI toLoci = to.getGeneLoci();
+    assertNotNull(toLoci);
+    // DBRefEntry constructor upper-cases 'source'
+    assertEquals("HUMAN", toLoci.getSpeciesId());
+    assertEquals("GRCh38", toLoci.getAssemblyId());
+    assertEquals("7", toLoci.getChromosomeId());
+
+    /*
+     * transcript 'exons' are 1-6, 7-16, 17-36
+     * CDS 1:12 is transcript 10-21
+     * transcript 'CDS' is 10-16, 17-21
+     * which is 'gene' 158-164, 210-214
+     */
+    MapList toMap = toLoci.getMap();
+    assertEquals(1, toMap.getFromRanges().size());
+    assertEquals(2, toMap.getFromRanges().get(0).length);
+    assertEquals(1, toMap.getFromRanges().get(0)[0]);
+    assertEquals(12, toMap.getFromRanges().get(0)[1]);
+    assertEquals(1, toMap.getToRanges().size());
+    assertEquals(4, toMap.getToRanges().get(0).length);
+    assertEquals(158, toMap.getToRanges().get(0)[0]);
+    assertEquals(164, toMap.getToRanges().get(0)[1]);
+    assertEquals(210, toMap.getToRanges().get(0)[2]);
+    assertEquals(214, toMap.getToRanges().get(0)[3]);
+    // or summarised as (but toString might change in future):
+    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
+            toMap.toString());
+
+    /*
+     * an existing value is not overridden 
+     */
+    geneMap = new MapList(new int[] { 1, 36 }, new int[] { 36, 1 }, 1, 1);
+    from.setGeneLoci("inhuman", "GRCh37", "6", geneMap);
+    AlignmentUtils.transferGeneLoci(from, map, to);
+    assertEquals("GRCh38", toLoci.getAssemblyId());
+    assertEquals("7", toLoci.getChromosomeId());
+    toMap = toLoci.getMap();
+    assertEquals("[ [1, 12] ] 1:1 to [ [158, 164, 210, 214] ]",
+            toMap.toString());
+  }
+
+  /**
+   * Tests for the method that maps nucleotide to protein based on CDS features
+   */
+  @Test(groups = "Functional")
+  public void testMapCdsToProtein()
+  {
+    SequenceI peptide = new Sequence("pep", "KLQ");
+
+    /*
+     * Case 1: CDS 3 times length of peptide
+     * NB method only checks lengths match, not translation
+     */
+    SequenceI dna = new Sequence("dna", "AACGacgtCTCCT");
+    dna.createDatasetSequence();
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 13, null));
+    MapList ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertEquals(3, ml.getFromRatio());
+    assertEquals(1, ml.getToRatio());
+    assertEquals("[[1, 3]]",
+            Arrays.deepToString(ml.getToRanges().toArray()));
+    assertEquals("[[1, 4], [9, 13]]",
+            Arrays.deepToString(ml.getFromRanges().toArray()));
+
+    /*
+     * Case 2: CDS 3 times length of peptide + stop codon
+     * (note code does not currently check trailing codon is a stop codon)
+     */
+    dna = new Sequence("dna", "AACGacgtCTCCTCCC");
+    dna.createDatasetSequence();
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 16, null));
+    ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertEquals(3, ml.getFromRatio());
+    assertEquals(1, ml.getToRatio());
+    assertEquals("[[1, 3]]",
+            Arrays.deepToString(ml.getToRanges().toArray()));
+    assertEquals("[[1, 4], [9, 13]]",
+            Arrays.deepToString(ml.getFromRanges().toArray()));
+
+    /*
+     * Case 3: CDS longer than 3 * peptide + stop codon - no mapping is made
+     */
+    dna = new Sequence("dna", "AACGacgtCTCCTTGATCA");
+    dna.createDatasetSequence();
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 19, null));
+    ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertNull(ml);
+
+    /*
+     * Case 4: CDS shorter than 3 * peptide - no mapping is made
+     */
+    dna = new Sequence("dna", "AACGacgtCTCC");
+    dna.createDatasetSequence();
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 12, null));
+    ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertNull(ml);
+
+    /*
+     * Case 5: CDS 3 times length of peptide + part codon - mapping is truncated
+     */
+    dna = new Sequence("dna", "AACGacgtCTCCTTG");
+    dna.createDatasetSequence();
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 1, 4, null));
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 9, 15, null));
+    ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertEquals(3, ml.getFromRatio());
+    assertEquals(1, ml.getToRatio());
+    assertEquals("[[1, 3]]",
+            Arrays.deepToString(ml.getToRanges().toArray()));
+    assertEquals("[[1, 4], [9, 13]]",
+            Arrays.deepToString(ml.getFromRanges().toArray()));
+
+    /*
+     * Case 6: incomplete start codon corresponding to X in peptide
+     */
+    dna = new Sequence("dna", "ACGacgtCTCCTTGG");
+    dna.createDatasetSequence();
+    SequenceFeature sf = new SequenceFeature("CDS", "", 1, 3, null);
+    sf.setPhase("2"); // skip 2 positions (AC) to start of next codon (GCT)
+    dna.addSequenceFeature(sf);
+    dna.addSequenceFeature(new SequenceFeature("CDS", "", 8, 15, null));
+    peptide = new Sequence("pep", "XLQ");
+    ml = AlignmentUtils.mapCdsToProtein(dna, peptide);
+    assertEquals("[[2, 3]]",
+            Arrays.deepToString(ml.getToRanges().toArray()));
+    assertEquals("[[3, 3], [8, 12]]",
+            Arrays.deepToString(ml.getFromRanges().toArray()));
+  }
+
+  /**
+   * Tests for the method that locates the CDS sequence that has a mapping to
+   * the given protein. That is, given a transcript-to-peptide mapping, find the
+   * cds-to-peptide mapping that relates to both, and return the CDS sequence.
+   */
+  @Test
+  public void testFindCdsForProtein()
+  {
+    List<AlignedCodonFrame> mappings = new ArrayList<>();
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    mappings.add(acf1);
+
+    SequenceI dna1 = new Sequence("dna1", "cgatATcgGCTATCTATGacg");
+    dna1.createDatasetSequence();
+
+    // NB we currently exclude STOP codon from CDS sequences
+    // the test would need to change if this changes in future
+    SequenceI cds1 = new Sequence("cds1", "ATGCTATCT");
+    cds1.createDatasetSequence();
+
+    SequenceI pep1 = new Sequence("pep1", "MLS");
+    pep1.createDatasetSequence();
+    List<AlignedCodonFrame> seqMappings = new ArrayList<>();
+    MapList mapList = new MapList(
+            new int[]
+            { 5, 6, 9, 15 }, new int[] { 1, 3 }, 3, 1);
+    Mapping dnaToPeptide = new Mapping(pep1.getDatasetSequence(), mapList);
+    
+    // add dna to peptide mapping
+    seqMappings.add(acf1);
+    acf1.addMap(dna1.getDatasetSequence(), pep1.getDatasetSequence(),
+            mapList);
+
+    /*
+     * first case - no dna-to-CDS mapping exists - search fails
+     */
+    SequenceI seq = AlignmentUtils.findCdsForProtein(mappings, dna1,
+            seqMappings, dnaToPeptide);
+    assertNull(seq);
+
+    /*
+     * second case - CDS-to-peptide mapping exists but no dna-to-CDS
+     * - search fails
+     */
+    // todo this test fails if the mapping is added to acf1, not acf2
+    // need to tidy up use of lists of mappings in AlignedCodonFrame
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    mappings.add(acf2);
+    MapList cdsToPeptideMapping = new MapList(new int[]
+    { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+    acf2.addMap(cds1.getDatasetSequence(), pep1.getDatasetSequence(),
+            cdsToPeptideMapping);
+    assertNull(AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+            dnaToPeptide));
+
+    /*
+     * third case - add dna-to-CDS mapping - CDS is now found!
+     */
+    MapList dnaToCdsMapping = new MapList(new int[] { 5, 6, 9, 15 },
+            new int[]
+            { 1, 9 }, 1, 1);
+    acf1.addMap(dna1.getDatasetSequence(), cds1.getDatasetSequence(),
+            dnaToCdsMapping);
+    seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+            dnaToPeptide);
+    assertSame(seq, cds1.getDatasetSequence());
+  }
+
+  /**
+   * Tests for the method that locates the CDS sequence that has a mapping to
+   * the given protein. That is, given a transcript-to-peptide mapping, find the
+   * cds-to-peptide mapping that relates to both, and return the CDS sequence.
+   * This test is for the case where transcript and CDS are the same length.
+   */
+  @Test
+  public void testFindCdsForProtein_noUTR()
+  {
+    List<AlignedCodonFrame> mappings = new ArrayList<>();
+    AlignedCodonFrame acf1 = new AlignedCodonFrame();
+    mappings.add(acf1);
+  
+    SequenceI dna1 = new Sequence("dna1", "ATGCTATCTTAA");
+    dna1.createDatasetSequence();
+  
+    // NB we currently exclude STOP codon from CDS sequences
+    // the test would need to change if this changes in future
+    SequenceI cds1 = new Sequence("cds1", "ATGCTATCT");
+    cds1.createDatasetSequence();
+  
+    SequenceI pep1 = new Sequence("pep1", "MLS");
+    pep1.createDatasetSequence();
+    List<AlignedCodonFrame> seqMappings = new ArrayList<>();
+    MapList mapList = new MapList(
+            new int[]
+            { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+    Mapping dnaToPeptide = new Mapping(pep1.getDatasetSequence(), mapList);
+    
+    // add dna to peptide mapping
+    seqMappings.add(acf1);
+    acf1.addMap(dna1.getDatasetSequence(), pep1.getDatasetSequence(),
+            mapList);
+  
+    /*
+     * first case - transcript lacks CDS features - it appears to be
+     * the CDS sequence and is returned
+     */
+    SequenceI seq = AlignmentUtils.findCdsForProtein(mappings, dna1,
+            seqMappings, dnaToPeptide);
+    assertSame(seq, dna1.getDatasetSequence());
+  
+    /*
+     * second case - transcript has CDS feature - this means it is
+     * not returned as a match for CDS (CDS sequences don't have CDS features)
+     */
+    dna1.addSequenceFeature(
+            new SequenceFeature(SequenceOntologyI.CDS, "cds", 1, 12, null));
+    seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+            dnaToPeptide);
+    assertNull(seq);
+
+    /*
+     * third case - CDS-to-peptide mapping exists but no dna-to-CDS
+     * - search fails
+     */
+    // todo this test fails if the mapping is added to acf1, not acf2
+    // need to tidy up use of lists of mappings in AlignedCodonFrame
+    AlignedCodonFrame acf2 = new AlignedCodonFrame();
+    mappings.add(acf2);
+    MapList cdsToPeptideMapping = new MapList(new int[]
+    { 1, 9 }, new int[] { 1, 3 }, 3, 1);
+    acf2.addMap(cds1.getDatasetSequence(), pep1.getDatasetSequence(),
+            cdsToPeptideMapping);
+    assertNull(AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+            dnaToPeptide));
+  
+    /*
+     * fourth case - add dna-to-CDS mapping - CDS is now found!
+     */
+    MapList dnaToCdsMapping = new MapList(new int[] { 1, 9 },
+            new int[]
+            { 1, 9 }, 1, 1);
+    acf1.addMap(dna1.getDatasetSequence(), cds1.getDatasetSequence(),
+            dnaToCdsMapping);
+    seq = AlignmentUtils.findCdsForProtein(mappings, dna1, seqMappings,
+            dnaToPeptide);
+    assertSame(seq, cds1.getDatasetSequence());
+  }
 }
index 95be1ff..0265af3 100644 (file)
@@ -106,7 +106,7 @@ public class CrossRefTest
   public void testFindXrefSourcesForSequence_proteinToDna()
   {
     SequenceI seq = new Sequence("Seq1", "MGKYQARLSS");
-    List<String> sources = new ArrayList<String>();
+    List<String> sources = new ArrayList<>();
     AlignmentI al = new Alignment(new SequenceI[] {});
 
     /*
@@ -132,8 +132,9 @@ public class CrossRefTest
     sources = new CrossRef(new SequenceI[] { seq }, al)
             .findXrefSourcesForSequences(false);
     // method is patched to remove EMBL from the sources to match
-    assertEquals(3, sources.size());
-    assertEquals("[EMBLCDS, GENEDB, ENSEMBL]", sources.toString());
+    assertEquals(4, sources.size());
+    assertEquals("[EMBLCDS, GENEDB, ENSEMBL, ENSEMBLGENOMES]",
+            sources.toString());
 
     /*
      * add a sequence to the alignment which has a dbref to UNIPROT|A1234
@@ -270,7 +271,7 @@ public class CrossRefTest
     pep1.addDBRef(new DBRefEntry("UNIPROT", "0", "Q9ZTS2"));
     AlignmentI al = new Alignment(new SequenceI[] { dna1, pep1 });
 
-    List<SequenceI> result = new ArrayList<SequenceI>();
+    List<SequenceI> result = new ArrayList<>();
 
     /*
      * first search for a dbref nowhere on the alignment:
index d2fa99a..6a31b31 100644 (file)
@@ -38,6 +38,7 @@ import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
 
 import java.io.IOException;
+import java.util.Iterator;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -135,7 +136,9 @@ public class DnaTest
             FileFormat.Fasta);
     HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(alf, cs);
-    Dna dna = new Dna(av, new int[] { 0, alf.getWidth() - 1 });
+    Iterator<int[]> contigs = cs.getVisContigsIterator(0, alf.getWidth(),
+            false);
+    Dna dna = new Dna(av, contigs);
     AlignmentI translated = dna.translateCdna();
     assertNotNull("Couldn't do a full width translation of test data.",
             translated);
@@ -163,7 +166,8 @@ public class DnaTest
         cs.hideColumns(0, ipos - 1);
       }
       cs.hideColumns(ipos + vwidth, alf.getWidth());
-      int[] vcontigs = cs.getVisibleContigs(0, alf.getWidth());
+      Iterator<int[]> vcontigs = cs.getVisContigsIterator(0,
+              alf.getWidth(), false);
       AlignViewportI av = new AlignViewport(alf, cs);
       Dna dna = new Dna(av, vcontigs);
       AlignmentI transAlf = dna.translateCdna();
@@ -190,7 +194,9 @@ public class DnaTest
             DataSourceType.PASTE, FileFormat.Fasta);
     HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(alf, cs);
-    Dna dna = new Dna(av, new int[] { 0, alf.getWidth() - 1 });
+    Iterator<int[]> contigs = cs.getVisContigsIterator(0, alf.getWidth(),
+            false);
+    Dna dna = new Dna(av, contigs);
     AlignmentI translated = dna.translateCdna();
     String aa = translated.getSequenceAt(0).getSequenceAsString();
     assertEquals(
@@ -213,7 +219,9 @@ public class DnaTest
     cs.hideColumns(24, 35); // hide codons 9-12
     cs.hideColumns(177, 191); // hide codons 60-64
     AlignViewportI av = new AlignViewport(alf, cs);
-    Dna dna = new Dna(av, new int[] { 0, alf.getWidth() - 1 });
+    Iterator<int[]> contigs = cs.getVisContigsIterator(0, alf.getWidth(),
+            false);
+    Dna dna = new Dna(av, contigs);
     AlignmentI translated = dna.translateCdna();
     String aa = translated.getSequenceAt(0).getSequenceAsString();
     assertEquals("AACDDGGGGHHIIIKKLLLLLLMNNPPPPQQRRRRRRSSSSSSTTTTVVVVW", aa);
@@ -298,7 +306,9 @@ public class DnaTest
             .generate(12, 8, 97, 5, 5);
     HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(cdna, cs);
-    Dna dna = new Dna(av, new int[] { 0, cdna.getWidth() - 1 });
+    Iterator<int[]> contigs = cs.getVisContigsIterator(0, cdna.getWidth(),
+            false);
+    Dna dna = new Dna(av, contigs);
     AlignmentI translated = dna.translateCdna();
 
     /*
@@ -313,7 +323,8 @@ public class DnaTest
     }
     AlignmentI cdnaReordered = new Alignment(sorted);
     av = new AlignViewport(cdnaReordered, cs);
-    dna = new Dna(av, new int[] { 0, cdna.getWidth() - 1 });
+    contigs = cs.getVisContigsIterator(0, cdna.getWidth(), false);
+    dna = new Dna(av, contigs);
     AlignmentI translated2 = dna.translateCdna();
 
     /*
@@ -544,7 +555,9 @@ public class DnaTest
 
     HiddenColumns cs = new HiddenColumns();
     AlignViewportI av = new AlignViewport(al, cs);
-    Dna testee = new Dna(av, new int[] { 0, al.getWidth() - 1 });
+    Iterator<int[]> contigs = cs.getVisContigsIterator(0, al.getWidth(),
+            false);
+    Dna testee = new Dna(av, contigs);
     AlignmentI reversed = testee.reverseCdna(false);
     assertEquals(1, reversed.getHeight());
     assertEquals(seqRev, reversed.getSequenceAt(0).getSequenceAsString());
index 70e59c5..e2e5594 100644 (file)
@@ -64,7 +64,7 @@ public class TestAlignSeq
     s2 = new Sequence("Seq2", "ASDFA");
     s2.setStart(5);
     s2.setEnd(9);
-    s3 = new Sequence("Seq1", "SDFAQQQSSS");
+    s3 = new Sequence("Seq3", "SDFAQQQSSS");
 
   }
 
@@ -125,10 +125,10 @@ public class TestAlignSeq
     };
 
     as.printAlignment(ps);
-    String expected = "Score = 320.0\nLength of alignment = 10\nSequence Seq1 :  3 - 18 (Sequence length = 14)\nSequence Seq1 :  1 - 10 (Sequence length = 10)\n\n"
-            + "Seq1 SDFAQQQRRR\n"
-            + "     |||||||   \n"
-            + "Seq1 SDFAQQQSSS\n\n" + "Percentage ID = 70.00\n";
+    String expected = "Score = 320.0\nLength of alignment = 10\nSequence Seq1/4-13 (Sequence length = 14)\nSequence Seq3/1-10 (Sequence length = 10)\n\n"
+            + "Seq1/4-13 SDFAQQQRRR\n"
+            + "          |||||||   \n"
+            + "Seq3/1-10 SDFAQQQSSS\n\n" + "Percentage ID = 70.00\n\n";
     assertEquals(expected, baos.toString());
   }
 }
index 2e89b0e..efee93b 100644 (file)
@@ -25,6 +25,8 @@ import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.analysis.Finder;
 import jalview.api.AlignViewControllerI;
+import jalview.api.FeatureColourI;
+import jalview.datamodel.Alignment;
 import jalview.datamodel.SearchResults;
 import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.Sequence;
@@ -35,7 +37,9 @@ import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
 
+import java.awt.Color;
 import java.util.Arrays;
 import java.util.BitSet;
 
@@ -67,13 +71,14 @@ public class AlignViewControllerTest
             null));
     seq1.addSequenceFeature(new SequenceFeature("Helix", "desc", 1, 15, 0f,
             null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10, 0f,
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "desc", 4, 10,
+            10f,
             null));
     seq3.addSequenceFeature(new SequenceFeature("Metal", "desc", 11, 15,
-            0f, null));
+            10f, null));
     // disulfide bond is a 'contact feature' - only select its 'start' and 'end'
-    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc", 8, 12,
-            0f, null));
+    seq3.addSequenceFeature(new SequenceFeature("disulfide bond", "desc",
+            8, 12, 0f, null));
 
     /*
      * select the first five columns --> Metal in seq1 cols 4-5
@@ -86,9 +91,18 @@ public class AlignViewControllerTest
     sg.addSequence(seq3, false);
     sg.addSequence(seq4, false);
 
+    /*
+     * set features visible on a viewport as only visible features are selected
+     */
+    AlignFrame af = new AlignFrame(new Alignment(new SequenceI[] { seq1,
+        seq2, seq3, seq4 }), 100, 100);
+    af.getFeatureRenderer().findAllFeatures(true);
+
+    AlignViewController avc = new AlignViewController(af, af.getViewport(),
+            af.alignPanel);
+
     BitSet bs = new BitSet();
-    int seqCount = AlignViewController.findColumnsWithFeature("Metal", sg,
-            bs);
+    int seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(3)); // base 0
@@ -99,7 +113,7 @@ public class AlignViewControllerTest
      */
     sg.setEndRes(6);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(2, seqCount);
     assertEquals(4, bs.cardinality());
     assertTrue(bs.get(3));
@@ -113,7 +127,7 @@ public class AlignViewControllerTest
     sg.setStartRes(13);
     sg.setEndRes(13);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(1, bs.cardinality());
     assertTrue(bs.get(13));
@@ -124,18 +138,35 @@ public class AlignViewControllerTest
     sg.setStartRes(17);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Metal", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
     /*
+     * threshold Metal to hide where score < 5
+     * seq1 feature in columns 4-6 is hidden
+     * seq2 feature in columns 6-7 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    af.getFeatureRenderer().setColour("Metal", fc);
+    sg.setStartRes(0);
+    sg.setEndRes(6);
+    bs.clear();
+    seqCount = avc.findColumnsWithFeature("Metal", sg, bs);
+    assertEquals(1, seqCount);
+    assertEquals(2, bs.cardinality());
+    assertTrue(bs.get(5));
+    assertTrue(bs.get(6));
+
+    /*
      * columns 11-13 should not match disulfide bond at 8/12
      */
     sg.setStartRes(10);
     sg.setEndRes(12);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
 
@@ -145,8 +176,7 @@ public class AlignViewControllerTest
     sg.setStartRes(5);
     sg.setEndRes(17);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("disulfide bond",
-            sg, bs);
+    seqCount = avc.findColumnsWithFeature("disulfide bond", sg, bs);
     assertEquals(1, seqCount);
     assertEquals(2, bs.cardinality());
     assertTrue(bs.get(8));
@@ -158,7 +188,7 @@ public class AlignViewControllerTest
     sg.setStartRes(0);
     sg.setEndRes(19);
     bs.clear();
-    seqCount = AlignViewController.findColumnsWithFeature("Pfam", sg, bs);
+    seqCount = avc.findColumnsWithFeature("Pfam", sg, bs);
     assertEquals(0, seqCount);
     assertEquals(0, bs.cardinality());
   }
index e47e9d6..19a725e 100644 (file)
@@ -20,8 +20,8 @@
  */
 package jalview.datamodel;
 
+import static org.testng.Assert.assertNull;
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertNull;
 
 import jalview.analysis.AlignSeq;
 import jalview.gui.JvOptionPane;
@@ -335,4 +335,92 @@ public class AlignmentAnnotationTests
     Assert.assertTrue(ann.isQuantitative(),
             "Mixed 'E' annotation set should be quantitative.");
   }
+
+  @Test(groups = "Functional")
+  public void testMakeVisibleAnnotation()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Annotation[] anns = new Annotation[] { null, null, new Annotation(1),
+        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
+        new Annotation(5), new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    AlignmentAnnotation ann = new AlignmentAnnotation("an", "some an",
+            anns);
+
+    // null annotations
+    AlignmentAnnotation emptyann = new AlignmentAnnotation("an", "some ann",
+            null);
+    emptyann.makeVisibleAnnotation(h);
+    assertNull(emptyann.annotations);
+
+    emptyann.makeVisibleAnnotation(3, 4, h);
+    assertNull(emptyann.annotations);
+
+    // without bounds, does everything
+    ann.makeVisibleAnnotation(h);
+    assertEquals(12, ann.annotations.length);
+    assertNull(ann.annotations[0]);
+    assertNull(ann.annotations[1]);
+    assertEquals(1.0f, ann.annotations[2].value);
+    assertEquals(2.0f, ann.annotations[3].value);
+    assertEquals(3.0f, ann.annotations[4].value);
+    assertNull(ann.annotations[5]);
+    assertNull(ann.annotations[6]);
+    assertEquals(4.0f, ann.annotations[7].value);
+    assertEquals(5.0f, ann.annotations[8].value);
+    assertEquals(6.0f, ann.annotations[9].value);
+    assertEquals(7.0f, ann.annotations[10].value);
+    assertEquals(8.0f, ann.annotations[11].value);
+
+    // without hidden cols, just truncates
+    ann.makeVisibleAnnotation(3, 5, h);
+    assertEquals(3, ann.annotations.length);
+    assertEquals(2.0f, ann.annotations[0].value);
+    assertEquals(3.0f, ann.annotations[1].value);
+    assertNull(ann.annotations[2]);
+
+    anns = new Annotation[] { null, null, new Annotation(1),
+        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
+        new Annotation(5), new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    ann = new AlignmentAnnotation("an", "some an", anns);
+    h.hideColumns(4, 7);
+    ann.makeVisibleAnnotation(1, 9, h);
+    assertEquals(5, ann.annotations.length);
+    assertNull(ann.annotations[0]);
+    assertEquals(1.0f, ann.annotations[1].value);
+    assertEquals(2.0f, ann.annotations[2].value);
+    assertEquals(5.0f, ann.annotations[3].value);
+    assertEquals(6.0f, ann.annotations[4].value);
+
+    anns = new Annotation[] { null, null, new Annotation(1),
+        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
+        new Annotation(5), new Annotation(6), new Annotation(7),
+        new Annotation(8) };
+    ann = new AlignmentAnnotation("an", "some an", anns);
+    h.hideColumns(1, 2);
+    ann.makeVisibleAnnotation(1, 9, h);
+    assertEquals(3, ann.annotations.length);
+    assertEquals(2.0f, ann.annotations[0].value);
+    assertEquals(5.0f, ann.annotations[1].value);
+    assertEquals(6.0f, ann.annotations[2].value);
+
+    anns = new Annotation[] { null, null, new Annotation(1),
+        new Annotation(2), new Annotation(3), null, null, new Annotation(4),
+        new Annotation(5), new Annotation(6), new Annotation(7),
+        new Annotation(8), new Annotation(9), new Annotation(10),
+        new Annotation(11), new Annotation(12), new Annotation(13),
+        new Annotation(14), new Annotation(15) };
+    ann = new AlignmentAnnotation("an", "some an", anns);
+    h = new HiddenColumns();
+    h.hideColumns(5, 18);
+    h.hideColumns(20, 21);
+    ann.makeVisibleAnnotation(1, 21, h);
+    assertEquals(5, ann.annotations.length);
+    assertEquals(1.0f, ann.annotations[1].value);
+    assertEquals(2.0f, ann.annotations[2].value);
+    assertEquals(3.0f, ann.annotations[3].value);
+    assertNull(ann.annotations[0]);
+    assertNull(ann.annotations[4]);
+  }
 }
index 4b5d096..1d1ebd6 100644 (file)
@@ -34,6 +34,7 @@ import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FileFormatI;
 import jalview.io.FormatAdapter;
+import jalview.util.Comparison;
 import jalview.util.MapList;
 
 import java.io.IOException;
@@ -247,7 +248,9 @@ public class AlignmentTest
                   if (raiseAssert)
                   {
                     Assert.fail(message
-                            + " DBRefEntry for sequence in alignment had map to sequence not in dataset");
+                            + " DBRefEntry " + dbr + " for sequence "
+                            + seqds
+                            + " in alignment has map to sequence not in dataset");
                   }
                   return false;
                 }
@@ -668,6 +671,17 @@ public class AlignmentTest
     // third found.. so
     assertFalse(iter.hasNext());
 
+    // search for annotation on one sequence with a particular label - expect
+    // one
+    SequenceI sqfound;
+    anns = al.findAnnotations(sqfound = al.getSequenceAt(1), null,
+            "Secondary Structure");
+    iter = anns.iterator();
+    assertTrue(iter.hasNext());
+    // expect reference to sequence 1 in the alignment
+    assertTrue(sqfound == iter.next().sequenceRef);
+    assertFalse(iter.hasNext());
+
     // null on all parameters == find all annotations
     anns = al.findAnnotations(null, null, null);
     iter = anns.iterator();
@@ -1321,4 +1335,153 @@ public class AlignmentTest
     // todo test coverage for annotations, mappings, groups,
     // hidden sequences, properties
   }
+
+  /**
+   * test that calcId == null on findOrCreate doesn't raise an NPE, and yields
+   * an annotation with a null calcId
+   * 
+   */
+  @Test(groups = "Functional")
+  public void testFindOrCreateForNullCalcId()
+  {
+    SequenceI seq = new Sequence("seq1", "FRMLPSRT-A--L-");
+    AlignmentI alignment = new Alignment(new SequenceI[] { seq });
+
+    AlignmentAnnotation ala = alignment.findOrCreateAnnotation(
+            "Temperature Factor", null, false, seq, null);
+    assertNotNull(ala);
+    assertEquals(seq, ala.sequenceRef);
+    assertEquals("", ala.calcId);
+  }
+
+  @Test(groups = "Functional")
+  public void testPropagateInsertions()
+  {
+    // create an alignment with no gaps - this will be the profile seq and other
+    // JPRED seqs
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(25, 10, 1234, 0, 0);
+
+    // get the profileseq
+    SequenceI profileseq = al.getSequenceAt(0);
+    SequenceI gappedseq = new Sequence(profileseq);
+    gappedseq.insertCharAt(5, al.getGapCharacter());
+    gappedseq.insertCharAt(6, al.getGapCharacter());
+    gappedseq.insertCharAt(7, al.getGapCharacter());
+    gappedseq.insertCharAt(8, al.getGapCharacter());
+
+    // force different kinds of padding
+    al.getSequenceAt(3).deleteChars(2, 23);
+    al.getSequenceAt(4).deleteChars(2, 27);
+    al.getSequenceAt(5).deleteChars(10, 27);
+
+    // create an alignment view with the gapped sequence
+    SequenceI[] seqs = new SequenceI[1];
+    seqs[0] = gappedseq;
+    AlignmentI newal = new Alignment(seqs);
+    HiddenColumns hidden = new HiddenColumns();
+    hidden.hideColumns(15, 17);
+
+    AlignmentView view = new AlignmentView(newal, hidden, null, true, false,
+            false);
+
+    // confirm that original contigs are as expected
+    Iterator<int[]> visible = hidden.getVisContigsIterator(0, 25, false);
+    int[] region = visible.next();
+    assertEquals("[0, 14]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[18, 24]", Arrays.toString(region));
+
+    // propagate insertions
+    HiddenColumns result = al.propagateInsertions(profileseq, view);
+
+    // confirm that the contigs have changed to account for the gaps
+    visible = result.getVisContigsIterator(0, 25, false);
+    region = visible.next();
+    assertEquals("[0, 10]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[14, 24]", Arrays.toString(region));
+
+    // confirm the alignment has been changed so that the other sequences have
+    // gaps inserted where the columns are hidden
+    assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[10]));
+    assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[11]));
+    assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[12]));
+    assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[13]));
+    assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[14]));
+
+  }
+
+  @Test(groups = "Functional")
+  public void testPropagateInsertionsOverlap()
+  {
+    // test propagateInsertions where gaps and hiddenColumns overlap
+
+    // create an alignment with no gaps - this will be the profile seq and other
+    // JPRED seqs
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(20, 10, 1234, 0, 0);
+
+    // get the profileseq
+    SequenceI profileseq = al.getSequenceAt(0);
+    SequenceI gappedseq = new Sequence(profileseq);
+    gappedseq.insertCharAt(5, al.getGapCharacter());
+    gappedseq.insertCharAt(6, al.getGapCharacter());
+    gappedseq.insertCharAt(7, al.getGapCharacter());
+    gappedseq.insertCharAt(8, al.getGapCharacter());
+
+    // create an alignment view with the gapped sequence
+    SequenceI[] seqs = new SequenceI[1];
+    seqs[0] = gappedseq;
+    AlignmentI newal = new Alignment(seqs);
+
+    // hide columns so that some overlap with the gaps
+    HiddenColumns hidden = new HiddenColumns();
+    hidden.hideColumns(7, 10);
+
+    AlignmentView view = new AlignmentView(newal, hidden, null, true, false,
+            false);
+
+    // confirm that original contigs are as expected
+    Iterator<int[]> visible = hidden.getVisContigsIterator(0, 20, false);
+    int[] region = visible.next();
+    assertEquals("[0, 6]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[11, 19]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // propagate insertions
+    HiddenColumns result = al.propagateInsertions(profileseq, view);
+
+    // confirm that the contigs have changed to account for the gaps
+    visible = result.getVisContigsIterator(0, 20, false);
+    region = visible.next();
+    assertEquals("[0, 4]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[7, 19]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // confirm the alignment has been changed so that the other sequences have
+    // gaps inserted where the columns are hidden
+    assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[4]));
+    assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[5]));
+    assertTrue(Comparison.isGap(al.getSequenceAt(1).getSequence()[6]));
+    assertFalse(Comparison.isGap(al.getSequenceAt(1).getSequence()[7]));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testPadGaps()
+  {
+    SequenceI seq1 = new Sequence("seq1", "ABCDEF--");
+    SequenceI seq2 = new Sequence("seq2", "-JKLMNO--");
+    SequenceI seq3 = new Sequence("seq2", "-PQR");
+    AlignmentI a = new Alignment(new SequenceI[] { seq1, seq2, seq3 });
+    a.setGapCharacter('.'); // this replaces existing gaps
+    assertEquals("ABCDEF..", seq1.getSequenceAsString());
+    a.padGaps();
+    // trailing gaps are pruned, short sequences padded with gap character
+    assertEquals("ABCDEF.", seq1.getSequenceAsString());
+    assertEquals(".JKLMNO", seq2.getSequenceAsString());
+    assertEquals(".PQR...", seq3.getSequenceAsString());
+  }
 }
diff --git a/test/jalview/datamodel/CigarArrayTest.java b/test/jalview/datamodel/CigarArrayTest.java
new file mode 100644 (file)
index 0000000..7bee423
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.gui.JvOptionPane;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class CigarArrayTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    SequenceI seq1 = new Sequence("sq1",
+            "ASFDDABACBACBACBACBACBACBABCABCBACBABCAB");
+    Sequence seq2 = new Sequence("sq2",
+            "TTTTTTACBCBABCABCABCABCBACBACBABCABCABCBA");
+
+    // construct alignment
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+
+    // hide columns
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(3, 6);
+    hc.hideColumns(16, 20);
+
+    // select group
+    SequenceGroup sg1 = new SequenceGroup();
+    sg1.addSequence(seq1, false);
+    sg1.setStartRes(2);
+    sg1.setEndRes(23);
+
+    // Cigar array meanings:
+    // M = match
+    // D = deletion
+    // I = insertion
+    // number preceding M/D/I is the number of residues which
+    // match/are deleted/are inserted
+    // In the CigarArray constructor only matches or deletions are created, as
+    // we are comparing a sequence to its own subsequence (the group) + hidden
+    // columns.
+
+    // no hidden columns case
+    CigarArray cig = new CigarArray(al, null, sg1);
+    String result = cig.getCigarstring();
+    assertEquals(result, "22M");
+
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "1M4D9M5D3M");
+
+    // group starts at hidden cols
+    sg1.setStartRes(3);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "4D9M5D3M");
+
+    // group starts at last but 1 hidden col
+    sg1.setStartRes(5);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M5D3M");
+
+    // group starts at last hidden col
+    sg1.setStartRes(6);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "1D9M5D3M");
+
+    // group starts just after hidden region
+    sg1.setStartRes(7);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "9M5D3M");
+
+    // group ends just before start of hidden region
+    sg1.setStartRes(5);
+    sg1.setEndRes(15);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M");
+
+    // group ends at start of hidden region
+    sg1.setEndRes(16);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M1D");
+
+    // group ends 1 after start of hidden region
+    sg1.setEndRes(17);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M2D");
+
+    // group ends at end of hidden region
+    sg1.setEndRes(20);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M5D");
+
+    // group ends just after end of hidden region
+    sg1.setEndRes(21);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M5D1M");
+
+    // group ends 2 after end of hidden region
+    sg1.setEndRes(22);
+    cig = new CigarArray(al, hc, sg1);
+    result = cig.getCigarstring();
+    assertEquals(result, "2D9M5D2M");
+  }
+}
index e99e952..8709961 100644 (file)
@@ -32,6 +32,7 @@ import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collections;
 import java.util.ConcurrentModificationException;
+import java.util.Iterator;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -132,9 +133,9 @@ public class ColumnSelectionTest
     // hide column 5 (and adjacent):
     cs.hideSelectedColumns(5, al.getHiddenColumns());
     // 4,5,6 now hidden:
-    List<int[]> hidden = al.getHiddenColumns().getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    Iterator<int[]> regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     // none now selected:
     assertTrue(cs.getSelected().isEmpty());
 
@@ -145,9 +146,9 @@ public class ColumnSelectionTest
     cs.addElement(5);
     cs.addElement(6);
     cs.hideSelectedColumns(4, al.getHiddenColumns());
-    hidden = al.getHiddenColumns().getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, hiding column (4, 5 and) 6
@@ -157,9 +158,9 @@ public class ColumnSelectionTest
     cs.addElement(5);
     cs.addElement(6);
     cs.hideSelectedColumns(6, al.getHiddenColumns());
-    hidden = al.getHiddenColumns().getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
 
     // repeat, with _only_ adjacent columns selected
@@ -168,9 +169,9 @@ public class ColumnSelectionTest
     cs.addElement(4);
     cs.addElement(6);
     cs.hideSelectedColumns(5, al.getHiddenColumns());
-    hidden = al.getHiddenColumns().getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[4, 6]", Arrays.toString(hidden.get(0)));
+    regions = al.getHiddenColumns().iterator();
+    assertEquals(1, al.getHiddenColumns().getNumberOfRegions());
+    assertEquals("[4, 6]", Arrays.toString(regions.next()));
     assertTrue(cs.getSelected().isEmpty());
   }
 
@@ -196,12 +197,12 @@ public class ColumnSelectionTest
 
     cs.hideSelectedColumns(al);
     assertTrue(cs.getSelected().isEmpty());
-    List<int[]> hidden = cols.getHiddenColumnsCopy();
-    assertEquals(4, hidden.size());
-    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
-    assertEquals("[7, 9]", Arrays.toString(hidden.get(1)));
-    assertEquals("[15, 18]", Arrays.toString(hidden.get(2)));
-    assertEquals("[20, 22]", Arrays.toString(hidden.get(3)));
+    Iterator<int[]> regions = cols.iterator();
+    assertEquals(4, cols.getNumberOfRegions());
+    assertEquals("[2, 4]", Arrays.toString(regions.next()));
+    assertEquals("[7, 9]", Arrays.toString(regions.next()));
+    assertEquals("[15, 18]", Arrays.toString(regions.next()));
+    assertEquals("[20, 22]", Arrays.toString(regions.next()));
   }
 
   /**
diff --git a/test/jalview/datamodel/HiddenColumnsCursorTest.java b/test/jalview/datamodel/HiddenColumnsCursorTest.java
new file mode 100644 (file)
index 0000000..97402b8
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import static org.testng.Assert.assertNull;
+import static org.testng.AssertJUnit.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class HiddenColumnsCursorTest
+{
+
+  @Test(groups = { "Functional" })
+  public void testConstructor()
+  {
+    HiddenColumnsCursor cursor = new HiddenColumnsCursor();
+    assertNull(cursor.findRegionForColumn(0, false));
+
+    List<int[]> hlist = new ArrayList<>();
+    cursor = new HiddenColumnsCursor(hlist);
+    assertNull(cursor.findRegionForColumn(0, false));
+
+    cursor = new HiddenColumnsCursor(hlist, 3, 12);
+    assertNull(cursor.findRegionForColumn(0, false));
+
+    hlist.add(new int[] { 3, 7 });
+    hlist.add(new int[] { 15, 25 });
+    cursor = new HiddenColumnsCursor(hlist);
+    HiddenCursorPosition p = cursor.findRegionForColumn(8, false);
+    assertEquals(1, p.getRegionIndex());
+
+    cursor = new HiddenColumnsCursor(hlist, 1, 5);
+    p = cursor.findRegionForColumn(8, false);
+    assertEquals(1, p.getRegionIndex());
+  }
+
+  /**
+   * Test the method which finds the corresponding region given a column
+   */
+  @Test(groups = { "Functional" })
+  public void testFindRegionForColumn()
+  {
+    HiddenColumnsCursor cursor = new HiddenColumnsCursor();
+    
+    HiddenCursorPosition pos = cursor.findRegionForColumn(20, false);
+    assertNull(pos);
+    
+    List<int[]> hidden = new ArrayList<>();
+    hidden.add(new int[] { 53, 76 });
+    hidden.add(new int[] { 104, 125 });
+
+    cursor = new HiddenColumnsCursor(hidden);
+
+    int regionIndex = cursor.findRegionForColumn(126, false).getRegionIndex();
+    assertEquals(2, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(125, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(108, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(104, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(103, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(77, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(76, false).getRegionIndex();
+    assertEquals(0, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(53, false).getRegionIndex();
+    assertEquals(0, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(52, false).getRegionIndex();
+    assertEquals(0, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(0, false).getRegionIndex();
+    assertEquals(0, regionIndex);
+
+    hidden.add(new int[] { 138, 155 });
+
+    cursor = new HiddenColumnsCursor(hidden);
+
+    regionIndex = cursor.findRegionForColumn(160, false).getRegionIndex();
+    assertEquals(3, regionIndex);
+
+    regionIndex = cursor.findRegionForColumn(100, false).getRegionIndex();
+    assertEquals(1, regionIndex);
+  }
+
+  /**
+   * Test the method which counts the number of hidden columns before a column
+   */
+  @Test(groups = { "Functional" })
+  public void testFindRegionForColumn_Visible()
+  {
+    HiddenColumnsCursor cursor = new HiddenColumnsCursor();
+
+    HiddenCursorPosition pos = cursor.findRegionForColumn(20, true);
+    assertNull(pos);
+
+    List<int[]> hidden = new ArrayList<>();
+    hidden.add(new int[] { 53, 76 });
+    hidden.add(new int[] { 104, 125 });
+
+    cursor = new HiddenColumnsCursor(hidden);
+
+    int offset = cursor.findRegionForColumn(80, true).getHiddenSoFar();
+    assertEquals(46, offset);
+
+    offset = cursor.findRegionForColumn(79, true).getHiddenSoFar();
+    assertEquals(24, offset);
+
+    offset = cursor.findRegionForColumn(53, true).getHiddenSoFar();
+    assertEquals(24, offset);
+
+    offset = cursor.findRegionForColumn(52, true).getHiddenSoFar();
+    assertEquals(0, offset);
+
+    offset = cursor.findRegionForColumn(10, true).getHiddenSoFar();
+    assertEquals(0, offset);
+
+    offset = cursor.findRegionForColumn(0, true).getHiddenSoFar();
+    assertEquals(0, offset);
+
+    offset = cursor.findRegionForColumn(79, true).getHiddenSoFar();
+    assertEquals(24, offset);
+
+    offset = cursor.findRegionForColumn(80, true).getHiddenSoFar();
+    assertEquals(46, offset);
+  }
+}
index 7c88d71..2916199 100644 (file)
@@ -26,26 +26,15 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.analysis.AlignmentGenerator;
-import jalview.gui.JvOptionPane;
 
 import java.util.Arrays;
 import java.util.BitSet;
-import java.util.List;
-import java.util.Random;
+import java.util.Iterator;
 
-import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 public class HiddenColumnsTest
 {
-
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
-  {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
-  }
-
   /**
    * Test the method which counts the number of hidden columns
    */
@@ -77,22 +66,22 @@ public class HiddenColumnsTest
   public void testFindColumnPosition()
   {
     HiddenColumns cs = new HiddenColumns();
-    assertEquals(5, cs.findColumnPosition(5));
+    assertEquals(5, cs.absoluteToVisibleColumn(5));
 
     // hiding column 6 makes no difference
     cs.hideColumns(6, 6);
-    assertEquals(5, cs.findColumnPosition(5));
+    assertEquals(5, cs.absoluteToVisibleColumn(5));
 
     // hiding column 4 moves column 5 to column 4
     cs.hideColumns(4, 4);
-    assertEquals(4, cs.findColumnPosition(5));
+    assertEquals(4, cs.absoluteToVisibleColumn(5));
 
     // hiding column 4 moves column 4 to position 3
-    assertEquals(3, cs.findColumnPosition(4));
+    assertEquals(3, cs.absoluteToVisibleColumn(4));
 
     // hiding columns 1 and 2 moves column 5 to column 2
     cs.hideColumns(1, 2);
-    assertEquals(2, cs.findColumnPosition(5));
+    assertEquals(2, cs.absoluteToVisibleColumn(5));
 
     // check with > 1 hidden column regions
     // where some columns are in the hidden regions
@@ -102,105 +91,82 @@ public class HiddenColumnsTest
     cs2.hideColumns(40, 44);
 
     // hiding columns 5-10 and 20-27 moves column 8 to column 4
-    assertEquals(4, cs2.findColumnPosition(8));
+    assertEquals(4, cs2.absoluteToVisibleColumn(8));
 
     // and moves column 24 to 13
-    assertEquals(13, cs2.findColumnPosition(24));
+    assertEquals(13, cs2.absoluteToVisibleColumn(24));
 
     // and moves column 28 to 14
-    assertEquals(14, cs2.findColumnPosition(28));
+    assertEquals(14, cs2.absoluteToVisibleColumn(28));
 
     // and moves column 40 to 25
-    assertEquals(25, cs2.findColumnPosition(40));
+    assertEquals(25, cs2.absoluteToVisibleColumn(40));
 
     // check when hidden columns start at 0 that the visible column
     // is returned as 0
     HiddenColumns cs3 = new HiddenColumns();
     cs3.hideColumns(0, 4);
-    assertEquals(0, cs3.findColumnPosition(2));
+    assertEquals(0, cs3.absoluteToVisibleColumn(2));
 
+    // check that column after the last hidden region doesn't crash
+    assertEquals(46, cs2.absoluteToVisibleColumn(65));
   }
 
-  /**
-   * Test the method that finds the visible column position a given distance
-   * before another column
-   */
   @Test(groups = { "Functional" })
-  public void testFindColumnNToLeft()
+  public void testVisibleContigsIterator()
   {
     HiddenColumns cs = new HiddenColumns();
 
-    // test that without hidden columns, findColumnNToLeft returns
-    // position n to left of provided position
-    int pos = cs.subtractVisibleColumns(3, 10);
-    assertEquals(7, pos);
-
-    // 0 returns same position
-    pos = cs.subtractVisibleColumns(0, 10);
-    assertEquals(10, pos);
-
-    // overflow to left returns negative number
-    pos = cs.subtractVisibleColumns(3, 0);
-    assertEquals(-3, pos);
-
-    // test that with hidden columns to left of result column
-    // behaviour is the same as above
-    cs.hideColumns(1, 3);
-
-    // position n to left of provided position
-    pos = cs.subtractVisibleColumns(3, 10);
-    assertEquals(7, pos);
-
-    // 0 returns same position
-    pos = cs.subtractVisibleColumns(0, 10);
-    assertEquals(10, pos);
-
-    // test with one set of hidden columns between start and required position
-    cs.hideColumns(12, 15);
-    pos = cs.subtractVisibleColumns(8, 17);
-    assertEquals(5, pos);
-
-    // test with two sets of hidden columns between start and required position
-    cs.hideColumns(20, 21);
-    pos = cs.subtractVisibleColumns(8, 23);
-    assertEquals(9, pos);
-
-    // repeat last 2 tests with no hidden columns to left of required position
-    ColumnSelection colsel = new ColumnSelection();
-    cs.revealAllHiddenColumns(colsel);
-
-    // test with one set of hidden columns between start and required position
-    cs.hideColumns(12, 15);
-    pos = cs.subtractVisibleColumns(8, 17);
-    assertEquals(5, pos);
-
-    // test with two sets of hidden columns between start and required position
-    cs.hideColumns(20, 21);
-    pos = cs.subtractVisibleColumns(8, 23);
-    assertEquals(9, pos);
-
-  }
+    Iterator<int[]> visible = cs.getVisContigsIterator(3, 10, false);
+    int[] region = visible.next();
+    assertEquals("[3, 9]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
 
-  @Test(groups = { "Functional" })
-  public void testGetVisibleContigs()
-  {
-    HiddenColumns cs = new HiddenColumns();
     cs.hideColumns(3, 6);
     cs.hideColumns(8, 9);
     cs.hideColumns(12, 12);
 
-    // start position is inclusive, end position exclusive:
-    int[] visible = cs.getVisibleContigs(1, 13);
-    assertEquals("[1, 2, 7, 7, 10, 11]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(4, 14);
-    assertEquals("[7, 7, 10, 11, 13, 13]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(3, 10);
-    assertEquals("[7, 7]", Arrays.toString(visible));
-
-    visible = cs.getVisibleContigs(4, 6);
-    assertEquals("[]", Arrays.toString(visible));
+    // Test both ends visible region
+
+    // start position is inclusive, end position exclusive
+    visible = cs.getVisContigsIterator(1, 13, false);
+    region = visible.next();
+    assertEquals("[1, 2]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[7, 7]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[10, 11]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // Test start hidden, end visible
+    visible = cs.getVisContigsIterator(4, 14, false);
+    region = visible.next();
+    assertEquals("[7, 7]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[10, 11]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[13, 13]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // Test start hidden, end hidden
+    visible = cs.getVisContigsIterator(3, 10, false);
+    region = visible.next();
+    assertEquals("[7, 7]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // Test start visible, end hidden
+    visible = cs.getVisContigsIterator(0, 13, false);
+    region = visible.next();
+    assertEquals("[0, 2]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[7, 7]", Arrays.toString(region));
+    region = visible.next();
+    assertEquals("[10, 11]", Arrays.toString(region));
+    assertFalse(visible.hasNext());
+
+    // Test empty result
+    visible = cs.getVisContigsIterator(4, 6, false);
+    assertFalse(visible.hasNext());
   }
 
   @Test(groups = { "Functional" })
@@ -216,14 +182,31 @@ public class HiddenColumnsTest
     assertFalse(cs.equals(cs2));
     assertFalse(cs2.equals(cs));
 
+    // with the wrong kind of object
+    assertFalse(cs.equals(new HiddenColumnsCursor()));
+
+    // with a different hiddenColumns object - by size
+    HiddenColumns cs3 = new HiddenColumns();
+    cs3.hideColumns(2, 3);
+    assertFalse(cs.equals(cs3));
+
     // with hidden columns added in a different order
     cs2.hideColumns(6, 9);
+    assertFalse(cs.equals(cs2));
+    assertFalse(cs2.equals(cs));
+
     cs2.hideColumns(5, 8);
 
     assertTrue(cs.equals(cs2));
     assertTrue(cs.equals(cs));
     assertTrue(cs2.equals(cs));
     assertTrue(cs2.equals(cs2));
+
+    // different ranges, same size
+    cs.hideColumns(10, 12);
+    cs2.hideColumns(10, 15);
+    assertFalse(cs.equals(cs2));
+
   }
 
   @Test(groups = "Functional")
@@ -232,102 +215,50 @@ public class HiddenColumnsTest
     HiddenColumns cs = new HiddenColumns();
     cs.hideColumns(10, 11);
     cs.hideColumns(5, 7);
+    Iterator<int[]> regions = cs.iterator();
     assertEquals("[5, 7]",
-            Arrays.toString(cs.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
 
     HiddenColumns cs2 = new HiddenColumns(cs);
+    regions = cs2.iterator();
     assertTrue(cs2.hasHiddenColumns());
-    assertEquals(2, cs2.getHiddenColumnsCopy().size());
+    assertEquals(2, cs2.getNumberOfRegions());
     // hidden columns are held in column order
     assertEquals("[5, 7]",
-            Arrays.toString(cs2.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
     assertEquals("[10, 11]",
-            Arrays.toString(cs2.getHiddenColumnsCopy().get(1)));
+            Arrays.toString(regions.next()));
   }
 
-  /**
-   * Test the code used to locate the reference sequence ruler origin
-   */
-  @Test(groups = { "Functional" })
-  public void testLocateVisibleBoundsofSequence()
+  @Test(groups = "Functional")
+  public void testCopyConstructor2()
   {
-    // create random alignment
-    AlignmentGenerator gen = new AlignmentGenerator(false);
-    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+    HiddenColumns cs = new HiddenColumns();
+    cs.hideColumns(10, 11);
+    cs.hideColumns(5, 7);
 
-    HiddenColumns cs = al.getHiddenColumns();
-    ColumnSelection colsel = new ColumnSelection();
+    HiddenColumns cs2 = new HiddenColumns(cs, 3, 9, 1);
+    assertTrue(cs2.hasHiddenColumns());
+    Iterator<int[]> regions = cs2.iterator();
 
-    SequenceI seq = new Sequence("RefSeq", "-A-SD-ASD--E---");
-    assertEquals(2, seq.findIndex(seq.getStart()));
-
-    // no hidden columns
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-    // hidden column on gap after end of sequence - should not affect bounds
-    colsel.hideSelectedColumns(13, al.getHiddenColumns());
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
+    // only [5,7] returned, offset by 1
+    assertEquals("[4, 6]",
+            Arrays.toString(regions.next()));
+    assertEquals(3, cs2.getSize());
 
-    cs.revealAllHiddenColumns(colsel);
-    // hidden column on gap before beginning of sequence - should vis bounds by
-    // one
-    colsel.hideSelectedColumns(0, al.getHiddenColumns());
-    assertEquals(
-            Arrays.toString(new int[] { seq.findIndex(seq.getStart()) - 2,
-                seq.findIndex(seq.getEnd()) - 2, seq.getStart(),
-                seq.getEnd(), seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-
-    cs.revealAllHiddenColumns(colsel);
-    // hide columns around most of sequence - leave one residue remaining
-    cs.hideColumns(1, 3);
-    cs.hideColumns(6, 11);
-    assertEquals("-D",
-            cs.getVisibleSequenceStrings(0, 5, new SequenceI[] { seq })[0]);
-    assertEquals(
-            Arrays.toString(new int[] { 1, 1, 3, 3,
-                seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
-    cs.revealAllHiddenColumns(colsel);
+    cs2 = new HiddenColumns(cs, 8, 15, 4);
+    regions = cs2.iterator();
+    assertTrue(cs2.hasHiddenColumns());
 
-    // hide whole sequence - should just get location of hidden region
-    // containing sequence
-    cs.hideColumns(1, 11);
-    assertEquals(
-            Arrays.toString(new int[] { 0, 1, 0, 0,
-                seq.findIndex(seq.getStart()) - 1,
-                seq.findIndex(seq.getEnd()) - 1 }),
-            Arrays.toString(cs.locateVisibleBoundsOfSequence(seq)));
+    // only [10,11] returned, offset by 4
+    assertEquals("[6, 7]",
+            Arrays.toString(regions.next()));
+    assertEquals(2, cs2.getSize());
 
+    cs2 = new HiddenColumns(cs, 6, 10, 4);
+    assertFalse(cs2.hasHiddenColumns());
   }
 
-  @Test(groups = { "Functional" })
-  public void testLocateVisibleBoundsPathologicals()
-  {
-    // test some pathological cases we missed
-    AlignmentI al = new Alignment(new SequenceI[] { new Sequence(
-            "refseqGaptest", "KTDVTI----------NFI-----G----L") });
-    HiddenColumns cs = new HiddenColumns();
-    cs.hideInsertionsFor(al.getSequenceAt(0));
-    assertEquals(
-            "G",
-            ""
-                    + al.getSequenceAt(0).getCharAt(
-                            cs.adjustForHiddenColumns(9)));
-
-  }
 
   @Test(groups = { "Functional" })
   public void testHideColumns()
@@ -339,80 +270,103 @@ public class HiddenColumnsTest
     ColumnSelection colsel = new ColumnSelection();
     HiddenColumns cs = al.getHiddenColumns();
     colsel.hideSelectedColumns(5, al.getHiddenColumns());
-    List<int[]> hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(0)));
+    Iterator<int[]> regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[5, 5]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 1);
 
     colsel.hideSelectedColumns(3, al.getHiddenColumns());
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(2, hidden.size());
+    regions = cs.iterator();
+    assertEquals(2, cs.getNumberOfRegions());
     // two hidden ranges, in order:
-    assertEquals(hidden.size(), cs.getHiddenColumnsCopy().size());
-    assertEquals("[3, 3]", Arrays.toString(hidden.get(0)));
-    assertEquals("[5, 5]", Arrays.toString(hidden.get(1)));
+    assertEquals("[3, 3]", Arrays.toString(regions.next()));
+    assertEquals("[5, 5]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 2);
 
     // hiding column 4 expands [3, 3] to [3, 4]
     // and merges to [5, 5] to make [3, 5]
     colsel.hideSelectedColumns(4, al.getHiddenColumns());
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[3, 5]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[3, 5]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 3);
 
     // clear hidden columns (note they are added to selected)
     cs.revealAllHiddenColumns(colsel);
     // it is now actually null but getter returns an empty list
-    assertTrue(cs.getHiddenColumnsCopy().isEmpty());
+    assertEquals(0, cs.getNumberOfRegions());
+    assertEquals(cs.getSize(), 0);
 
     cs.hideColumns(3, 6);
-    hidden = cs.getHiddenColumnsCopy();
-    int[] firstHiddenRange = hidden.get(0);
+    regions = cs.iterator();
+    int[] firstHiddenRange = regions.next();
     assertEquals("[3, 6]", Arrays.toString(firstHiddenRange));
+    assertEquals(cs.getSize(), 4);
 
     // adding a subrange of already hidden should do nothing
     cs.hideColumns(4, 5);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
     assertEquals("[3, 6]",
-            Arrays.toString(cs.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 4);
     cs.hideColumns(3, 5);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
     assertEquals("[3, 6]",
-            Arrays.toString(cs.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 4);
     cs.hideColumns(4, 6);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
     assertEquals("[3, 6]",
-            Arrays.toString(cs.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 4);
     cs.hideColumns(3, 6);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
     assertEquals("[3, 6]",
-            Arrays.toString(cs.getHiddenColumnsCopy().get(0)));
+            Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 4);
 
     cs.revealAllHiddenColumns(colsel);
     cs.hideColumns(2, 4);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 4]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[2, 4]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 3);
 
     // extend contiguous with 2 positions overlap
     cs.hideColumns(3, 5);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 5]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[2, 5]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 4);
 
     // extend contiguous with 1 position overlap
     cs.hideColumns(5, 6);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[2, 6]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[2, 6]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 5);
 
     // extend contiguous with overlap both ends:
     cs.hideColumns(1, 7);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[1, 7]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[1, 7]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 7);
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(15, 18);
+    cs.hideColumns(2, 4);
+    cs.hideColumns(7, 9);
+    regions = cs.iterator();
+    assertEquals(3, cs.getNumberOfRegions());
+    assertEquals("[2, 4]", Arrays.toString(regions.next()));
+    assertEquals("[7, 9]", Arrays.toString(regions.next()));
+    assertEquals("[15, 18]", Arrays.toString(regions.next()));
+    assertEquals(cs.getSize(), 10);
   }
 
   /**
@@ -424,11 +378,18 @@ public class HiddenColumnsTest
   {
     ColumnSelection colsel = new ColumnSelection();
     HiddenColumns cs = new HiddenColumns();
+
+    // test with null hidden columns
+    cs.revealHiddenColumns(5, colsel);
+    assertTrue(colsel.getSelected().isEmpty());
+
     cs.hideColumns(5, 8);
     colsel.addElement(10);
     cs.revealHiddenColumns(5, colsel);
-    // hidden columns list now null but getter returns empty list:
-    assertTrue(cs.getHiddenColumnsCopy().isEmpty());
+
+    // hiddenColumns now empty
+    assertEquals(0, cs.getSize());
+
     // revealed columns are marked as selected (added to selection):
     assertEquals("[10, 5, 6, 7, 8]", colsel.getSelected().toString());
 
@@ -436,36 +397,67 @@ public class HiddenColumnsTest
     colsel = new ColumnSelection();
     cs = new HiddenColumns();
     cs.hideColumns(5, 8);
-    List<int[]> hidden = cs.getHiddenColumnsCopy();
+
+    int prevSize = cs.getSize();
     cs.revealHiddenColumns(6, colsel);
-    assertEquals(hidden.size(), cs.getHiddenColumnsCopy().size());
+    assertEquals(prevSize, cs.getSize());
+    assertTrue(colsel.getSelected().isEmpty());
+
+    // reveal hidden columns when there is more than one region
+    cs.hideColumns(20, 23);
+    // now there are 2 hidden regions
+    assertEquals(2, cs.getNumberOfRegions());
+
+    cs.revealHiddenColumns(20, colsel);
+
+    // hiddenColumns now has one region
+    assertEquals(1, cs.getNumberOfRegions());
+
+    // revealed columns are marked as selected (added to selection):
+    assertEquals("[20, 21, 22, 23]", colsel.getSelected().toString());
+
+    // call with a column past the end of the hidden column ranges
+    colsel.clear();
+    cs.revealHiddenColumns(20, colsel);
+    // hiddenColumns still has 1 region
+    assertEquals(1, cs.getNumberOfRegions());
     assertTrue(colsel.getSelected().isEmpty());
   }
 
   @Test(groups = { "Functional" })
   public void testRevealAllHiddenColumns()
   {
-    HiddenColumns cs = new HiddenColumns();
+    HiddenColumns hidden = new HiddenColumns();
     ColumnSelection colsel = new ColumnSelection();
-    cs.hideColumns(5, 8);
-    cs.hideColumns(2, 3);
+
+    // test with null hidden columns
+    hidden.revealAllHiddenColumns(colsel);
+    assertTrue(colsel.getSelected().isEmpty());
+
+    hidden.hideColumns(5, 8);
+    hidden.hideColumns(2, 3);
     colsel.addElement(11);
     colsel.addElement(1);
-    cs.revealAllHiddenColumns(colsel);
+    hidden.revealAllHiddenColumns(colsel);
 
     /*
      * revealing hidden columns adds them (in order) to the (unordered)
      * selection list
      */
-    assertTrue(cs.getHiddenColumnsCopy().isEmpty());
-    assertEquals("[11, 1, 2, 3, 5, 6, 7, 8]", colsel.getSelected()
-            .toString());
+
+    // hiddenColumns now empty
+    assertEquals(0, hidden.getSize());
+
+    assertEquals("[11, 1, 2, 3, 5, 6, 7, 8]",
+            colsel.getSelected().toString());
   }
 
   @Test(groups = { "Functional" })
   public void testIsVisible()
   {
     HiddenColumns cs = new HiddenColumns();
+    assertTrue(cs.isVisible(5));
+
     cs.hideColumns(2, 4);
     cs.hideColumns(6, 7);
     assertTrue(cs.isVisible(0));
@@ -477,6 +469,7 @@ public class HiddenColumnsTest
     assertTrue(cs.isVisible(5));
     assertFalse(cs.isVisible(6));
     assertFalse(cs.isVisible(7));
+    assertTrue(cs.isVisible(8));
   }
 
   /**
@@ -493,15 +486,17 @@ public class HiddenColumnsTest
     HiddenColumns cs = new HiddenColumns();
     cs.hideColumns(49, 59);
     cs.hideColumns(69, 79);
-    List<int[]> hidden = cs.getHiddenColumnsCopy();
-    assertEquals(2, hidden.size());
-    assertEquals("[49, 59]", Arrays.toString(hidden.get(0)));
-    assertEquals("[69, 79]", Arrays.toString(hidden.get(1)));
+    Iterator<int[]> regions = cs.iterator();
+    assertEquals(2, cs.getNumberOfRegions());
+    assertEquals("[49, 59]", Arrays.toString(regions.next()));
+    assertEquals("[69, 79]", Arrays.toString(regions.next()));
+    assertEquals(22, cs.getSize());
 
     cs.hideColumns(48, 80);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[48, 80]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[48, 80]", Arrays.toString(regions.next()));
+    assertEquals(33, cs.getSize());
 
     /*
      * another...joining hidden ranges
@@ -512,9 +507,10 @@ public class HiddenColumnsTest
     cs.hideColumns(50, 60);
     // hiding 21-49 should merge to one range
     cs.hideColumns(21, 49);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[10, 60]", Arrays.toString(hidden.get(0)));
+    regions = cs.iterator();
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals("[10, 60]", Arrays.toString(regions.next()));
+    assertEquals(51, cs.getSize());
 
     /*
      * another...left overlap, subsumption, right overlap,
@@ -528,14 +524,15 @@ public class HiddenColumnsTest
     cs.hideColumns(60, 70);
 
     cs.hideColumns(15, 45);
-    hidden = cs.getHiddenColumnsCopy();
-    assertEquals(2, hidden.size());
-    assertEquals("[10, 50]", Arrays.toString(hidden.get(0)));
-    assertEquals("[60, 70]", Arrays.toString(hidden.get(1)));
+    regions = cs.iterator();
+    assertEquals(2, cs.getNumberOfRegions());
+    assertEquals("[10, 50]", Arrays.toString(regions.next()));
+    assertEquals("[60, 70]", Arrays.toString(regions.next()));
+    assertEquals(52, cs.getSize());
   }
 
   @Test(groups = { "Functional" })
-  public void testHideBitset()
+  public void testHideColumns_BitSet()
   {
     HiddenColumns cs;
 
@@ -544,80 +541,45 @@ public class HiddenColumnsTest
     // one hidden range
     one.set(1);
     cs = new HiddenColumns();
-    cs.hideMarkedBits(one);
-    assertEquals(1, cs.getHiddenColumnsCopy().size());
+    cs.hideColumns(one);
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals(1, cs.getSize());
 
     one.set(2);
     cs = new HiddenColumns();
-    cs.hideMarkedBits(one);
-    assertEquals(1, cs.getHiddenColumnsCopy().size());
+    cs.hideColumns(one);
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals(2, cs.getSize());
 
     one.set(3);
     cs = new HiddenColumns();
-    cs.hideMarkedBits(one);
-    assertEquals(1, cs.getHiddenColumnsCopy().size());
+    cs.hideColumns(one);
+    assertEquals(1, cs.getNumberOfRegions());
+    assertEquals(3, cs.getSize());
 
     // split
     one.clear(2);
     cs = new HiddenColumns();
-    cs.hideMarkedBits(one);
-    assertEquals(2, cs.getHiddenColumnsCopy().size());
+    cs.hideColumns(one);
+    assertEquals(2, cs.getNumberOfRegions());
+    assertEquals(2, cs.getSize());
 
-    assertEquals(0, cs.adjustForHiddenColumns(0));
-    assertEquals(2, cs.adjustForHiddenColumns(1));
-    assertEquals(4, cs.adjustForHiddenColumns(2));
+    assertEquals(0, cs.visibleToAbsoluteColumn(0));
+    assertEquals(2, cs.visibleToAbsoluteColumn(1));
+    assertEquals(4, cs.visibleToAbsoluteColumn(2));
 
     // one again
     one.clear(1);
     cs = new HiddenColumns();
-    cs.hideMarkedBits(one);
+    cs.hideColumns(one);
+    assertEquals(1, cs.getSize());
 
-    assertEquals(1, cs.getHiddenColumnsCopy().size());
+    assertEquals(1, cs.getNumberOfRegions());
 
-    assertEquals(0, cs.adjustForHiddenColumns(0));
-    assertEquals(1, cs.adjustForHiddenColumns(1));
-    assertEquals(2, cs.adjustForHiddenColumns(2));
-    assertEquals(4, cs.adjustForHiddenColumns(3));
-  }
-
-  @Test(groups = { "Functional" })
-  public void testGetBitset()
-  {
-    BitSet toMark, fromMark;
-    long seed = -3241532;
-    Random number = new Random(seed);
-    for (int n = 0; n < 1000; n++)
-    {
-      // create a random bitfield
-      toMark = BitSet.valueOf(new long[] { number.nextLong(),
-          number.nextLong(), number.nextLong() });
-      toMark.set(n * number.nextInt(10), n * (25 + number.nextInt(25)));
-      HiddenColumns hc = new HiddenColumns();
-      hc.hideMarkedBits(toMark);
-
-      // see if we can recover bitfield
-      hc.markHiddenRegions(fromMark = new BitSet());
-      assertEquals(toMark, fromMark);
-    }
-  }
-
-  @Test(groups = { "Functional" })
-  public void testFindHiddenRegionPositions()
-  {
-    HiddenColumns hc = new HiddenColumns();
-
-    List<Integer> positions = hc.findHiddenRegionPositions();
-    assertTrue(positions.isEmpty());
-
-    hc.hideColumns(3, 7);
-    hc.hideColumns(10, 10);
-    hc.hideColumns(14, 15);
-
-    positions = hc.findHiddenRegionPositions();
-    assertEquals(3, positions.size());
-    assertEquals(3, positions.get(0).intValue());
-    assertEquals(5, positions.get(1).intValue());
-    assertEquals(8, positions.get(2).intValue());
+    assertEquals(0, cs.visibleToAbsoluteColumn(0));
+    assertEquals(1, cs.visibleToAbsoluteColumn(1));
+    assertEquals(2, cs.visibleToAbsoluteColumn(2));
+    assertEquals(4, cs.visibleToAbsoluteColumn(3));
   }
 
   @Test(groups = { "Functional" })
@@ -637,7 +599,7 @@ public class HiddenColumnsTest
   }
 
   @Test(groups = "Functional")
-  public void getVisibleStartAndEndIndexTest()
+  public void testGetVisibleStartAndEndIndex()
   {
     Sequence seq = new Sequence("testSeq", "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
     AlignmentI align = new Alignment(new SequenceI[] { seq });
@@ -663,6 +625,13 @@ public class HiddenColumnsTest
     System.out.println(startEnd[0] + " : " + startEnd[1]);
     assertEquals(1, startEnd[0]);
     assertEquals(23, startEnd[1]);
+
+    // force lowest range to start of alignment
+    hc = new HiddenColumns();
+    hc.hideColumns(3, 4);
+    startEnd = hc.getVisibleStartAndEndIndex(align.getWidth());
+    assertEquals(0, startEnd[0]);
+    assertEquals(25, startEnd[1]);
   }
 
   @Test(groups = "Functional")
@@ -677,58 +646,683 @@ public class HiddenColumnsTest
     hc.hideColumns(10, 10);
     hc.hideColumns(14, 15);
 
-    result = hc.getRegionWithEdgeAtRes(3);
+    result = hc.getRegionWithEdgeAtRes(2);
     assertEquals(3, result[0]);
     assertEquals(7, result[1]);
 
+    result = hc.getRegionWithEdgeAtRes(4);
+    assertEquals(10, result[0]);
+    assertEquals(10, result[1]);
+
     result = hc.getRegionWithEdgeAtRes(5);
     assertEquals(10, result[0]);
     assertEquals(10, result[1]);
 
     result = hc.getRegionWithEdgeAtRes(6);
     assertNull(result);
+
+    result = hc.getRegionWithEdgeAtRes(0);
+    assertNull(result);
+
+    result = hc.getRegionWithEdgeAtRes(7);
+    assertEquals(14, result[0]);
+    assertEquals(15, result[1]);
+
+    result = hc.getRegionWithEdgeAtRes(8);
+    assertEquals(14, result[0]);
+    assertEquals(15, result[1]);
+
+    result = hc.getRegionWithEdgeAtRes(16);
+    assertNull(result);
   }
 
   @Test(groups = "Functional")
-  public void testPropagateInsertions()
+  public void testHasHiddenColumns()
   {
-    // create an alignment with no gaps - this will be the profile seq and other
-    // JPRED seqs
-    AlignmentGenerator gen = new AlignmentGenerator(false);
-    AlignmentI al = gen.generate(20, 10, 1234, 0, 0);
-
-    // get the profileseq
-    SequenceI profileseq = al.getSequenceAt(0);
-    SequenceI gappedseq = new Sequence(profileseq);
-    gappedseq.insertCharAt(5, al.getGapCharacter());
-    gappedseq.insertCharAt(6, al.getGapCharacter());
-    gappedseq.insertCharAt(7, al.getGapCharacter());
-    gappedseq.insertCharAt(8, al.getGapCharacter());
-    
-    // create an alignment view with the gapped sequence
-    SequenceI[] seqs = new SequenceI[1];
-    seqs[0] = gappedseq;
-    AlignmentI newal = new Alignment(seqs);
-    HiddenColumns hidden = new HiddenColumns();
-    hidden.hideColumns(15, 17);
-
-    AlignmentView view = new AlignmentView(newal, hidden, null, true, false,
-            false);
-
-    // confirm that original contigs are as expected
-    int[] oldcontigs = hidden.getVisibleContigs(0, 20);
-    int[] testcontigs = { 0, 14, 18, 19 };
-    assertTrue(Arrays.equals(oldcontigs, testcontigs));
-            
-    // propagate insertions
-    HiddenColumns result = HiddenColumns.propagateInsertions(profileseq, al,
-            view);
-
-    // confirm that the contigs have changed to account for the gaps
-    int[] newcontigs = result.getVisibleContigs(0, 20);
-    testcontigs[1] = 10;
-    testcontigs[2] = 14;
-    assertTrue(Arrays.equals(newcontigs, testcontigs));
-    
+    HiddenColumns h = new HiddenColumns();
+
+    // new HiddenColumns2 has no hidden cols
+    assertFalse(h.hasHiddenColumns());
+
+    // some columns hidden, returns true
+    h.hideColumns(5, 10);
+    assertTrue(h.hasHiddenColumns());
+
+    // reveal columns, no hidden cols again
+    ColumnSelection sel = new ColumnSelection();
+    h.revealAllHiddenColumns(sel);
+    assertFalse(h.hasHiddenColumns());
+  }
+
+  @Test(groups = "Functional")
+  public void testHasManyHiddenColumns()
+  {
+    HiddenColumns h = new HiddenColumns();
+
+    // h has no hidden cols
+    assertFalse(h.hasMultiHiddenColumnRegions());
+
+    // one set of columns hidden, returns false
+    h.hideColumns(5, 10);
+    assertFalse(h.hasMultiHiddenColumnRegions());
+
+    // two sets hidden, returns true
+    h.hideColumns(15, 17);
+    assertTrue(h.hasMultiHiddenColumnRegions());
+
+    // back to one block, asserts false
+    h.hideColumns(11, 14);
+    assertFalse(h.hasMultiHiddenColumnRegions());
+  }
+
+  @Test(groups = "Functional")
+  public void testAdjustForHiddenColumns()
+  {
+    HiddenColumns h = new HiddenColumns();
+    // returns input value when there are no hidden columns
+    assertEquals(10, h.visibleToAbsoluteColumn(10));
+
+    h.hideColumns(20, 30);
+    assertEquals(10, h.visibleToAbsoluteColumn(10));
+    assertEquals(20 + 11, h.visibleToAbsoluteColumn(20));
+    assertEquals(35 + 11, h.visibleToAbsoluteColumn(35));
+
+    h.hideColumns(5, 7);
+    assertEquals(10 + 3, h.visibleToAbsoluteColumn(10));
+    assertEquals(20 + 14, h.visibleToAbsoluteColumn(20));
+    assertEquals(35 + 14, h.visibleToAbsoluteColumn(35));
+
+    ColumnSelection sel = new ColumnSelection();
+    h.revealAllHiddenColumns(sel);
+    h.hideColumns(0, 1);
+    assertEquals(4, h.visibleToAbsoluteColumn(2));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetNextHiddenBoundary_Left()
+  {
+    HiddenColumns h = new HiddenColumns();
+
+    // returns same value if no hidden cols
+    assertEquals(3, h.getNextHiddenBoundary(true, 3));
+
+    h.hideColumns(5, 10);
+    assertEquals(10, h.getNextHiddenBoundary(true, 15));
+    assertEquals(3, h.getNextHiddenBoundary(true, 3));
+    assertEquals(7, h.getNextHiddenBoundary(true, 7));
+
+    h.hideColumns(15, 20);
+    assertEquals(10, h.getNextHiddenBoundary(true, 15));
+    assertEquals(20, h.getNextHiddenBoundary(true, 21));
+  }
+
+  @Test(groups = "Functional")
+  public void testGetNextHiddenBoundary_Right()
+  {
+    HiddenColumns h = new HiddenColumns();
+
+    // returns same value if no hidden cols
+    assertEquals(3, h.getNextHiddenBoundary(false, 3));
+
+    h.hideColumns(5, 10);
+    assertEquals(5, h.getNextHiddenBoundary(false, 3));
+    assertEquals(15, h.getNextHiddenBoundary(false, 15));
+    assertEquals(7, h.getNextHiddenBoundary(false, 7));
+
+    h.hideColumns(15, 20);
+    assertEquals(15, h.getNextHiddenBoundary(false, 7));
+    assertEquals(15, h.getNextHiddenBoundary(false, 14));
+
+    // returns same value if there is no next hidden column
+    assertEquals(22, h.getNextHiddenBoundary(false, 22));
+  }
+
+  @Test(groups = "Functional")
+  public void testIterator()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Iterator<int[]> result = h.iterator();
+    assertFalse(result.hasNext());
+
+    h.hideColumns(5, 10);
+    result = h.iterator();
+    int[] next = result.next();
+    assertEquals(5, next[0]);
+    assertEquals(10, next[1]);
+    assertFalse(result.hasNext());
+
+    h.hideColumns(22, 23);
+    result = h.iterator();
+    next = result.next();
+    assertEquals(5, next[0]);
+    assertEquals(10, next[1]);
+    next = result.next();
+    assertEquals(22, next[0]);
+    assertEquals(23, next[1]);
+    assertFalse(result.hasNext());
+
+    // test for only one hidden region at start of alignment
+    ColumnSelection sel = new ColumnSelection();
+    h.revealAllHiddenColumns(sel);
+    h.hideColumns(0, 1);
+    result = h.iterator();
+    next = result.next();
+    assertEquals(0, next[0]);
+    assertEquals(1, next[1]);
+    assertFalse(result.hasNext());
+  }
+
+  /* @Test(groups = "Functional")
+  public void testGetVisibleSequenceStrings()
+  {
+    HiddenColumns h = new HiddenColumns();
+    SequenceI seq1 = new Sequence("TEST1", "GALMFWKQESPVICYHRNDT");
+    SequenceI seq2 = new Sequence("TEST2", "VICYHRNDTGA");
+    SequenceI[] seqs = new SequenceI[2];
+    seqs[0] = seq1;
+    seqs[1] = seq2;
+    String[] result = h.getVisibleSequenceStrings(5, 10, seqs);
+    assertEquals(2, result.length);
+    assertEquals("WKQES", result[0]);
+    assertEquals("RNDTG", result[1]);
+  
+    h.hideColumns(6, 8);
+    result = h.getVisibleSequenceStrings(5, 10, seqs);
+    assertEquals(2, result.length);
+    assertEquals("WS", result[0]);
+    assertEquals("RG", result[1]);
+  
+    SequenceI seq = new Sequence("RefSeq", "-A-SD-ASD--E---");
+    ColumnSelection sel = new ColumnSelection();
+    h.revealAllHiddenColumns(sel);
+    h.hideColumns(1, 3);
+    h.hideColumns(6, 11);
+    assertEquals("-D",
+            h.getVisibleSequenceStrings(0, 5, new SequenceI[]
+    { seq })[0]);
+  }*/
+
+  @Test(groups = "Functional")
+  public void testHideInsertionsFor()
+  {
+    HiddenColumns h = new HiddenColumns();
+    HiddenColumns h2 = new HiddenColumns();
+    SequenceI seq1 = new Sequence("TEST1", "GAL---MFW-KQESPVICY--HRNDT");
+    SequenceI seq2 = new Sequence("TEST1", "GALMFWKQESPVICYHRNDT");
+
+    h.hideList(seq2.getInsertions());
+    assertTrue(h.equals(h2));
+    assertEquals(0, h.getSize());
+
+    h.hideList(seq1.getInsertions());
+    h2.hideColumns(3, 5);
+    h2.hideColumns(9, 9);
+    h2.hideColumns(19, 20);
+    assertTrue(h.equals(h2));
+    assertEquals(6, h.getSize());
+  }
+
+  @Test(groups = "Functional")
+  public void testHideColumns_BitSet_range()
+  {
+    HiddenColumns h = new HiddenColumns();
+    HiddenColumns h2 = new HiddenColumns();
+
+    BitSet tohide = new BitSet(25);
+    h.hideColumns(tohide);
+    assertTrue(h.equals(h2));
+
+    // when setting bitset, first param is inclusive, second exclusive
+    tohide.set(3, 6);
+    tohide.set(9);
+    tohide.set(15, 21);
+    h.clearAndHideColumns(tohide, 5, 23);
+
+    h2.hideColumns(5, 5);
+    h2.hideColumns(9, 9);
+    h2.hideColumns(15, 20);
+    assertTrue(h.equals(h2));
+    assertEquals(h.getSize(), h2.getSize());
+
+    tohide.clear();
+    tohide.set(41);
+    h.clearAndHideColumns(tohide, 23, 30);
+    assertTrue(h.equals(h2));
+    assertEquals(h.getSize(), h2.getSize());
+
+    tohide.set(41);
+    h.clearAndHideColumns(tohide, 30, 45);
+    h2.hideColumns(41, 41);
+    assertTrue(h.equals(h2));
+    assertEquals(h.getSize(), h2.getSize());
+
+    tohide.clear();
+    tohide.set(25, 28);
+    h.clearAndHideColumns(tohide, 17, 50);
+    h2 = new HiddenColumns();
+    h2.hideColumns(5, 5);
+    h2.hideColumns(9, 9);
+    h2.hideColumns(15, 16);
+    h2.hideColumns(25, 27);
+    assertTrue(h.equals(h2));
+    assertEquals(h.getSize(), h2.getSize());
+
+    HiddenColumns hc = new HiddenColumns();
+    hc.hideColumns(3, 5);
+    hc.hideColumns(15, 20);
+    hc.hideColumns(45, 60);
+
+    tohide = new BitSet();
+
+    // all unhidden if tohide is empty and range covers hidden
+    hc.clearAndHideColumns(tohide, 1, 70);
+    assertTrue(!hc.hasHiddenColumns());
+    assertEquals(0, hc.getSize());
+
+    hc.hideColumns(3, 5);
+    hc.hideColumns(15, 20);
+    hc.hideColumns(45, 60);
+    assertEquals(25, hc.getSize());
+
+    // but not if range does not cover hidden
+    hc.clearAndHideColumns(tohide, 23, 40);
+    assertTrue(hc.hasHiddenColumns());
+    assertEquals(25, hc.getSize());
+
+    // and partial unhide if range partially covers
+    hc.clearAndHideColumns(tohide, 1, 17);
+    Iterator<int[]> it = hc.iterator();
+    assertTrue(it.hasNext());
+    int[] region = it.next();
+
+    assertEquals(18, region[0]);
+    assertEquals(20, region[1]);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+
+    assertEquals(45, region[0]);
+    assertEquals(60, region[1]);
+
+    assertFalse(it.hasNext());
+    assertEquals(19, hc.getSize());
+  }
+
+  @Test(groups = "Functional")
+  public void testOffsetByVisibleColumns()
+  {
+    HiddenColumns h = new HiddenColumns();
+    int result = h.offsetByVisibleColumns(-1, 10);
+    assertEquals(9, result);
+
+    h.hideColumns(7, 9);
+    result = h.offsetByVisibleColumns(-4, 10);
+    assertEquals(3, result);
+
+    h.hideColumns(14, 15);
+    result = h.offsetByVisibleColumns(-4, 10);
+    assertEquals(3, result);
+
+    result = h.offsetByVisibleColumns(-10, 17);
+    assertEquals(2, result);
+
+    result = h.offsetByVisibleColumns(-1, 7);
+    assertEquals(5, result);
+
+    result = h.offsetByVisibleColumns(-1, 8);
+    assertEquals(5, result);
+
+    result = h.offsetByVisibleColumns(-3, 15);
+    assertEquals(10, result);
+
+    ColumnSelection sel = new ColumnSelection();
+    h.revealAllHiddenColumns(sel);
+    h.hideColumns(0, 30);
+    result = h.offsetByVisibleColumns(-31, 0);
+    assertEquals(-31, result);
+
+    HiddenColumns cs = new HiddenColumns();
+
+    // test that without hidden columns, offsetByVisibleColumns returns
+    // position n to left of provided position
+    long pos = cs.offsetByVisibleColumns(-3, 10);
+    assertEquals(7, pos);
+
+    // 0 returns same position
+    pos = cs.offsetByVisibleColumns(0, 10);
+    assertEquals(10, pos);
+
+    // overflow to left returns negative number
+    pos = cs.offsetByVisibleColumns(-3, 0);
+    assertEquals(-3, pos);
+
+    // test that with hidden columns to left of result column
+    // behaviour is the same as above
+    cs.hideColumns(1, 3);
+
+    // position n to left of provided position
+    pos = cs.offsetByVisibleColumns(-3, 10);
+    assertEquals(7, pos);
+
+    // 0 returns same position
+    pos = cs.offsetByVisibleColumns(0, 10);
+    assertEquals(10, pos);
+
+    // test with one set of hidden columns between start and required position
+    cs.hideColumns(12, 15);
+    pos = cs.offsetByVisibleColumns(-8, 17);
+    assertEquals(5, pos);
+
+    // test with two sets of hidden columns between start and required position
+    cs.hideColumns(20, 21);
+    pos = cs.offsetByVisibleColumns(-8, 23);
+    assertEquals(9, pos);
+
+    // repeat last 2 tests with no hidden columns to left of required position
+    ColumnSelection colsel = new ColumnSelection();
+    cs.revealAllHiddenColumns(colsel);
+
+    // test with one set of hidden columns between start and required position
+    cs.hideColumns(12, 15);
+    pos = cs.offsetByVisibleColumns(-8, 17);
+    assertEquals(5, pos);
+
+    // test with two sets of hidden columns between start and required position
+    cs.hideColumns(20, 21);
+    pos = cs.offsetByVisibleColumns(-8, 23);
+    assertEquals(9, pos);
+
+    // test with right (positive) offsets
+
+    // test that without hidden columns, offsetByVisibleColumns returns
+    // position n to right of provided position
+    pos = cs.offsetByVisibleColumns(3, 7);
+    assertEquals(10, pos);
+
+    // test that with hidden columns to left of result column
+    // behaviour is the same as above
+    cs.hideColumns(1, 3);
+
+    // test with one set of hidden columns between start and required position
+    cs.hideColumns(12, 15);
+    pos = cs.offsetByVisibleColumns(8, 5);
+    assertEquals(17, pos);
+
+    // test with two sets of hidden columns between start and required position
+    cs.hideColumns(20, 21);
+    pos = cs.offsetByVisibleColumns(8, 9);
+    assertEquals(23, pos);
+
+    // repeat last 2 tests with no hidden columns to left of required position
+    colsel = new ColumnSelection();
+    cs.revealAllHiddenColumns(colsel);
+
+    // test with one set of hidden columns between start and required position
+    cs.hideColumns(12, 15);
+    pos = cs.offsetByVisibleColumns(8, 5);
+    assertEquals(17, pos);
+
+    // test with two sets of hidden columns between start and required position
+    cs.hideColumns(20, 21);
+    pos = cs.offsetByVisibleColumns(8, 9);
+    assertEquals(23, pos);
+  }
+
+  @Test(groups = "Functional")
+  public void testBoundedIterator()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Iterator<int[]> it = h.getBoundedIterator(0, 10);
+
+    // no hidden columns = nothing to iterate over
+    assertFalse(it.hasNext());
+
+    // [start,end] contains all hidden columns
+    // all regions are returned
+    h.hideColumns(3, 10);
+    h.hideColumns(14, 16);
+    it = h.getBoundedIterator(0, 20);
+    assertTrue(it.hasNext());
+    int[] next = it.next();
+    assertEquals(3, next[0]);
+    assertEquals(10, next[1]);
+    next = it.next();
+    assertEquals(14, next[0]);
+    assertEquals(16, next[1]);
+    assertFalse(it.hasNext());
+
+    // [start,end] overlaps a region
+    // 1 region returned
+    it = h.getBoundedIterator(5, 7);
+    assertTrue(it.hasNext());
+    next = it.next();
+    assertEquals(3, next[0]);
+    assertEquals(10, next[1]);
+    assertFalse(it.hasNext());
+
+    // [start,end] fully contains 1 region and start of last
+    // - 2 regions returned
+    it = h.getBoundedIterator(3, 15);
+    assertTrue(it.hasNext());
+    next = it.next();
+    assertEquals(3, next[0]);
+    assertEquals(10, next[1]);
+    next = it.next();
+    assertEquals(14, next[0]);
+    assertEquals(16, next[1]);
+    assertFalse(it.hasNext());
+
+    // [start,end] contains end of first region and whole of last region
+    // - 2 regions returned
+    it = h.getBoundedIterator(4, 20);
+    assertTrue(it.hasNext());
+    next = it.next();
+    assertEquals(3, next[0]);
+    assertEquals(10, next[1]);
+    next = it.next();
+    assertEquals(14, next[0]);
+    assertEquals(16, next[1]);
+    assertFalse(it.hasNext());
+  }
+
+  @Test(groups = "Functional")
+  public void testBoundedStartIterator()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Iterator<Integer> it = h.getStartRegionIterator(0, 10);
+
+    // no hidden columns = nothing to iterate over
+    assertFalse(it.hasNext());
+
+    // [start,end] contains all hidden columns
+    // all regions are returned
+    h.hideColumns(3, 10);
+    h.hideColumns(14, 16);
+    it = h.getStartRegionIterator(0, 20);
+    assertTrue(it.hasNext());
+    int next = it.next();
+    assertEquals(3, next);
+    next = it.next();
+    assertEquals(6, next);
+    assertFalse(it.hasNext());
+
+    // [start,end] does not contain a start of a region
+    // no regions to iterate over
+    it = h.getStartRegionIterator(4, 5);
+    assertFalse(it.hasNext());
+
+    // [start,end] fully contains 1 region and start of last
+    // - 2 regions returned
+    it = h.getStartRegionIterator(3, 7);
+    assertTrue(it.hasNext());
+    next = it.next();
+    assertEquals(3, next);
+    next = it.next();
+    assertEquals(6, next);
+    assertFalse(it.hasNext());
+
+    // [start,end] contains whole of last region
+    // - 1 region returned
+    it = h.getStartRegionIterator(4, 20);
+    assertTrue(it.hasNext());
+    next = it.next();
+    assertEquals(6, next);
+    assertFalse(it.hasNext());
+  }
+
+  @Test(groups = "Functional")
+  public void testVisibleBlocksVisBoundsIterator()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Iterator<int[]> regions = h.getVisContigsIterator(0, 31, true);
+
+    // only 1 visible region spanning 0-30 if nothing is hidden
+    assertTrue(regions.hasNext());
+    int[] region = regions.next();
+    assertEquals(0, region[0]);
+    assertEquals(30, region[1]);
+    assertFalse(regions.hasNext());
+
+    // hide 1 region in middle
+    // 2 regions one on either side
+    // second region boundary accounts for hidden columns
+    h.hideColumns(10, 15);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(0, region[0]);
+    assertEquals(9, region[1]);
+    region = regions.next();
+    assertEquals(16, region[0]);
+    assertEquals(36, region[1]);
+    assertFalse(regions.hasNext());
+
+    // single hidden region at left
+    h = new HiddenColumns();
+    h.hideColumns(0, 5);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(6, region[0]);
+    assertEquals(36, region[1]);
+    assertFalse(regions.hasNext());
+
+    // single hidden region at right
+    h = new HiddenColumns();
+    h.hideColumns(27, 30);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(0, region[0]);
+    assertEquals(26, region[1]);
+    region = regions.next();
+    assertEquals(31, region[0]);
+    assertEquals(34, region[1]);
+    assertFalse(regions.hasNext());
+
+    // hidden region at left + hidden region in middle
+    h = new HiddenColumns();
+    h.hideColumns(0, 5);
+    h.hideColumns(23, 25);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(6, region[0]);
+    assertEquals(22, region[1]);
+    region = regions.next();
+    assertEquals(26, region[0]);
+    assertEquals(39, region[1]);
+    assertFalse(regions.hasNext());
+
+    // hidden region at right + hidden region in middle
+    h = new HiddenColumns();
+    h.hideColumns(27, 30);
+    h.hideColumns(11, 14);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(0, region[0]);
+    assertEquals(10, region[1]);
+    region = regions.next();
+    assertEquals(15, region[0]);
+    assertEquals(26, region[1]);
+    region = regions.next();
+    assertEquals(31, region[0]);
+    assertEquals(38, region[1]);
+    assertFalse(regions.hasNext());
+
+    // hidden region at left and right
+    h = new HiddenColumns();
+    h.hideColumns(27, 35);
+    h.hideColumns(0, 4);
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(5, region[0]);
+    assertEquals(26, region[1]);
+    region = regions.next();
+    assertEquals(36, region[0]);
+    assertEquals(44, region[1]);
+    assertFalse(regions.hasNext());
+
+    // multiple hidden regions
+    h = new HiddenColumns();
+    h.hideColumns(1, 1);
+    h.hideColumns(3, 5);
+    h.hideColumns(9, 11);
+    h.hideColumns(22, 26);
+
+    regions = h.getVisContigsIterator(0, 31, true);
+
+    assertTrue(regions.hasNext());
+    region = regions.next();
+    assertEquals(0, region[0]);
+    assertEquals(0, region[1]);
+    region = regions.next();
+    assertEquals(2, region[0]);
+    assertEquals(2, region[1]);
+    region = regions.next();
+    assertEquals(6, region[0]);
+    assertEquals(8, region[1]);
+    region = regions.next();
+    assertEquals(12, region[0]);
+    assertEquals(21, region[1]);
+    region = regions.next();
+    assertEquals(27, region[0]);
+    assertEquals(42, region[1]);
+    assertFalse(regions.hasNext());
+  }
+
+  /*
+   * the VisibleColsIterator is tested elsewhere, this just tests that 
+   * it can be retrieved from HiddenColumns
+   */
+  @Test(groups = "Functional")
+  public void testGetVisibleColsIterator()
+  {
+    HiddenColumns h = new HiddenColumns();
+    Iterator<Integer> it = h.getVisibleColsIterator(0, 10);
+
+    assertTrue(it instanceof RangeElementsIterator);
+  }
+
+  @Test(groups = "Functional")
+  public void testHashCode()
+  {
+    HiddenColumns h = new HiddenColumns();
+    h.hideColumns(0, 25);
+
+    int result = h.hashCode();
+    assertTrue(result > 0);
+
+    h.hideColumns(30, 50);
+    assertTrue(h.hashCode() > 0);
+    assertTrue(result != h.hashCode());
   }
 }
@@ -22,12 +22,13 @@ package jalview.datamodel;
 
 import static org.testng.Assert.assertTrue;
 
+import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
-public class VisibleColsIteratorTest
+public class RangeElementsIteratorTest
 {
   HiddenColumns hiddenCols;
 
@@ -50,11 +51,12 @@ public class VisibleColsIteratorTest
   @Test(groups = { "Functional" })
   public void testHasNextAndNextWithHidden()
   {
-    VisibleColsIterator it = new VisibleColsIterator(0, 6, hiddenCols);
+    Iterator<Integer> it = hiddenCols.getVisibleColsIterator(0, 6);
     int count = 0;
     while (it.hasNext())
     {
-      it.next();
+      int result = it.next();
+      System.out.println(result);
       count++;
     }
     assertTrue(count == 4, "hasNext() is false after 4 iterations");
@@ -67,8 +69,8 @@ public class VisibleColsIteratorTest
   @Test(groups = { "Functional" })
   public void testHasNextAndNextNoHidden()
   {
-    VisibleColsIterator it2 = new VisibleColsIterator(0, 3,
-            new HiddenColumns());
+    HiddenColumns test = new HiddenColumns();
+    Iterator<Integer> it2 = test.getVisibleColsIterator(0, 3);
     int count = 0;
     while (it2.hasNext())
     {
@@ -85,8 +87,7 @@ public class VisibleColsIteratorTest
   @Test(groups = { "Functional" })
   public void testHasNextAndNextStartHidden()
   {
-    VisibleColsIterator it3 = new VisibleColsIterator(0, 6,
-            hiddenColsAtStart);
+    Iterator<Integer> it3 = hiddenColsAtStart.getVisibleColsIterator(0, 6);
     int count = 0;
     while (it3.hasNext())
     {
@@ -103,7 +104,7 @@ public class VisibleColsIteratorTest
   @Test(groups = { "Functional" })
   public void testHasNextAndNextEndHidden()
   {
-    VisibleColsIterator it4 = new VisibleColsIterator(0, 4, hiddenCols);
+    Iterator<Integer> it4 = hiddenCols.getVisibleColsIterator(0, 4);
     int count = 0;
     while (it4.hasNext())
     {
@@ -123,7 +124,7 @@ public class VisibleColsIteratorTest
     expectedExceptions = { NoSuchElementException.class })
   public void testLastNextWithHidden() throws NoSuchElementException
   {
-    VisibleColsIterator it = new VisibleColsIterator(0, 3, hiddenCols);
+    Iterator<Integer> it = hiddenCols.getVisibleColsIterator(0, 3);
     while (it.hasNext())
     {
       it.next();
@@ -140,8 +141,8 @@ public class VisibleColsIteratorTest
     expectedExceptions = { NoSuchElementException.class })
   public void testLastNextNoHidden() throws NoSuchElementException
   {
-    VisibleColsIterator it2 = new VisibleColsIterator(0, 3,
-            new HiddenColumns());
+    HiddenColumns test = new HiddenColumns();
+    Iterator<Integer> it2 = test.getVisibleColsIterator(0, 3);
     while (it2.hasNext())
     {
       it2.next();
@@ -158,8 +159,7 @@ public class VisibleColsIteratorTest
     expectedExceptions = { NoSuchElementException.class })
   public void testLastNextStartHidden() throws NoSuchElementException
   {
-    VisibleColsIterator it3 = new VisibleColsIterator(0, 6,
-            hiddenColsAtStart);
+    Iterator<Integer> it3 = hiddenColsAtStart.getVisibleColsIterator(0, 6);
     while (it3.hasNext())
     {
       it3.next();
@@ -176,7 +176,7 @@ public class VisibleColsIteratorTest
     expectedExceptions = { NoSuchElementException.class })
   public void testLastNextEndHidden() throws NoSuchElementException
   {
-    VisibleColsIterator it4 = new VisibleColsIterator(0, 4, hiddenCols);
+    Iterator<Integer> it4 = hiddenCols.getVisibleColsIterator(0, 4);
     while (it4.hasNext())
     {
       it4.next();
@@ -192,7 +192,7 @@ public class VisibleColsIteratorTest
     expectedExceptions = { UnsupportedOperationException.class })
   public void testRemove() throws UnsupportedOperationException
   {
-    VisibleColsIterator it = new VisibleColsIterator(0, 3, hiddenCols);
+    Iterator<Integer> it = hiddenCols.getVisibleColsIterator(0, 3);
     it.remove();
   }
 }
index fbeb365..c955979 100644 (file)
@@ -273,4 +273,47 @@ public class SequenceFeatureTest
             "group");
     assertTrue(sf.isContactFeature());
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetDetailsReport()
+  {
+    // single locus, no group, no score
+    SequenceFeature sf = new SequenceFeature("variant", "G,C", 22, 22, null);
+    String expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    // contact feature
+    sf = new SequenceFeature("Disulphide Bond", "a description", 28, 31,
+            null);
+    expected = "<br><table><tr><td>Type</td><td>Disulphide Bond</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>28:31</td><td></td></tr>"
+            + "<tr><td>Description</td><td>a description</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    sf = new SequenceFeature("variant", "G,C", 22, 33,
+            12.5f, "group");
+    sf.setValue("Parent", "ENSG001");
+    sf.setValue("Child", "ENSP002");
+    expected = "<br><table><tr><td>Type</td><td>variant</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>22-33</td><td></td></tr>"
+            + "<tr><td>Description</td><td>G,C</td><td></td></tr>"
+            + "<tr><td>Score</td><td>12.5</td><td></td></tr>"
+            + "<tr><td>Group</td><td>group</td><td></td></tr>"
+            + "<tr><td>Child</td><td></td><td>ENSP002</td></tr>"
+            + "<tr><td>Parent</td><td></td><td>ENSG001</td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+
+    /*
+     * feature with embedded html link in description
+     */
+    String desc = "<html>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></html>";
+    sf = new SequenceFeature("Pfam", desc, 8, 83, "Uniprot");
+    expected = "<br><table><tr><td>Type</td><td>Pfam</td><td></td></tr>"
+            + "<tr><td>Start/end</td><td>8-83</td><td></td></tr>"
+            + "<tr><td>Description</td><td>Fer2 Status: True Positive <a href=\"http://pfam.xfam.org/family/PF00111\">Pfam 8_8</a></td><td></td></tr>"
+            + "<tr><td>Group</td><td>Uniprot</td><td></td></tr></table>";
+    assertEquals(expected, sf.getDetailsReport());
+  }
 }
index 6844072..79bb2bb 100644 (file)
@@ -28,6 +28,7 @@ import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.analysis.AlignmentGenerator;
 import jalview.commands.EditCommand;
 import jalview.commands.EditCommand.Action;
 import jalview.datamodel.PDBEntry.Type;
@@ -38,16 +39,17 @@ import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Vector;
 
-import junit.extensions.PA;
-
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class SequenceTest
 {
 
@@ -245,7 +247,9 @@ public class SequenceTest
     sq.sequenceChanged();
     assertEquals(6, sq.findIndex(9));
 
-    sq = new Sequence("test/8-13", "-A--B-C-D-E-F--");
+    final String aligned = "-A--B-C-D-E-F--";
+    assertEquals(15, aligned.length());
+    sq = new Sequence("test/8-13", aligned);
     assertEquals(2, sq.findIndex(8));
     sq.sequenceChanged();
     assertEquals(5, sq.findIndex(9));
@@ -261,6 +265,29 @@ public class SequenceTest
     // beyond end returns last residue column
     sq.sequenceChanged();
     assertEquals(13, sq.findIndex(99));
+
+    /*
+     * residue before sequence 'end' but beyond end of sequence returns 
+     * length of sequence (last column) (rightly or wrongly!)
+     */
+    sq = new Sequence("test/8-15", "A-B-C-"); // trailing gap case
+    assertEquals(6, sq.getLength());
+    sq.sequenceChanged();
+    assertEquals(sq.getLength(), sq.findIndex(14));
+    sq = new Sequence("test/8-99", "-A--B-C-D"); // trailing residue case
+    sq.sequenceChanged();
+    assertEquals(sq.getLength(), sq.findIndex(65));
+
+    /*
+     * residue after sequence 'start' but before first residue returns 
+     * zero (before first column) (rightly or wrongly!)
+     */
+    sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+    sq.sequenceChanged();
+    assertEquals(0, sq.findIndex(3));
+    sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+    sq.sequenceChanged();
+    assertEquals(0, sq.findIndex(2));
   }
 
   /**
@@ -799,7 +826,7 @@ public class SequenceTest
     Assert.assertEquals(pdbe1a,
             sq.getDatasetSequence().getPDBEntry("1PDB"),
             "PDB Entry '1PDB' not found on dataset sequence via getPDBEntry.");
-    ArrayList<Annotation> annotsList = new ArrayList<Annotation>();
+    ArrayList<Annotation> annotsList = new ArrayList<>();
     System.out.println(">>>>>> " + sq.getSequenceAsString().length());
     annotsList.add(new Annotation("A", "A", 'X', 0.1f));
     annotsList.add(new Annotation("A", "A", 'X', 0.1f));
@@ -1400,14 +1427,61 @@ public class SequenceTest
   {
     Sequence sq = new Sequence("test/8-13", "-A--BCD-EF--");
 
-    // find F given A
+    // find F given A, check cursor is now at the found position
     assertEquals(10, sq.findIndex(13, new SequenceCursor(sq, 8, 2, 0)));
+    SequenceCursor cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(13, cursor.residuePosition);
+    assertEquals(10, cursor.columnPosition);
 
     // find A given F
     assertEquals(2, sq.findIndex(8, new SequenceCursor(sq, 13, 10, 0)));
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(8, cursor.residuePosition);
+    assertEquals(2, cursor.columnPosition);
 
-    // find C given C
+    // find C given C (no cursor update is done for this case)
     assertEquals(6, sq.findIndex(10, new SequenceCursor(sq, 10, 6, 0)));
+    SequenceCursor cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor2, cursor);
+
+    /*
+     * sequence 'end' beyond end of sequence returns length of sequence 
+     *  (for compatibility with pre-cursor code)
+     *  - also verify the cursor is left in a valid state
+     */
+    sq = new Sequence("test/8-99", "-A--B-C-D-E-F--"); // trailing gap case
+    assertEquals(7, sq.findIndex(10)); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(10, cursor.residuePosition);
+    assertEquals(7, cursor.columnPosition);
+    assertEquals(sq.getLength(), sq.findIndex(65));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    sq = new Sequence("test/8-99", "-A--B-C-D-E-F"); // trailing residue case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(sq.getLength(), sq.findIndex(65));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    /*
+     * residue after sequence 'start' but before first residue should return 
+     * zero (for compatibility with pre-cursor code)
+     */
+    sq = new Sequence("test/8-15", "-A-B-C-"); // leading gap case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(0, sq.findIndex(3));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
+
+    sq = new Sequence("test/8-15", "A-B-C-"); // leading residue case
+    sq.findIndex(10); // establishes a cursor
+    cursor = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertEquals(0, sq.findIndex(2));
+    cursor2 = (SequenceCursor) PA.getValue(sq, "cursor");
+    assertSame(cursor, cursor2); // not updated for this case!
   }
 
   @Test(groups = { "Functional" })
@@ -1672,4 +1746,385 @@ public class SequenceTest
     assertEquals(new Range(8, 13), sq.findPositions(1, 13)); // the lot
     assertEquals(new Range(8, 13), sq.findPositions(1, 99));
   }
+
+  @Test(groups = { "Functional" })
+  public void testGapBitset()
+  {
+    SequenceI sq = new Sequence("test/8-13", "-ABC---DE-F--");
+    BitSet bs = sq.gapBitset();
+    BitSet expected = new BitSet();
+    expected.set(0);
+    expected.set(4, 7);
+    expected.set(9);
+    expected.set(11, 13);
+
+    assertTrue(bs.equals(expected));
+
+  }
+
+  public void testFindFeatures_largeEndPos()
+  {
+    /*
+     * imitate a PDB sequence where end is larger than end position
+     */
+    SequenceI sq = new Sequence("test", "-ABC--DEF--", 1, 20);
+    sq.createDatasetSequence();
+  
+    assertTrue(sq.findFeatures(1, 9).isEmpty());
+    // should be no array bounds exception - JAL-2772
+    assertTrue(sq.findFeatures(1, 15).isEmpty());
+  
+    // add feature on BCD
+    SequenceFeature sfBCD = new SequenceFeature("Cath", "desc", 2, 4, 2f,
+            null);
+    sq.addSequenceFeature(sfBCD);
+  
+    // no features in columns 1-2 (-A)
+    List<SequenceFeature> found = sq.findFeatures(1, 2);
+    assertTrue(found.isEmpty());
+  
+    // columns 1-6 (-ABC--) includes BCD
+    found = sq.findFeatures(1, 6);
+    assertEquals(1, found.size());
+    assertTrue(found.contains(sfBCD));
+
+    // columns 10-11 (--) should find nothing
+    found = sq.findFeatures(10, 11);
+    assertEquals(0, found.size());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetName()
+  {
+    SequenceI sq = new Sequence("test", "-ABC---DE-F--");
+    assertEquals("test", sq.getName());
+    assertEquals(1, sq.getStart());
+    assertEquals(6, sq.getEnd());
+
+    sq.setName("testing");
+    assertEquals("testing", sq.getName());
+
+    sq.setName("test/8-10");
+    assertEquals("test", sq.getName());
+    assertEquals(8, sq.getStart());
+    assertEquals(13, sq.getEnd()); // note end is recomputed
+
+    sq.setName("testing/7-99");
+    assertEquals("testing", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd()); // end may be beyond physical end
+
+    sq.setName("/2-3");
+    assertEquals("", sq.getName());
+    assertEquals(2, sq.getStart());
+    assertEquals(7, sq.getEnd());
+
+    sq.setName("test/"); // invalid
+    assertEquals("test/", sq.getName());
+    assertEquals(2, sq.getStart());
+    assertEquals(7, sq.getEnd());
+
+    sq.setName("test/6-13/7-99");
+    assertEquals("test/6-13", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/0-5"); // 0 is invalid - ignored
+    assertEquals("test/0-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/a-5"); // a is invalid - ignored
+    assertEquals("test/a-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/6-5"); // start > end is invalid - ignored
+    assertEquals("test/6-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5"); // invalid - ignored
+    assertEquals("test/5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/-5"); // invalid - ignored
+    assertEquals("test/-5", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5-"); // invalid - ignored
+    assertEquals("test/5-", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName("test/5-6-7"); // invalid - ignored
+    assertEquals("test/5-6-7", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+
+    sq.setName(null); // invalid, gets converted to space
+    assertEquals("", sq.getName());
+    assertEquals(7, sq.getStart());
+    assertEquals(99, sq.getEnd());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCheckValidRange()
+  {
+    Sequence sq = new Sequence("test/7-12", "-ABC---DE-F--");
+    assertEquals(7, sq.getStart());
+    assertEquals(12, sq.getEnd());
+
+    /*
+     * checkValidRange ensures end is at least the last residue position
+     */
+    PA.setValue(sq, "end", 2);
+    sq.checkValidRange();
+    assertEquals(12, sq.getEnd());
+
+    /*
+     * end may be beyond the last residue position
+     */
+    PA.setValue(sq, "end", 22);
+    sq.checkValidRange();
+    assertEquals(22, sq.getEnd());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testDeleteChars_withGaps()
+  {
+    /*
+     * delete gaps only
+     */
+    SequenceI sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+    sq.deleteChars(1, 2); // delete first gap
+    assertEquals("AB-C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(10, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues at start (no new dataset sequence)
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(0, 3); // delete A-B
+    assertEquals("-C", sq.getSequenceAsString());
+    assertEquals(10, sq.getStart());
+    assertEquals(10, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues at end (no new dataset sequence)
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(2, 5); // delete B-C
+    assertEquals("A-", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(8, sq.getEnd());
+    assertEquals("ABC", sq.getDatasetSequence().getSequenceAsString());
+
+    /*
+     * delete gaps and residues internally (new dataset sequence)
+     * first delete from gap to residue
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(1, 3); // delete -B
+    assertEquals("A-C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, sq.getDatasetSequence().getStart());
+    assertEquals(9, sq.getDatasetSequence().getEnd());
+
+    /*
+     * internal delete from gap to gap
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(1, 4); // delete -B-
+    assertEquals("AC", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, sq.getDatasetSequence().getStart());
+    assertEquals(9, sq.getDatasetSequence().getEnd());
+
+    /*
+     * internal delete from residue to residue
+     */
+    sq = new Sequence("test/8-10", "A-B-C");
+    sq.createDatasetSequence();
+    sq.deleteChars(2, 3); // delete B
+    assertEquals("A--C", sq.getSequenceAsString());
+    assertEquals(8, sq.getStart());
+    assertEquals(9, sq.getEnd());
+    assertEquals("AC", sq.getDatasetSequence().getSequenceAsString());
+    assertEquals(8, sq.getDatasetSequence().getStart());
+    assertEquals(9, sq.getDatasetSequence().getEnd());
+  }
+
+  /**
+   * Test the code used to locate the reference sequence ruler origin
+   */
+  @Test(groups = { "Functional" })
+  public void testLocateVisibleStartofSequence()
+  {
+    // create random alignment
+    AlignmentGenerator gen = new AlignmentGenerator(false);
+    AlignmentI al = gen.generate(50, 20, 123, 5, 5);
+
+    HiddenColumns cs = al.getHiddenColumns();
+    ColumnSelection colsel = new ColumnSelection();
+
+    SequenceI seq = new Sequence("RefSeq", "-A-SD-ASD--E---");
+    assertEquals(2, seq.findIndex(seq.getStart()));
+
+    // no hidden columns
+    assertEquals(seq.findIndex(seq.getStart()) - 1,
+            seq.firstResidueOutsideIterator(cs.iterator()));
+
+    // hidden column on gap after end of sequence - should not affect bounds
+    colsel.hideSelectedColumns(13, al.getHiddenColumns());
+    assertEquals(seq.findIndex(seq.getStart()) - 1,
+            seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    // hidden column on gap before beginning of sequence - should vis bounds by
+    // one
+    colsel.hideSelectedColumns(0, al.getHiddenColumns());
+    assertEquals(seq.findIndex(seq.getStart()) - 2,
+            cs.absoluteToVisibleColumn(
+                    seq.firstResidueOutsideIterator(cs.iterator())));
+
+    cs.revealAllHiddenColumns(colsel);
+    // hide columns around most of sequence - leave one residue remaining
+    cs.hideColumns(1, 3);
+    cs.hideColumns(6, 11);
+
+    Iterator<int[]> it = cs.getVisContigsIterator(0, 6, false);
+
+    assertEquals("-D", seq.getSequenceStringFromIterator(it));
+    // cs.getVisibleSequenceStrings(0, 5, new SequenceI[]
+    // { seq })[0]);
+
+    assertEquals(4, seq.firstResidueOutsideIterator(cs.iterator()));
+    cs.revealAllHiddenColumns(colsel);
+
+    // hide whole sequence - should just get location of hidden region
+    // containing sequence
+    cs.hideColumns(1, 11);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 15);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    SequenceI seq2 = new Sequence("RefSeq2", "-------A-SD-ASD--E---");
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(7, 17);
+    assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(3, 17);
+    assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(3, 19);
+    assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 0);
+    assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 1);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 2);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(1, 1);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(1, 2);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(1, 3);
+    assertEquals(4, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 2);
+    cs.hideColumns(5, 6);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 2);
+    cs.hideColumns(5, 6);
+    cs.hideColumns(9, 10);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 2);
+    cs.hideColumns(7, 11);
+    assertEquals(3, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(2, 4);
+    cs.hideColumns(7, 11);
+    assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(2, 4);
+    cs.hideColumns(7, 12);
+    assertEquals(1, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(1, 11);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 12);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 4);
+    cs.hideColumns(6, 12);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 1);
+    cs.hideColumns(3, 12);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(3, 14);
+    cs.hideColumns(17, 19);
+    assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(3, 7);
+    cs.hideColumns(9, 14);
+    cs.hideColumns(17, 19);
+    assertEquals(0, seq2.firstResidueOutsideIterator(cs.iterator()));
+
+    cs.revealAllHiddenColumns(colsel);
+    cs.hideColumns(0, 1);
+    cs.hideColumns(3, 4);
+    cs.hideColumns(6, 8);
+    cs.hideColumns(10, 12);
+    assertEquals(0, seq.firstResidueOutsideIterator(cs.iterator()));
+
+  }
 }
diff --git a/test/jalview/datamodel/StartRegionIteratorTest.java b/test/jalview/datamodel/StartRegionIteratorTest.java
new file mode 100644 (file)
index 0000000..23d0b00
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class StartRegionIteratorTest
+{
+  /**
+   * Test the start region iterator
+   */
+  @Test(groups = { "Functional" })
+  public void testBasicBoundsIterator()
+  {
+    List<int[]> hiddenColumns = null;
+
+    // null hidden columns
+    Iterator<Integer> it = new StartRegionIterator(3, 10,
+            hiddenColumns);
+    assertFalse(it.hasNext());
+
+    hiddenColumns = new ArrayList<>();
+
+    // no hidden columns
+    it = new StartRegionIterator(3, 10, hiddenColumns);
+    assertFalse(it.hasNext());
+
+    // add some hidden columns
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    it = new StartRegionIterator(3, 10, hiddenColumns);
+    assertTrue(it.hasNext());
+    Integer result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(3, 15, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(3, 18, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(3, 19, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertFalse(it.hasNext());
+
+    hiddenColumns.add(new int[] { 47, 50 });
+
+    it = new StartRegionIterator(15, 60, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(25, (int) result);
+    assertFalse(it.hasNext());
+  }
+
+  /**
+   * Test the start region iterator with null cursor
+   */
+  @Test(groups = { "Functional" })
+  public void testBoundsIteratorUsingNullCursor()
+  {
+    List<int[]> hiddenColumns = null;
+    HiddenCursorPosition pos = null;
+
+    // null hidden columns
+    Iterator<Integer> it = new StartRegionIterator(pos, 3, 10,
+            hiddenColumns);
+    assertFalse(it.hasNext());
+
+    hiddenColumns = new ArrayList<>();
+
+    // no hidden columns
+    it = new StartRegionIterator(pos, 3, 10, hiddenColumns);
+    assertFalse(it.hasNext());
+
+    // add some hidden columns
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    it = new StartRegionIterator(pos, 3, 10, hiddenColumns);
+    assertTrue(it.hasNext());
+    Integer result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 15, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 18, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 19, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertFalse(it.hasNext());
+
+    hiddenColumns.add(new int[] { 47, 50 });
+
+    it = new StartRegionIterator(pos, 15, 60, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(25, (int) result);
+    assertFalse(it.hasNext());
+  }
+
+  /**
+   * Test the start region iterator with nonnull cursor
+   */
+  @Test(groups = { "Functional" })
+  public void testBoundsIteratorUsingCursor()
+  {
+    List<int[]> hiddenColumns = new ArrayList<>();
+
+    // add some hidden columns
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    HiddenCursorPosition pos = new HiddenCursorPosition(0, 0);
+
+    Iterator<Integer> it = new StartRegionIterator(pos, 3, 10,
+            hiddenColumns);
+    assertTrue(it.hasNext());
+    Integer result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 15, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 18, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertFalse(it.hasNext());
+
+    it = new StartRegionIterator(pos, 3, 19, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(5, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertFalse(it.hasNext());
+
+    pos = new HiddenCursorPosition(1, 6);
+    hiddenColumns.add(new int[] { 47, 50 });
+
+    it = new StartRegionIterator(pos, 15, 60, hiddenColumns);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(19, (int) result);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(25, (int) result);
+    assertFalse(it.hasNext());
+  }
+}
diff --git a/test/jalview/datamodel/VisibleContigsIteratorTest.java b/test/jalview/datamodel/VisibleContigsIteratorTest.java
new file mode 100644 (file)
index 0000000..8f31dae
--- /dev/null
@@ -0,0 +1,223 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.datamodel;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VisibleContigsIteratorTest
+{
+  /**
+   * Test the iterator with single visible regions
+   */
+  @Test(groups = { "Functional" })
+  public void testSimpleVisibleRegions()
+  {
+    List<int[]> hiddenColumns = null;
+
+    // null hidden columns
+    VisibleContigsIterator it = new VisibleContigsIterator(3, 10,
+            hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    int[] result = it.next();
+    assertEquals(3, result[0]);
+    assertEquals(9, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    hiddenColumns = new ArrayList<>();
+
+    // no hidden columns
+    it = new VisibleContigsIterator(3, 10,
+            hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(3, result[0]);
+    assertEquals(9, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    // hidden columns, but not where we are looking
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    it = new VisibleContigsIterator(2, 3, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(2, result[0]);
+    assertEquals(2, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(5, 7, hiddenColumns);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(11, 15, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(11, result[0]);
+    assertEquals(14, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(50, 60, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(50, result[0]);
+    assertEquals(59, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+  }
+
+  /**
+   * Test the iterator with multiple visible regions
+   */
+  @Test(groups = { "Functional" })
+  public void testMultipleVisibleRegions()
+  {
+    List<int[]> hiddenColumns = new ArrayList<>();
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    // all hidden columns covered
+    VisibleContigsIterator it = new VisibleContigsIterator(3, 50,
+            hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    int[] result = it.next();
+    assertEquals(3, result[0]);
+    assertEquals(4, result[1]);
+
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(11, result[0]);
+    assertEquals(24, result[1]);
+
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(41, result[0]);
+    assertEquals(49, result[1]);
+
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+  }
+
+  /**
+   * Test the iterator with regions which start/end at hidden region edges
+   */
+  @Test(groups = { "Functional" })
+  public void testVisibleRegionsAtHiddenEdges()
+  {
+    List<int[]> hiddenColumns = new ArrayList<>();
+    hiddenColumns.add(new int[] { 5, 10 });
+    hiddenColumns.add(new int[] { 25, 40 });
+
+    VisibleContigsIterator it = new VisibleContigsIterator(0, 10,
+            hiddenColumns);
+    assertTrue(it.hasNext());
+    assertTrue(it.endsAtHidden());
+    int[] result = it.next();
+    assertEquals(0, result[0]);
+    assertEquals(4, result[1]);
+    assertFalse(it.hasNext());
+    assertTrue(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(2, 11, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertTrue(it.endsAtHidden());
+    result = it.next();
+    assertEquals(2, result[0]);
+    assertEquals(4, result[1]);
+    assertFalse(it.hasNext());
+    assertTrue(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(2, 12, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(2, result[0]);
+    assertEquals(4, result[1]);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(11, result[0]);
+    assertEquals(11, result[1]);
+    assertFalse(it.hasNext());
+    assertFalse(it.endsAtHidden());
+
+    it = new VisibleContigsIterator(13, 25, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(13, result[0]);
+    assertEquals(24, result[1]);
+    assertFalse(it.hasNext());
+
+    it = new VisibleContigsIterator(13, 26, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertTrue(it.endsAtHidden());
+    result = it.next();
+    assertEquals(13, result[0]);
+    assertEquals(24, result[1]);
+    assertFalse(it.hasNext());
+
+    it = new VisibleContigsIterator(13, 27, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertTrue(it.endsAtHidden());
+    result = it.next();
+    assertEquals(13, result[0]);
+    assertEquals(24, result[1]);
+    assertFalse(it.hasNext());
+
+    it = new VisibleContigsIterator(13, 41, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertTrue(it.endsAtHidden());
+    result = it.next();
+    assertEquals(13, result[0]);
+    assertEquals(24, result[1]);
+    assertFalse(it.hasNext());
+
+    it = new VisibleContigsIterator(13, 42, hiddenColumns);
+    assertTrue(it.hasNext());
+    assertFalse(it.endsAtHidden());
+    result = it.next();
+    assertEquals(13, result[0]);
+    assertEquals(24, result[1]);
+    assertTrue(it.hasNext());
+    result = it.next();
+    assertEquals(41, result[0]);
+    assertEquals(41, result[1]);
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureAttributesTest.java b/test/jalview/datamodel/features/FeatureAttributesTest.java
new file mode 100644 (file)
index 0000000..686cd2f
--- /dev/null
@@ -0,0 +1,131 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.features.FeatureAttributes.Datatype;
+
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class FeatureAttributesTest
+{
+
+  /**
+   * clear down attributes map before tests
+   */
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    ((Map<?, ?>) PA.getValue(fa, "attributes")).clear();
+  }
+
+  /**
+   * clear down attributes map after tests
+   */
+  @AfterMethod(alwaysRun = true)
+  public void tearDown()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    ((Map<?, ?>) PA.getValue(fa, "attributes")).clear();
+  }
+
+  /**
+   * Test the method that keeps attribute names in non-case-sensitive order,
+   * including handling of 'compound' names
+   */
+  @Test(groups="Functional")
+  public void testAttributeNameComparator()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    Comparator<String[]> comp = (Comparator<String[]>) PA.getValue(fa,
+            "comparator");
+
+    assertEquals(
+            comp.compare(new String[] { "CSQ" }, new String[] { "csq" }), 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "a" },
+            new String[] { "csq" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ" }, new String[] { "csq",
+        "b" }) < 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "AF" }, new String[] {
+        "csq", "ac" }) > 0);
+
+    assertTrue(comp.compare(new String[] { "CSQ", "ac" }, new String[] {
+        "csq", "AF" }) < 0);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMinMax()
+  {
+    SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+            "group");
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    assertNull(fa.getMinMax("Pfam", "kd"));
+    sf.setValue("domain", "xyz");
+    assertNull(fa.getMinMax("Pfam", "kd"));
+    sf.setValue("kd", "some text");
+    assertNull(fa.getMinMax("Pfam", "kd"));
+    sf.setValue("kd", "1.3");
+    assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { 1.3f, 1.3f });
+    sf.setValue("kd", "-2.6");
+    assertEquals(fa.getMinMax("Pfam", "kd"), new float[] { -2.6f, 1.3f });
+    Map<String, String> csq = new HashMap<>();
+    csq.put("AF", "-3");
+    sf.setValue("CSQ", csq);
+    assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+            new float[]
+            { -3f, -3f });
+    csq.put("AF", "4");
+    sf.setValue("CSQ", csq);
+    assertEquals(fa.getMinMax("Pfam", "CSQ", "AF"),
+            new float[]
+            { -3f, 4f });
+  }
+
+  /**
+   * Test the method that returns an attribute description, provided it is
+   * recorded and unique
+   */
+  @Test(groups = "Functional")
+  public void testGetDescription()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    // with no description returns null
+    assertNull(fa.getDescription("Pfam", "kd"));
+    // with a unique description, returns that value
+    fa.addDescription("Pfam", "desc1", "kd");
+    assertEquals(fa.getDescription("Pfam", "kd"), "desc1");
+    // with ambiguous description, returns null
+    fa.addDescription("Pfam", "desc2", "kd");
+    assertNull(fa.getDescription("Pfam", "kd"));
+  }
+
+  @Test(groups = "Functional")
+  public void testDatatype()
+  {
+    FeatureAttributes fa = FeatureAttributes.getInstance();
+    assertNull(fa.getDatatype("Pfam", "kd"));
+    SequenceFeature sf = new SequenceFeature("Pfam", "desc", 10, 20,
+            "group");
+    sf.setValue("kd", "-1");
+    sf.setValue("domain", "Metal");
+    sf.setValue("phase", "1");
+    sf.setValue("phase", "reverse");
+    assertEquals(fa.getDatatype("Pfam", "kd"), Datatype.Number);
+    assertEquals(fa.getDatatype("Pfam", "domain"), Datatype.Character);
+    assertEquals(fa.getDatatype("Pfam", "phase"), Datatype.Mixed);
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java
new file mode 100644 (file)
index 0000000..a2d2c9a
--- /dev/null
@@ -0,0 +1,419 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.matcher.Condition;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherSetTest
+{
+  @Test(groups = "Functional")
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fms.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fms.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testAnd()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "helix domain", 11, 12,
+            6.2f, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.and(fm1);
+    assertTrue(fms.matches(sf));
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byLabelPass = FeatureMatcher.byLabel(Condition.Contains,
+            "Helix");
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+
+    /*
+     * OR a failed attribute condition with a failed score condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.and(fm2);
+    assertFalse(fms.matches(sf));
+    FeatureMatcher byScoreFail = FeatureMatcher.byScore(Condition.LT,
+            "5.9");
+    fms.or(byScoreFail);
+    assertFalse(fms.matches(sf));
+
+    /*
+     * OR failed attribute and score conditions with matched label condition
+     */
+    fms = new FeatureMatcherSet();
+    fms.or(fm2);
+    fms.or(byScoreFail);
+    assertFalse(fms.matches(sf));
+    fms.or(byLabelPass);
+    assertTrue(fms.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm1.toString(), "AF < 1.2");
+
+    FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "path", "CLIN_SIG");
+    assertEquals(fm2.toString(), "CLIN_SIG does not contain 'path'");
+
+    /*
+     * AND them
+     */
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.and(fm1);
+    assertEquals(fms.toString(), "AF < 1.2");
+    fms.and(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) and (CLIN_SIG does not contain 'path')");
+
+    /*
+     * OR them
+     */
+    fms = new FeatureMatcherSet();
+    assertEquals(fms.toString(), "");
+    fms.or(fm1);
+    assertEquals(fms.toString(), "AF < 1.2");
+    fms.or(fm2);
+    assertEquals(fms.toString(),
+            "(AF < 1.2) or (CLIN_SIG does not contain 'path')");
+
+    try
+    {
+      fms.and(fm1);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testOr()
+  {
+    // condition1: AF value contains "dog" (matches)
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.Contains,
+            "dog", "AF");
+    // condition 2: CSQ value does not contain "how" (does not match)
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "how", "CSQ");
+
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    sf.setValue("AF", "raining cats and dogs");
+    sf.setValue("CSQ", "showers");
+
+    assertTrue(fm1.matches(sf));
+    assertFalse(fm2.matches(sf));
+
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.matches(sf)); // if no conditions, then 'all' pass
+    fms.or(fm1);
+    assertTrue(fms.matches(sf));
+    fms.or(fm2);
+    assertTrue(fms.matches(sf)); // true or false makes true
+
+    fms = new FeatureMatcherSet();
+    fms.or(fm2);
+    assertFalse(fms.matches(sf));
+    fms.or(fm1);
+    assertTrue(fms.matches(sf)); // false or true makes true
+
+    try
+    {
+      fms.and(fm2);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  @Test(groups = "Functional")
+  public void testIsEmpty()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2.0",
+            "AF");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertTrue(fms.isEmpty());
+    fms.and(fm);
+    assertFalse(fms.isEmpty());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatchers()
+  {
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+
+    /*
+     * empty iterable:
+     */
+    Iterator<FeatureMatcherI> iterator = fms.getMatchers().iterator();
+    assertFalse(iterator.hasNext());
+
+    /*
+     * one matcher:
+     */
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    fms.and(fm1);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertFalse(iterator.hasNext());
+
+    /*
+     * two matchers:
+     */
+    FeatureMatcherI fm2 = FeatureMatcher.byAttribute(Condition.LT, "8f",
+            "AF");
+    fms.and(fm2);
+    iterator = fms.getMatchers().iterator();
+    assertSame(fm1, iterator.next());
+    assertSame(fm2, iterator.next());
+    assertFalse(iterator.hasNext());
+  }
+
+  /**
+   * Tests for the 'compound attribute' key i.e. where first key's value is a map
+   * from which we take the value for the second key, e.g. CSQ : Consequence
+   */
+  @Test(groups = "Functional")
+  public void testMatches_compoundKey()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "CSQ", "Consequence");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 2, 10, "grp");
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    Map<String, String> csq = new HashMap<>();
+    sf.setValue("CSQ", csq);
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "-2");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-1");
+    assertTrue(fms.matches(sf));
+    csq.put("Consequence", "-3");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "junk");
+    assertFalse(fms.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "CSQ",
+            "Consequence");
+    fms = new FeatureMatcherSet();
+    fms.and(fm);
+    assertFalse(fms.matches(sf));
+    csq.put("PolyPhen", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "damaging");
+    assertFalse(fms.matches(sf));
+    csq.put("Consequence", "Catastrophic");
+    assertTrue(fms.matches(sf));
+  }
+
+  /**
+   * Tests for toStableString which (unlike toString) does not i18n the
+   * conditions
+   * 
+   * @see FeatureMatcherTest#testToStableString()
+   */
+  @Test(groups = "Functional")
+  public void testToStableString()
+  {
+    FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm1.toStableString(), "AF LT 1.2");
+  
+    FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains,
+            "path", "CLIN_SIG");
+    assertEquals(fm2.toStableString(), "CLIN_SIG NotContains path");
+  
+    /*
+     * AND them
+     */
+    FeatureMatcherSetI fms = new FeatureMatcherSet();
+    assertEquals(fms.toStableString(), "");
+    fms.and(fm1);
+    // no brackets needed if a single condition
+    assertEquals(fms.toStableString(), "AF LT 1.2");
+    // brackets if more than one condition
+    fms.and(fm2);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) AND (CLIN_SIG NotContains path)");
+  
+    /*
+     * OR them
+     */
+    fms = new FeatureMatcherSet();
+    assertEquals(fms.toStableString(), "");
+    fms.or(fm1);
+    assertEquals(fms.toStableString(), "AF LT 1.2");
+    fms.or(fm2);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) OR (CLIN_SIG NotContains path)");
+  
+    /*
+     * attribute or value including space is quoted
+     */
+    FeatureMatcher fm3 = FeatureMatcher.byAttribute(Condition.NotMatches,
+            "foo bar", "CSQ", "Poly Phen");
+    assertEquals(fm3.toStableString(),
+            "'CSQ:Poly Phen' NotMatches 'foo bar'");
+    fms.or(fm3);
+    assertEquals(fms.toStableString(),
+            "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')");
+
+    try
+    {
+      fms.and(fm1);
+      fail("Expected exception");
+    } catch (IllegalStateException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Tests for parsing a string representation of a FeatureMatcherSet
+   * 
+   * @see FeatureMatcherSetTest#testToStableString()
+   */
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    String descriptor = "AF LT 1.2";
+    FeatureMatcherSetI fms = FeatureMatcherSet.fromString(descriptor);
+
+    /*
+     * shortcut asserts by verifying a 'roundtrip', 
+     * which we trust if other tests pass :-)
+     */
+    assertEquals(fms.toStableString(), descriptor);
+
+    // brackets optional, quotes optional, condition case insensitive
+    fms = FeatureMatcherSet.fromString("('AF' lt '1.2')");
+    assertEquals(fms.toStableString(), descriptor);
+
+    descriptor = "(AF LT 1.2) AND (CLIN_SIG NotContains path)";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // AND is not case-sensitive
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) and (CLIN_SIG NotContains path)");
+    assertEquals(fms.toStableString(), descriptor);
+  
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path)";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // OR is not case-sensitive
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) or (CLIN_SIG NotContains path)");
+    assertEquals(fms.toStableString(), descriptor);
+
+    // can get away without brackets on last match condition
+    fms = FeatureMatcherSet
+            .fromString("(AF LT 1.2) or CLIN_SIG NotContains path");
+    assertEquals(fms.toStableString(), descriptor);
+  
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    fms = FeatureMatcherSet.fromString(descriptor);
+    assertEquals(fms.toStableString(), descriptor);
+
+    // can't mix OR and AND
+    descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) AND ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    assertNull(FeatureMatcherSet.fromString(descriptor));
+
+    // can't mix AND and OR
+    descriptor = "(AF LT 1.2) and (CLIN_SIG NotContains path) or ('CSQ:Poly Phen' NotMatches 'foo bar')";
+    assertNull(FeatureMatcherSet.fromString(descriptor));
+
+    // brackets missing
+    assertNull(FeatureMatcherSet
+            .fromString("AF LT 1.2 or CLIN_SIG NotContains path"));
+
+    // invalid conjunction
+    assertNull(FeatureMatcherSet.fromString("(AF LT 1.2) but (AF GT -2)"));
+
+    // unbalanced quote (1)
+    assertNull(FeatureMatcherSet.fromString("('AF lt '1.2')"));
+
+    // unbalanced quote (2)
+    assertNull(FeatureMatcherSet.fromString("('AF' lt '1.2)"));
+  }
+}
diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java
new file mode 100644 (file)
index 0000000..4bd34cb
--- /dev/null
@@ -0,0 +1,352 @@
+package jalview.datamodel.features;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
+import jalview.util.matcher.Condition;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class FeatureMatcherTest
+{
+  @Test(groups = "Functional")
+  public void testMatches_byLabel()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, "grp");
+
+    /*
+     * contains - not case sensitive
+     */
+    assertTrue(
+            FeatureMatcher.byLabel(Condition.Contains, "IS").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.Contains, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.Contains, "ISNT").matches(sf));
+
+    /*
+     * does not contain
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "isnt")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "is")
+            .matches(sf));
+
+    /*
+     * matches
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY label")
+            .matches(sf));
+    assertFalse(FeatureMatcher.byLabel(Condition.Matches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * does not match
+     */
+    assertFalse(FeatureMatcher
+            .byLabel(Condition.NotMatches, "THIS is MY label").matches(sf));
+    assertTrue(FeatureMatcher.byLabel(Condition.NotMatches, "THIS is MY")
+            .matches(sf));
+
+    /*
+     * is present / not present
+     */
+    assertTrue(FeatureMatcher.byLabel(Condition.Present, "").matches(sf));
+    assertFalse(
+            FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_byScore()
+  {
+    SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11,
+            12, 3.2f, "grp");
+
+    assertTrue(FeatureMatcher.byScore(Condition.LT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LT, "2.2").matches(sf));
+
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "3.2").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.EQ, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.EQ, "3.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GE, "3.3").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GE, "2.2").matches(sf));
+
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.3").matches(sf));
+    assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf));
+    assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_byAttribute()
+  {
+    /*
+     * a numeric matcher - MatcherTest covers more conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher
+            .byAttribute(Condition.GE, "-2", "AF");
+    SequenceFeature sf = new SequenceFeature("Cath", "desc", 11, 12, "grp");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "foobar");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "-2");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-1");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AF", "-3");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "");
+    assertFalse(fm.matches(sf));
+
+    /*
+     * a string pattern matcher
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Contains, "Cat", "AF");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AF", "raining cats and dogs");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AC");
+    assertFalse(fm.matches(sf));
+    sf.setValue("AC", "21");
+    assertTrue(fm.matches(sf));
+
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AC_Females");
+    assertTrue(fm.matches(sf));
+    sf.setValue("AC_Females", "21");
+    assertFalse(fm.matches(sf));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+
+    /*
+     * toString uses the i18n translation of the enum conditions
+     */
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm.toString(), "AF < 1.2");
+
+    /*
+     * Present / NotPresent omit the value pattern
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+    assertEquals(fm.toString(), "AF is present");
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+    assertEquals(fm.toString(), "AF is not present");
+
+    /*
+     * by Label
+     */
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foobar");
+    assertEquals(fm.toString(),
+            MessageManager.getString("label.label") + " matches 'foobar'");
+
+    /*
+     * by Score
+     */
+    fm = FeatureMatcher.byScore(Condition.GE, "12.2");
+    assertEquals(fm.toString(),
+            MessageManager.getString("label.score") + " >= 12.2");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetAttribute()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2",
+            "AF");
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+
+    /*
+     * compound key (attribute / subattribute)
+     */
+    fm = FeatureMatcher.byAttribute(Condition.GE, "-2F", "CSQ",
+            "Consequence");
+    assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
+
+    /*
+     * answers null if match is by Label or by Score
+     */
+    assertNull(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .getAttribute());
+    assertNull(FeatureMatcher.byScore(Condition.LE, "-1").getAttribute());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByAttribute()
+  {
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByAttribute());
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByAttribute());
+    assertTrue(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByAttribute());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByLabel()
+  {
+    assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByLabel());
+    assertFalse(FeatureMatcher.byScore(Condition.LE, "-1").isByLabel());
+    assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByLabel());
+  }
+
+  @Test(groups = "Functional")
+  public void testIsByScore()
+  {
+    assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo")
+            .isByScore());
+    assertTrue(FeatureMatcher.byScore(Condition.LE, "-1").isByScore());
+    assertFalse(FeatureMatcher.byAttribute(Condition.LE, "-1", "AC")
+            .isByScore());
+  }
+
+  @Test(groups = "Functional")
+  public void testGetMatcher()
+  {
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f",
+            "AF");
+    assertEquals(fm.getMatcher().getCondition(), Condition.GE);
+    assertEquals(fm.getMatcher().getFloatValue(), -2F);
+    assertEquals(fm.getMatcher().getPattern(), "-2.0");
+  }
+
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    FeatureMatcherI fm = FeatureMatcher.fromString("'AF' LT 1.2");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.LT, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
+    assertEquals(fm.getMatcher().getPattern(), "1.2");
+
+    // quotes are optional, condition is not case sensitive
+    fm = FeatureMatcher.fromString("AF lt '1.2'");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.LT, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getFloatValue(), 1.2f);
+    assertEquals(fm.getMatcher().getPattern(), "1.2");
+
+    fm = FeatureMatcher.fromString("'AF' Present");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "AF" });
+    assertSame(Condition.Present, fm.getMatcher().getCondition());
+
+    fm = FeatureMatcher.fromString("CSQ:Consequence contains damaging");
+    assertFalse(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" });
+    assertSame(Condition.Contains, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "damaging");
+
+    // keyword Label is not case sensitive
+    fm = FeatureMatcher.fromString("LABEL Matches 'foobar'");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foobar");
+
+    fm = FeatureMatcher.fromString("'Label' matches 'foo bar'");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+
+    // quotes optional on pattern
+    fm = FeatureMatcher.fromString("'Label' matches foo bar");
+    assertTrue(fm.isByLabel());
+    assertFalse(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.Matches, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "foo bar");
+
+    fm = FeatureMatcher.fromString("Score GE 12.2");
+    assertFalse(fm.isByLabel());
+    assertTrue(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.GE, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "12.2");
+    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+
+    // keyword Score is not case sensitive
+    fm = FeatureMatcher.fromString("'SCORE' ge '12.2'");
+    assertFalse(fm.isByLabel());
+    assertTrue(fm.isByScore());
+    assertNull(fm.getAttribute());
+    assertSame(Condition.GE, fm.getMatcher().getCondition());
+    assertEquals(fm.getMatcher().getPattern(), "12.2");
+    assertEquals(fm.getMatcher().getFloatValue(), 12.2f);
+
+    // invalid numeric pattern
+    assertNull(FeatureMatcher.fromString("Score eq twelve"));
+    // unbalanced opening quote
+    assertNull(FeatureMatcher.fromString("'Score ge 12.2"));
+    // unbalanced pattern quote
+    assertNull(FeatureMatcher.fromString("'Score' ge '12.2"));
+    // pattern missing
+    assertNull(FeatureMatcher.fromString("Score ge"));
+    // condition and pattern missing
+    assertNull(FeatureMatcher.fromString("Score"));
+    // everything missing
+    assertNull(FeatureMatcher.fromString(""));
+  }
+
+  /**
+   * Tests for toStableString which (unlike toString) does not i18n the
+   * conditions
+   */
+  @Test(groups = "Functional")
+  public void testToStableString()
+  {
+    // attribute name not quoted unless it contains space
+    FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2",
+            "AF");
+    assertEquals(fm.toStableString(), "AF LT 1.2");
+
+    /*
+     * Present / NotPresent omit the value pattern
+     */
+    fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF");
+    assertEquals(fm.toStableString(), "AF Present");
+    fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF");
+    assertEquals(fm.toStableString(), "AF NotPresent");
+
+    /*
+     * by Label
+     * pattern not quoted unless it contains space
+     */
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foobar");
+    assertEquals(fm.toStableString(), "Label Matches foobar");
+
+    fm = FeatureMatcher.byLabel(Condition.Matches, "foo bar");
+    assertEquals(fm.toStableString(), "Label Matches 'foo bar'");
+
+    /*
+     * by Score
+     */
+    fm = FeatureMatcher.byScore(Condition.GE, "12.2");
+    assertEquals(fm.toStableString(), "Score GE 12.2");
+  }
+}
index a144f03..39d6dce 100644 (file)
@@ -1032,6 +1032,9 @@ public class SequenceFeaturesTest
     assertEquals(features.size(), 2);
     assertTrue(features.contains(sf2));
     assertTrue(features.contains(sf3));
+
+    features = store.getFeaturesByOntology("sequence_variant");
+    assertTrue(features.isEmpty());
   }
 
   @Test(groups = "Functional")
index 6611e05..779962c 100644 (file)
@@ -228,6 +228,9 @@ public class EnsemblCdnaTest
     sf.setValue("Parent", "transcript:" + accId);
     assertTrue(testee.retainFeature(sf, accId));
 
+    // test is not case-sensitive
+    assertTrue(testee.retainFeature(sf, accId.toLowerCase()));
+
     // feature with wrong parent is not retained
     sf.setValue("Parent", "transcript:XYZ");
     assertFalse(testee.retainFeature(sf, accId));
index a8c491c..217742d 100644 (file)
@@ -25,6 +25,7 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.api.FeatureSettingsModelI;
+import jalview.bin.Cache;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
@@ -53,6 +54,7 @@ public class EnsemblGeneTest
   @BeforeClass(alwaysRun = true)
   public void setUp()
   {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
     SequenceOntologyFactory.setInstance(new SequenceOntologyLite());
   }
 
@@ -173,7 +175,8 @@ public class EnsemblGeneTest
     // NMD_transcript_variant treated like transcript in Ensembl
     SequenceFeature sf3 = new SequenceFeature("NMD_transcript_variant", "",
             22000, 22500, 0f, null);
-    sf3.setValue("Parent", "gene:" + geneId);
+    // id matching should not be case-sensitive
+    sf3.setValue("Parent", "gene:" + geneId.toLowerCase());
     sf3.setValue("transcript_id", "transcript3");
     genomic.addSequenceFeature(sf3);
 
@@ -259,6 +262,9 @@ public class EnsemblGeneTest
     sf.setValue("ID", "gene:" + accId);
     assertTrue(testee.identifiesSequence(sf, accId));
 
+    // test is not case-sensitive
+    assertTrue(testee.identifiesSequence(sf, accId.toLowerCase()));
+
     // transcript not valid:
     sf = new SequenceFeature("transcript", "", 1, 2, 0f, null);
     sf.setValue("ID", "gene:" + accId);
@@ -296,4 +302,28 @@ public class EnsemblGeneTest
     assertEquals(-1, fc.compare("coding_exon", "feature_variant"));
     assertEquals(1f, fc.getTransparency());
   }
+
+  @Test(groups = "Network")
+  public void testGetGeneIds()
+  {
+    /*
+     * ENSG00000158828 gene id PINK1 human
+     * ENST00000321556 transcript for the same gene - should not be duplicated
+     * P30419 Uniprot identifier for ENSG00000136448
+     * ENST00000592782 transcript for Uniprot gene - should not be duplicated
+     * BRAF - gene name resolvabe (at time of writing) for 6 model species
+     */
+    String ids = "ENSG00000158828 ENST00000321556 P30419 ENST00000592782 BRAF";
+    EnsemblGene testee = new EnsemblGene();
+    List<String> geneIds = testee.getGeneIds(ids);
+    assertEquals(8, geneIds.size());
+    assertTrue(geneIds.contains("ENSG00000158828"));
+    assertTrue(geneIds.contains("ENSG00000136448"));
+    assertTrue(geneIds.contains("ENSG00000157764")); // BRAF human
+    assertTrue(geneIds.contains("ENSMUSG00000002413")); // mouse
+    assertTrue(geneIds.contains("ENSRNOG00000010957")); // rat
+    assertTrue(geneIds.contains("ENSXETG00000004845")); // xenopus
+    assertTrue(geneIds.contains("ENSDARG00000017661")); // zebrafish
+    assertTrue(geneIds.contains("ENSGALG00000012865")); // chicken
+  }
 }
index 31001da..cc3a3db 100644 (file)
  */
 package jalview.ext.ensembl;
 
+import static org.testng.Assert.assertTrue;
+
 import jalview.datamodel.AlignmentI;
 
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.List;
 
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 public class EnsemblRestClientTest
 {
+  private EnsemblRestClient sf;
 
   @Test(suiteName = "live")
   public void testIsEnsemblAvailable()
   {
-    EnsemblRestClient sf = new EnsemblRestClient()
+    boolean isAvailable = sf.isEnsemblAvailable();
+    if (isAvailable)
+    {
+      System.out.println("Ensembl is UP!");
+    }
+    else
+    {
+      System.err
+              .println("Ensembl is DOWN or unreachable ******************* BAD!");
+    }
+  }
+
+  @BeforeMethod(alwaysRun = true)
+  protected void setUp()
+  {
+    sf = new EnsemblRestClient()
     {
 
       @Override
@@ -74,16 +93,14 @@ public class EnsemblRestClientTest
       }
 
     };
-    boolean isAvailable = sf.isEnsemblAvailable();
-    if (isAvailable)
-    {
-      System.out.println("Ensembl is UP!");
-    }
-    else
+  }
+
+  @Test(groups = "Network")
+  public void testCheckEnsembl_overload()
+  {
+    for (int i = 0; i < 20; i++)
     {
-      System.err
-              .println("Ensembl is DOWN or unreachable ******************* BAD!");
+      assertTrue(sf.checkEnsembl(), "Error on " + (i + 1) + "th ping");
     }
   }
-
 }
index aa2c315..42afa82 100644 (file)
@@ -21,9 +21,7 @@
 package jalview.ext.ensembl;
 
 import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertSame;
-import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.datamodel.Alignment;
 import jalview.datamodel.SequenceFeature;
@@ -191,34 +189,6 @@ public class EnsemblSeqProxyTest
 
   }
 
-  @Test(groups = "Functional")
-  public void testIsTranscriptIdentifier()
-  {
-    EnsemblSeqProxy testee = new EnsemblGene();
-    assertFalse(testee.isTranscriptIdentifier(null));
-    assertFalse(testee.isTranscriptIdentifier(""));
-    assertFalse(testee.isTranscriptIdentifier("ENSG00000012345"));
-    assertTrue(testee.isTranscriptIdentifier("ENST00000012345"));
-    assertTrue(testee.isTranscriptIdentifier("ENSMUST00000012345"));
-    assertFalse(testee.isTranscriptIdentifier("enst00000012345"));
-    assertFalse(testee.isTranscriptIdentifier("ENST000000123456"));
-    assertFalse(testee.isTranscriptIdentifier("ENST0000001234"));
-  }
-
-  @Test(groups = "Functional")
-  public void testIsGeneIdentifier()
-  {
-    EnsemblSeqProxy testee = new EnsemblGene();
-    assertFalse(testee.isGeneIdentifier(null));
-    assertFalse(testee.isGeneIdentifier(""));
-    assertFalse(testee.isGeneIdentifier("ENST00000012345"));
-    assertTrue(testee.isGeneIdentifier("ENSG00000012345"));
-    assertTrue(testee.isGeneIdentifier("ENSMUSG00000012345"));
-    assertFalse(testee.isGeneIdentifier("ensg00000012345"));
-    assertFalse(testee.isGeneIdentifier("ENSG000000123456"));
-    assertFalse(testee.isGeneIdentifier("ENSG0000001234"));
-  }
-
   /**
    * Test the method that appends a single allele's reverse complement to a
    * string buffer
diff --git a/test/jalview/ext/ensembl/SpeciesTest.java b/test/jalview/ext/ensembl/SpeciesTest.java
new file mode 100644 (file)
index 0000000..44658e7
--- /dev/null
@@ -0,0 +1,30 @@
+package jalview.ext.ensembl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Set;
+
+import org.testng.annotations.Test;
+
+public class SpeciesTest
+{
+  @Test
+  public void testGetModelOrganisms()
+  {
+    Set<Species> models = Species.getModelOrganisms();
+    assertTrue(models.contains(Species.human));
+    assertFalse(models.contains(Species.horse));
+    for (Species s : Species.values())
+    {
+      if (s.isModelOrganism())
+      {
+        assertTrue(models.contains(s));
+      }
+      else
+      {
+        assertFalse(models.contains(s));
+      }
+    }
+  }
+}
index 350b599..28c5cf0 100644 (file)
  */
 package jalview.ext.htsjdk;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
 import jalview.datamodel.SequenceI;
-import jalview.gui.JvOptionPane;
 
 import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 
-import org.testng.Assert;
-import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 /**
@@ -35,25 +41,108 @@ import org.testng.annotations.Test;
  */
 public class TestHtsContigDb
 {
+  @Test(groups = "Functional")
+  public final void testGetSequenceProxy() throws Exception
+  {
+    String pathname = "test/jalview/ext/htsjdk/pgmb.fasta";
+    HtsContigDb db = new HtsContigDb("ADB", new File(pathname));
+    
+    assertTrue(db.isValid());
+    assertTrue(db.isIndexed()); // htsjdk opens the .fai file
+    
+    SequenceI sq = db.getSequenceProxy("Deminut");
+    assertNotNull(sq);
+    assertEquals(sq.getLength(), 606);
+
+    /*
+     * read a sequence earlier in the file
+     */
+    sq = db.getSequenceProxy("PPL_06716");
+    assertNotNull(sq);
+    assertEquals(sq.getLength(), 602);
+    
+    // dict = db.getDictionary(f, truncate))
+  }
 
-  @BeforeClass(alwaysRun = true)
-  public void setUpJvOptionPane()
+  /**
+   * Trying to open a .fai file directly results in IllegalArgumentException -
+   * have to provide the unindexed file name instead
+   */
+  @Test(
+    groups = "Functional",
+    expectedExceptions = java.lang.IllegalArgumentException.class)
+  public final void testGetSequenceProxy_indexed()
   {
-    JvOptionPane.setInteractiveMode(false);
-    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+    String pathname = "test/jalview/ext/htsjdk/pgmb.fasta.fai";
+    new HtsContigDb("ADB", new File(pathname));
+    fail("Expected exception opening .fai file");
   }
 
+  /**
+   * Tests that exercise
+   * <ul>
+   * <li>opening an unindexed fasta file</li>
+   * <li>creating a .fai index</li>
+   * <li>opening the fasta file, now using the index</li>
+   * <li>error on creating index if overwrite not allowed</li>
+   * </ul>
+   * 
+   * @throws IOException
+   */
   @Test(groups = "Functional")
-  public final void testHTSReferenceSequence() throws Exception
+  public void testCreateFastaSequenceIndex() throws IOException
   {
-    HtsContigDb remmadb = new HtsContigDb("REEMADB", new File(
-            "test/jalview/ext/htsjdk/pgmb.fasta"));
+    File fasta = new File("test/jalview/ext/htsjdk/pgmb.fasta");
+    
+    /*
+     * create .fai with no overwrite fails if it exists
+     */
+    try {
+      HtsContigDb.createFastaSequenceIndex(fasta.toPath(), false);
+      fail("Expected exception");
+    } catch (IOException e)
+    {
+      // expected
+    }
 
-    Assert.assertTrue(remmadb.isValid());
+    /*
+     * create a copy of the .fasta (as a temp file)
+     */
+    File copyFasta = File.createTempFile("copyFasta", ".fasta");
+    copyFasta.deleteOnExit();
+    assertTrue(copyFasta.exists());
+    Files.copy(fasta.toPath(), copyFasta.toPath(),
+            StandardCopyOption.REPLACE_EXISTING);
 
-    SequenceI sq = remmadb.getSequenceProxy("Deminut");
-    Assert.assertNotNull(sq);
-    Assert.assertNotEquals(0, sq.getLength());
+    /*
+     * open the Fasta file - not indexed, as no .fai file yet exists
+     */
+    HtsContigDb db = new HtsContigDb("ADB", copyFasta);
+    assertTrue(db.isValid());
+    assertFalse(db.isIndexed());
+    db.close();
+
+    /*
+     * create the .fai index, re-open the .fasta file - now indexed
+     */
+    HtsContigDb.createFastaSequenceIndex(copyFasta.toPath(), true);
+    db = new HtsContigDb("ADB", copyFasta);
+    assertTrue(db.isValid());
+    assertTrue(db.isIndexed());
+    db.close();
   }
 
+  /**
+   * A convenience 'test' that may be run to create a .fai file for any given
+   * fasta file
+   * 
+   * @throws IOException
+   */
+  @Test(enabled = false)
+  public void testCreateIndex() throws IOException
+  {
+
+    File fasta = new File("test/jalview/io/vcf/contigs.fasta");
+    HtsContigDb.createFastaSequenceIndex(fasta.toPath(), true);
+  }
 }
diff --git a/test/jalview/ext/htsjdk/VCFReaderTest.java b/test/jalview/ext/htsjdk/VCFReaderTest.java
new file mode 100644 (file)
index 0000000..bf617ae
--- /dev/null
@@ -0,0 +1,200 @@
+package jalview.ext.htsjdk;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+import htsjdk.samtools.util.CloseableIterator;
+import htsjdk.variant.variantcontext.Allele;
+import htsjdk.variant.variantcontext.VariantContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import org.testng.annotations.Test;
+
+public class VCFReaderTest
+{
+  private static final String[] VCF = new String[] {
+      "##fileformat=VCFv4.2",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      "20\t3\t.\tC\tG\t.\tPASS\tDP=100", // SNP C/G
+      "20\t7\t.\tG\tGA\t.\tPASS\tDP=100", // insertion G/GA
+      "18\t2\t.\tACG\tA\t.\tPASS\tDP=100" }; // deletion ACG/A
+
+  // gnomAD exome variant dataset
+  private static final String VCF_PATH = "/Volumes/gjb/smacgowan/NOBACK/resources/gnomad/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+
+  /**
+   * A test to exercise some basic functionality of the htsjdk VCF reader,
+   * reading from a non-index VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testReadVcf_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+    CloseableIterator<VariantContext> variants = reader.iterator();
+
+    /*
+     * SNP C/G variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isSNP());
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "C");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "C");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "G");
+
+    /*
+     * Insertion G -> GA
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleInsertion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    /*
+     * Deletion ACG -> A
+     */
+    vc = variants.next();
+    assertFalse(vc.isSNP());
+    assertTrue(vc.isSimpleDeletion());
+    ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "ACG");
+    alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "ACG");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "A");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+
+  /**
+   * Creates a temporary file to be read by the htsjdk VCF reader
+   * 
+   * @return
+   * @throws IOException
+   */
+  protected File writeVcfFile() throws IOException
+  {
+    File f = File.createTempFile("Test", "vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF) {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+  
+  /**
+   * A 'test' that demonstrates querying an indexed VCF file for features in a
+   * specified interval
+   * 
+   * @throws IOException
+   */
+  @Test
+  public void testQuery_indexed() throws IOException
+  {
+    /*
+     * if not specified, assumes index file is filename.tbi
+     */
+    VCFReader reader = new VCFReader(VCF_PATH);
+  
+    /*
+     * gene NMT1 (human) is on chromosome 17
+     * GCHR38 (Ensembl): 45051610-45109016
+     * GCHR37 (gnoMAD): 43128978-43186384
+     * CDS begins at offset 9720, first CDS variant at offset 9724
+     */
+    CloseableIterator<VariantContext> features = reader.query("17",
+            43128978 + 9724, 43128978 + 9734); // first 11 CDS positions
+
+    assertEquals(printNext(features), 43138702);
+    assertEquals(printNext(features), 43138704);
+    assertEquals(printNext(features), 43138707);
+    assertEquals(printNext(features), 43138708);
+    assertEquals(printNext(features), 43138710);
+    assertEquals(printNext(features), 43138711);
+    assertFalse(features.hasNext());
+
+    features.close();
+    reader.close();
+  }
+
+  /**
+   * Prints the toString value of the next variant, and returns its start
+   * location
+   * 
+   * @param features
+   * @return
+   */
+  protected int printNext(CloseableIterator<VariantContext> features)
+  {
+    VariantContext next = features.next();
+    System.out.println(next.toString());
+    return next.getStart();
+  }
+
+  // "https://storage.cloud.google.com/gnomad-public/release/2.0.1/vcf/exomes/gnomad.exomes.r2.0.1.sites.vcf.gz";
+  
+  /**
+   * Test the query method that wraps a non-indexed VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testQuery_plain() throws IOException
+  {
+    File f = writeVcfFile();
+    VCFReader reader = new VCFReader(f.getAbsolutePath());
+
+    /*
+     * query for overlap of 5-8 - should find variant at 7
+     */
+    CloseableIterator<VariantContext> variants = reader.query("20", 5, 8);
+  
+    /*
+     * INDEL G/GA variant
+     */
+    VariantContext vc = variants.next();
+    assertTrue(vc.isIndel());
+    assertEquals(vc.getStart(), 7);
+    assertEquals(vc.getEnd(), 7);
+    Allele ref = vc.getReference();
+    assertEquals(ref.getBaseString(), "G");
+    List<Allele> alleles = vc.getAlleles();
+    assertEquals(alleles.size(), 2);
+    assertTrue(alleles.get(0).isReference());
+    assertEquals(alleles.get(0).getBaseString(), "G");
+    assertFalse(alleles.get(1).isReference());
+    assertEquals(alleles.get(1).getBaseString(), "GA");
+
+    assertFalse(variants.hasNext());
+
+    variants.close();
+    reader.close();
+  }
+}
index 3309adf..e42b54f 100644 (file)
@@ -115,7 +115,7 @@ public class JmolCommandsTest
     String chainACommand = commands[0].commands[0];
     // M colour is #82827d == (130, 130, 125) (see strand.html help page)
     assertTrue(chainACommand
-            .contains(";select 21:A/1.1;color[130,130,125]"));
+            .contains("select 21:A/1.1;color[130,130,125]")); // first one
     // H colour is #60609f == (96, 96, 159)
     assertTrue(chainACommand.contains(";select 22:A/1.1;color[96,96,159]"));
     // hidden columns are Gray (128, 128, 128)
@@ -128,7 +128,7 @@ public class JmolCommandsTest
     String chainBCommand = commands[1].commands[0];
     // M colour is #82827d == (130, 130, 125)
     assertTrue(chainBCommand
-            .contains(";select 21:B/2.1;color[130,130,125]"));
+            .contains("select 21:B/2.1;color[130,130,125]"));
     // V colour is #ffff00 == (255, 255, 0)
     assertTrue(chainBCommand
 .contains(";select 22:B/2.1;color[255,255,0]"));
index 792f7ad..e451ed2 100644 (file)
  */
 package jalview.ext.jmol;
 
+import static org.junit.Assert.assertNotNull;
+import static org.testng.Assert.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.api.structures.JalviewStructureDisplayI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
+import jalview.datamodel.PDBEntry;
+import jalview.datamodel.SearchResultsI;
 import jalview.datamodel.SequenceI;
 import jalview.gui.AlignFrame;
 import jalview.gui.JvOptionPane;
@@ -32,6 +36,10 @@ import jalview.gui.Preferences;
 import jalview.gui.StructureViewer;
 import jalview.gui.StructureViewer.ViewerType;
 import jalview.io.DataSourceType;
+import jalview.io.FileFormat;
+import jalview.io.FileLoader;
+
+import java.lang.reflect.InvocationTargetException;
 
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
@@ -54,8 +62,10 @@ public class JmolViewerTest
   @BeforeClass(alwaysRun = true)
   public static void setUpBeforeClass() throws Exception
   {
-    Jalview.main(new String[] { "-noquestionnaire", "-nonews", "-props",
-        "test/jalview/ext/rbvi/chimera/testProps.jvprops" });
+    Jalview.main(
+            new String[]
+            { "-noquestionnaire", "-nonews", "-props",
+                "test/jalview/ext/rbvi/chimera/testProps.jvprops" });
   }
 
   /**
@@ -70,10 +80,11 @@ public class JmolViewerTest
   @Test(groups = { "Functional" })
   public void testSingleSeqViewJMol()
   {
-    Cache.setProperty(Preferences.STRUCTURE_DISPLAY, ViewerType.JMOL.name());
+    Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
+            ViewerType.JMOL.name());
     String inFile = "examples/1gaq.txt";
-    AlignFrame af = new jalview.io.FileLoader().LoadFileWaitTillLoaded(
-            inFile, DataSourceType.FILE);
+    AlignFrame af = new jalview.io.FileLoader()
+            .LoadFileWaitTillLoaded(inFile, DataSourceType.FILE);
     assertTrue("Didn't read input file " + inFile, af != null);
     for (SequenceI sq : af.getViewport().getAlignment().getSequences())
     {
@@ -87,13 +98,13 @@ public class JmolViewerTest
       {
         for (int q = 0; q < dsq.getAllPDBEntries().size(); q++)
         {
-          final StructureViewer structureViewer = new StructureViewer(af
-                  .getViewport().getStructureSelectionManager());
+          final StructureViewer structureViewer = new StructureViewer(
+                  af.getViewport().getStructureSelectionManager());
           structureViewer.setViewerType(ViewerType.JMOL);
           JalviewStructureDisplayI jmolViewer = structureViewer
                   .viewStructures(dsq.getAllPDBEntries().elementAt(q),
-                          new SequenceI[] { sq }, af.getCurrentView()
-                                  .getAlignPanel());
+                          new SequenceI[]
+                          { sq }, af.getCurrentView().getAlignPanel());
           /*
            * Wait for viewer load thread to complete
            */
@@ -116,5 +127,86 @@ public class JmolViewerTest
     }
   }
 
+  @Test(groups = { "Functional" })
+  public void testAddStrToSingleSeqViewJMol()
+          throws InvocationTargetException, InterruptedException
+  {
+    Cache.setProperty(Preferences.STRUCTURE_DISPLAY,
+            ViewerType.JMOL.name());
+    String inFile = "examples/1gaq.txt";
+    AlignFrame af = new jalview.io.FileLoader(true)
+            .LoadFileWaitTillLoaded(inFile, DataSourceType.FILE);
+    assertTrue("Didn't read input file " + inFile, af != null);
+    // show a structure for 4th Sequence
+    SequenceI sq1 = af.getViewport().getAlignment().getSequences().get(0);
+    final StructureViewer structureViewer = new StructureViewer(
+            af.getViewport().getStructureSelectionManager());
+    structureViewer.setViewerType(ViewerType.JMOL);
+    JalviewStructureDisplayI jmolViewer = structureViewer.viewStructures(
+            sq1.getDatasetSequence().getAllPDBEntries().elementAt(0),
+            new SequenceI[]
+            { sq1 }, af.getCurrentView().getAlignPanel());
+    /*
+     * Wait for viewer load thread to complete
+     */
+    try
+    {
+      while (!jmolViewer.getBinding().isFinishedInit())
+      {
+        Thread.sleep(500);
+      }
+    } catch (InterruptedException e)
+    {
+    }
+
+    assertTrue(jmolViewer.isVisible());
+
+    // add another pdb file and add it to view
+    final String _inFile = "examples/3W5V.pdb";
+    inFile = _inFile;
+    FileLoader fl = new FileLoader();
+    fl.LoadFile(af.getCurrentView(), _inFile, DataSourceType.FILE,
+            FileFormat.PDB);
+    try
+    {
+      int time = 0;
+      do
+      {
+        Thread.sleep(50); // hope we can avoid race condition
+
+      } while (++time < 30
+              && af.getViewport().getAlignment().getHeight() == 3);
+    } catch (Exception q)
+    {
+    }
+    ;
+    assertTrue("Didn't paste additional structure" + inFile,
+            af.getViewport().getAlignment().getHeight() > 3);
+    SequenceI sq2 = af.getViewport().getAlignment().getSequenceAt(3);
+    PDBEntry pdbe = sq2.getDatasetSequence().getAllPDBEntries().get(0);
+    assertTrue(pdbe.getFile().contains(inFile));
+    structureViewer.viewStructures(pdbe, new SequenceI[] { sq2 },
+            af.alignPanel);
+    /*
+     * Wait for viewer load thread to complete
+     */
+    try
+    {
+      while (structureViewer.isBusy())
+      {
+        Thread.sleep(500);
+      }
+    } catch (InterruptedException e)
+    {
+    }
+    assertEquals(jmolViewer.getBinding().getPdbCount(), 2);
+    String mouseOverTest = "[GLY]293:A.CA/2.1 #2164";
+    ((JalviewJmolBinding) jmolViewer.getBinding()).mouseOverStructure(2164,
+            mouseOverTest);
+    SearchResultsI highlight = af.alignPanel.getSeqPanel()
+            .getLastSearchResults();
+    assertNotNull("Didn't find highlight from second structure mouseover",
+            highlight.getResults(sq2, sq2.getStart(), sq2.getEnd()));
+  }
 
 }
index c9e1cad..63d5e4e 100644 (file)
@@ -29,6 +29,11 @@ public class AtomSpecModelTest
     assertEquals(model.getAtomSpec(), "#0:1-4.B,5-9.C|#1:2-5.A,8.A,5-10.B");
     model.addRange(0, 3, 10, "C"); // subsumes 5-9
     assertEquals(model.getAtomSpec(), "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B");
+    model.addRange(5, 25, 35, " "); // empty chain code - e.g. from homology
+                                    // modelling
+    assertEquals(model.getAtomSpec(),
+            "#0:1-4.B,3-10.C|#1:2-5.A,8.A,5-10.B|#5:25-35.");
+
   }
 
 }
index b76a295..31e1887 100644 (file)
@@ -107,4 +107,29 @@ public class SequenceOntologyTest
     assertFalse(so.isA("CDS_region", "CDS"));// part_of
     assertFalse(so.isA("polypeptide", "CDS")); // derives_from
   }
+
+  @Test(groups = "Functional")
+  public void testIsSequenceVariant()
+  {
+    assertFalse(so.isA("CDS", "sequence_variant"));
+    assertTrue(so.isA("sequence_variant", "sequence_variant"));
+
+    /*
+     * these should all be sub-types of sequence_variant
+     */
+    assertTrue(so.isA("structural_variant", "sequence_variant"));
+    assertTrue(so.isA("feature_variant", "sequence_variant"));
+    assertTrue(so.isA("gene_variant", "sequence_variant"));
+    assertTrue(so.isA("transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("NMD_transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("missense_variant", "sequence_variant"));
+    assertTrue(so.isA("synonymous_variant", "sequence_variant"));
+    assertTrue(so.isA("frameshift_variant", "sequence_variant"));
+    assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("stop_gained", "sequence_variant"));
+    assertTrue(so.isA("stop_lost", "sequence_variant"));
+    assertTrue(so.isA("inframe_deletion", "sequence_variant"));
+    assertTrue(so.isA("inframe_insertion", "sequence_variant"));
+  }
 }
index af9c045..b0aaab9 100644 (file)
@@ -26,10 +26,12 @@ import static org.testng.Assert.assertNotSame;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
+import jalview.api.FeatureColourI;
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
 import jalview.datamodel.Alignment;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
@@ -39,6 +41,7 @@ import jalview.io.FileLoader;
 import jalview.io.Jalview2xmlTests;
 import jalview.renderer.ResidueShaderI;
 import jalview.schemes.BuriedColourScheme;
+import jalview.schemes.FeatureColour;
 import jalview.schemes.HelixColourScheme;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.StrandColourScheme;
@@ -46,7 +49,7 @@ import jalview.schemes.TurnColourScheme;
 import jalview.util.MessageManager;
 
 import java.awt.Color;
-import java.util.List;
+import java.util.Iterator;
 
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
@@ -69,54 +72,81 @@ public class AlignFrameTest
   {
     SequenceI seq1 = new Sequence("Seq1", "ABCDEFGHIJ");
     SequenceI seq2 = new Sequence("Seq2", "ABCDEFGHIJ");
-    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5,
-            Float.NaN, null));
-    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10,
-            Float.NaN, null));
+    seq1.addSequenceFeature(new SequenceFeature("Metal", "", 1, 5, 0f, null));
+    seq2.addSequenceFeature(new SequenceFeature("Metal", "", 6, 10, 10f,
+            null));
     seq1.addSequenceFeature(new SequenceFeature("Turn", "", 2, 4,
             Float.NaN, null));
     seq2.addSequenceFeature(new SequenceFeature("Turn", "", 7, 9,
             Float.NaN, null));
     AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
-    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(), al.getHeight());
+    AlignFrame alignFrame = new AlignFrame(al, al.getWidth(),
+            al.getHeight());
+
+    /*
+     * make all features visible (select feature columns checks visibility)
+     */
+    alignFrame.getFeatureRenderer().findAllFeatures(true);
 
     /*
      * hiding a feature not present does nothing
      */
     assertFalse(alignFrame.hideFeatureColumns("exon", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-    assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-            .getHiddenColumnsCopy()
-            .isEmpty());
+
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 0);
+
     assertFalse(alignFrame.hideFeatureColumns("exon", false));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-    assertTrue(alignFrame.getViewport().getAlignment().getHiddenColumns()
-            .getHiddenColumnsCopy()
-            .isEmpty());
+
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 0);
 
     /*
      * hiding a feature in all columns does nothing
      */
     assertFalse(alignFrame.hideFeatureColumns("Metal", true));
     assertTrue(alignFrame.getViewport().getColumnSelection().isEmpty());
-    List<int[]> hidden = alignFrame.getViewport().getAlignment()
-            .getHiddenColumns()
-            .getHiddenColumnsCopy();
-    assertTrue(hidden.isEmpty());
+
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 0);
+
+
+    /*
+     * threshold Metal to hide features where score < 5
+     * seq1 feature in columns 1-5 is hidden
+     * seq2 feature in columns 6-10 is shown
+     */
+    FeatureColourI fc = new FeatureColour(Color.red, Color.blue, 0f, 10f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(5f);
+    alignFrame.getFeatureRenderer().setColour("Metal", fc);
+    assertTrue(alignFrame.hideFeatureColumns("Metal", true));
+    HiddenColumns hidden = alignFrame.getViewport().getAlignment().getHiddenColumns();
+    assertEquals(hidden.getNumberOfRegions(), 1);
+    Iterator<int[]> regions = hidden.iterator();
+    int[] next = regions.next();
+    assertEquals(next[0], 5);
+    assertEquals(next[1], 9);
 
     /*
      * hide a feature present in some columns
      * sequence positions [2-4], [7-9] are column positions
      * [1-3], [6-8] base zero
      */
+    alignFrame.getViewport().showAllHiddenColumns();
     assertTrue(alignFrame.hideFeatureColumns("Turn", true));
-    hidden = alignFrame.getViewport().getAlignment().getHiddenColumns()
-            .getHiddenColumnsCopy();
-    assertEquals(hidden.size(), 2);
-    assertEquals(hidden.get(0)[0], 1);
-    assertEquals(hidden.get(0)[1], 3);
-    assertEquals(hidden.get(1)[0], 6);
-    assertEquals(hidden.get(1)[1], 8);
+    regions = alignFrame.getViewport().getAlignment()
+            .getHiddenColumns().iterator();
+    assertEquals(alignFrame.getViewport().getAlignment().getHiddenColumns()
+            .getNumberOfRegions(), 2);
+    next = regions.next();
+    assertEquals(next[0], 1);
+    assertEquals(next[1], 3);
+    next = regions.next();
+    assertEquals(next[0], 6);
+    assertEquals(next[1], 8);
   }
 
   @BeforeClass(alwaysRun = true)
index 812fd8f..5ed0cac 100644 (file)
@@ -96,57 +96,6 @@ public class AlignViewportTest
     testee = new AlignViewport(al);
   }
 
-  @Test(groups = { "Functional" })
-  public void testCollateForPdb()
-  {
-    // JBP: What behaviour is this supposed to test ?
-    /*
-     * Set up sequence pdb ids
-     */
-    PDBEntry pdb1 = new PDBEntry("1ABC", "B", Type.PDB, "1ABC.pdb");
-    PDBEntry pdb2 = new PDBEntry("2ABC", "C", Type.PDB, "2ABC.pdb");
-    PDBEntry pdb3 = new PDBEntry("3ABC", "D", Type.PDB, "3ABC.pdb");
-
-    /*
-     * seq1 and seq3 refer to 1abcB, seq2 to 2abcC, none to 3abcD
-     */
-    al.getSequenceAt(0).getDatasetSequence()
-            .addPDBId(new PDBEntry("1ABC", "B", Type.PDB, "1ABC.pdb"));
-    al.getSequenceAt(2).getDatasetSequence()
-            .addPDBId(new PDBEntry("1ABC", "B", Type.PDB, "1ABC.pdb"));
-    al.getSequenceAt(1).getDatasetSequence()
-            .addPDBId(new PDBEntry("2ABC", "C", Type.PDB, "2ABC.pdb"));
-    /*
-     * Add a second chain PDB xref to Seq2 - should not result in a duplicate in
-     * the results
-     */
-    al.getSequenceAt(1).getDatasetSequence()
-            .addPDBId(new PDBEntry("2ABC", "D", Type.PDB, "2ABC.pdb"));
-    /*
-     * Seq3 refers to 3abc - this does not match 3ABC (as the code stands)
-     */
-    al.getSequenceAt(2).getDatasetSequence()
-            .addPDBId(new PDBEntry("3abc", "D", Type.PDB, "3ABC.pdb"));
-
-    /*
-     * run method under test
-     */
-    SequenceI[][] seqs = testee.collateForPDB(new PDBEntry[] { pdb1, pdb2,
-        pdb3 });
-
-    // seq1 and seq3 refer to PDBEntry[0]
-    assertEquals(2, seqs[0].length);
-    assertSame(al.getSequenceAt(0), seqs[0][0]);
-    assertSame(al.getSequenceAt(2), seqs[0][1]);
-
-    // seq2 refers to PDBEntry[1]
-    assertEquals(1, seqs[1].length);
-    assertSame(al.getSequenceAt(1), seqs[1][0]);
-
-    // no sequence refers to PDBEntry[2]
-    assertEquals(0, seqs[2].length);
-  }
-
   /**
    * Test that a mapping is not deregistered when a second view is closed but
    * the first still holds a reference to the mapping
index b228ba1..2819dbf 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.gui;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
 
 import jalview.bin.Cache;
 import jalview.bin.Jalview;
@@ -218,4 +219,31 @@ public class AlignmentPanelTest
             .getAlignment().getWidth() - 1 - 21); // 21 is the number of hidden
                                                   // columns
   }
+
+  /**
+   * Test that update layout reverts to original (unwrapped) values for endRes
+   * and endSeq when switching from wrapped to unwrapped mode (JAL-2739)
+   */
+  @Test(groups = "Functional")
+  public void TestUpdateLayout_endRes()
+  {
+    // get details of original alignment dimensions
+    ViewportRanges ranges = af.getViewport().getRanges();
+    int endres = ranges.getEndRes();
+
+    // wrap
+    af.alignPanel.getAlignViewport().setWrapAlignment(true);
+    af.alignPanel.updateLayout();
+
+    // endRes changes
+    assertNotEquals(ranges.getEndRes(), endres);
+
+    // unwrap
+    af.alignPanel.getAlignViewport().setWrapAlignment(false);
+    af.alignPanel.updateLayout();
+
+    // endRes and endSeq back to original values
+    assertEquals(ranges.getEndRes(), endres);
+
+  }
 }
diff --git a/test/jalview/gui/AnnotationColumnChooserTest.java b/test/jalview/gui/AnnotationColumnChooserTest.java
new file mode 100644 (file)
index 0000000..912cd27
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.AssertJUnit.assertEquals;
+
+import jalview.analysis.AnnotationSorter.SequenceAnnotationOrder;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SequenceI;
+import jalview.io.DataSourceType;
+import jalview.io.FileFormat;
+import jalview.io.FormatAdapter;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Unit tests for AnnotationChooser
+ * 
+ * @author kmourao
+ *
+ */
+public class AnnotationColumnChooserTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUpJvOptionPane()
+  {
+    JvOptionPane.setInteractiveMode(false);
+    JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
+  }
+
+  // 4 sequences x 13 positions
+  final static String TEST_DATA = ">FER_CAPAA Ferredoxin\n"
+          + "TIETHKEAELVG-\n"
+          + ">FER_CAPAN Ferredoxin, chloroplast precursor\n"
+          + "TIETHKEAELVG-\n"
+          + ">FER1_SOLLC Ferredoxin-1, chloroplast precursor\n"
+          + "TIETHKEEELTA-\n" + ">Q93XJ9_SOLTU Ferredoxin I precursor\n"
+          + "TIETHKEEELTA-\n";
+
+  AnnotationChooser testee;
+
+  AlignmentPanel parentPanel;
+
+  AlignFrame af;
+
+  @BeforeMethod(alwaysRun = true)
+  public void setUp() throws IOException
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    // pin down annotation sort order for test
+    Cache.applicationProperties.setProperty(Preferences.SORT_ANNOTATIONS,
+            SequenceAnnotationOrder.NONE.name());
+    final String TRUE = Boolean.TRUE.toString();
+    Cache.applicationProperties.setProperty(Preferences.SHOW_AUTOCALC_ABOVE,
+            TRUE);
+    Cache.applicationProperties.setProperty("SHOW_QUALITY", TRUE);
+    Cache.applicationProperties.setProperty("SHOW_CONSERVATION", TRUE);
+    Cache.applicationProperties.setProperty("SHOW_IDENTITY", TRUE);
+
+    AlignmentI al = new FormatAdapter().readFile(TEST_DATA,
+            DataSourceType.PASTE, FileFormat.Fasta);
+    af = new AlignFrame(al, 700, 500);
+    parentPanel = new AlignmentPanel(af, af.getViewport());
+    addAnnotations();
+  }
+
+  /**
+   * Add 4 annotations, 3 of them sequence-specific.
+   * 
+   * <PRE>
+   * ann1 - for sequence 0 - label 'IUPRED' ann2 - not sequence related - label
+   * 'Beauty' ann3 - for sequence 3 - label 'JMol' ann4 - for sequence 2 - label
+   * 'IUPRED' ann5 - for sequence 1 - label 'JMol'
+   */
+  private void addAnnotations()
+  {
+    Annotation an = new Annotation(2f);
+    Annotation[] anns = new Annotation[] { an, an, an };
+    AlignmentAnnotation ann0 = new AlignmentAnnotation("IUPRED", "", anns);
+    AlignmentAnnotation ann1 = new AlignmentAnnotation("Beauty", "", anns);
+    AlignmentAnnotation ann2 = new AlignmentAnnotation("JMol", "", anns);
+    AlignmentAnnotation ann3 = new AlignmentAnnotation("IUPRED", "", anns);
+    AlignmentAnnotation ann4 = new AlignmentAnnotation("JMol", "", anns);
+    SequenceI[] seqs = parentPanel.getAlignment().getSequencesArray();
+    ann0.setSequenceRef(seqs[0]);
+    ann2.setSequenceRef(seqs[3]);
+    ann3.setSequenceRef(seqs[2]);
+    ann4.setSequenceRef(seqs[1]);
+    parentPanel.getAlignment().addAnnotation(ann0);
+    parentPanel.getAlignment().addAnnotation(ann1);
+    parentPanel.getAlignment().addAnnotation(ann2);
+    parentPanel.getAlignment().addAnnotation(ann3);
+    parentPanel.getAlignment().addAnnotation(ann4);
+  }
+
+  /**
+   * Test reset
+   */
+  @Test(groups = { "Functional" })
+  public void testReset()
+  {
+    AnnotationColumnChooser acc = new AnnotationColumnChooser(
+            af.getViewport(), af.alignPanel);
+
+    HiddenColumns oldhidden = new HiddenColumns();
+    oldhidden.hideColumns(10, 20);
+    acc.setOldHiddenColumns(oldhidden);
+
+    HiddenColumns newHidden = new HiddenColumns();
+    newHidden.hideColumns(0, 3);
+    newHidden.hideColumns(22, 25);
+    af.getViewport().setHiddenColumns(newHidden);
+
+    HiddenColumns currentHidden = af.getViewport().getAlignment()
+            .getHiddenColumns();
+    Iterator<int[]> regions = currentHidden.iterator();
+    int[] next = regions.next();
+    assertEquals(0, next[0]);
+    assertEquals(3, next[1]);
+    next = regions.next();
+    assertEquals(22, next[0]);
+    assertEquals(25, next[1]);
+
+    // now reset hidden columns
+    acc.reset();
+    currentHidden = af.getViewport().getAlignment().getHiddenColumns();
+    regions = currentHidden.iterator();
+    next = regions.next();
+    assertEquals(10, next[0]);
+    assertEquals(20, next[1]);
+
+    // check works with empty hidden columns as old columns
+    oldhidden = new HiddenColumns();
+    acc.setOldHiddenColumns(oldhidden);
+    acc.reset();
+    currentHidden = af.getViewport().getAlignment().getHiddenColumns();
+    assertFalse(currentHidden.hasHiddenColumns());
+
+    // check works with empty hidden columns as new columns
+    oldhidden.hideColumns(10, 20);
+    acc.setOldHiddenColumns(oldhidden);
+    currentHidden = af.getViewport().getAlignment().getHiddenColumns();
+    assertFalse(currentHidden.hasHiddenColumns());
+
+    acc.reset();
+    currentHidden = af.getViewport().getAlignment().getHiddenColumns();
+    regions = currentHidden.iterator();
+    next = regions.next();
+    assertEquals(10, next[0]);
+    assertEquals(20, next[1]);
+  }
+}
diff --git a/test/jalview/gui/FeatureSettingsTest.java b/test/jalview/gui/FeatureSettingsTest.java
new file mode 100644 (file)
index 0000000..6ddebf8
--- /dev/null
@@ -0,0 +1,191 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import jalview.api.FeatureColourI;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.schemes.FeatureColour;
+import jalview.util.matcher.Condition;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+import org.testng.annotations.Test;
+
+public class FeatureSettingsTest
+{
+  /**
+   * Test a roundtrip of save and reload of feature colours and filters as XML
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testSaveLoad() throws IOException
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE);
+    SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0);
+
+    /*
+     * add some features to the sequence
+     */
+    int score = 1;
+    addFeatures(seq1, "type1", score++);
+    addFeatures(seq1, "type2", score++);
+    addFeatures(seq1, "type3", score++);
+    addFeatures(seq1, "type4", score++);
+    addFeatures(seq1, "type5", score++);
+
+    /*
+     * set colour schemes for features
+     */
+    FeatureRenderer fr = af.getFeatureRenderer();
+
+    // type1: red
+    fr.setColour("type1", new FeatureColour(Color.red));
+
+    // type2: by label
+    FeatureColourI byLabel = new FeatureColour();
+    byLabel.setColourByLabel(true);
+    fr.setColour("type2", byLabel);
+
+    // type3: by score above threshold
+    FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
+            10);
+    byScore.setAboveThreshold(true);
+    byScore.setThreshold(2f);
+    fr.setColour("type3", byScore);
+
+    // type4: by attribute AF
+    FeatureColourI byAF = new FeatureColour();
+    byAF.setColourByLabel(true);
+    byAF.setAttributeName("AF");
+    fr.setColour("type4", byAF);
+
+    // type5: by attribute CSQ:PolyPhen below threshold
+    FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
+            1, 10);
+    byPolyPhen.setBelowThreshold(true);
+    byPolyPhen.setThreshold(3f);
+    byPolyPhen.setAttributeName("CSQ", "PolyPhen");
+    fr.setColour("type5", byPolyPhen);
+
+    /*
+     * set filters for feature types
+     */
+
+    // filter type1 features by (label contains "x")
+    FeatureMatcherSetI filterByX = new FeatureMatcherSet();
+    filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x"));
+    fr.setFeatureFilter("type1", filterByX);
+
+    // filter type2 features by (score <= 2.4 and score > 1.1)
+    FeatureMatcherSetI filterByScore = new FeatureMatcherSet();
+    filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4"));
+    filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1"));
+    fr.setFeatureFilter("type2", filterByScore);
+
+    // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0)
+    FeatureMatcherSetI filterByXY = new FeatureMatcherSet();
+    filterByXY
+            .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF"));
+    filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ",
+            "PolyPhen"));
+    fr.setFeatureFilter("type3", filterByXY);
+
+    /*
+     * save colours and filters to an XML file
+     */
+    File coloursFile = File.createTempFile("testSaveLoad", ".fc");
+    coloursFile.deleteOnExit();
+    FeatureSettings fs = new FeatureSettings(af);
+    fs.save(coloursFile);
+
+    /*
+     * change feature colours and filters
+     */
+    FeatureColourI pink = new FeatureColour(Color.pink);
+    fr.setColour("type1", pink);
+    fr.setColour("type2", pink);
+    fr.setColour("type3", pink);
+    fr.setColour("type4", pink);
+    fr.setColour("type5", pink);
+
+    FeatureMatcherSetI filter2 = new FeatureMatcherSet();
+    filter2.and(FeatureMatcher.byLabel(Condition.NotContains, "y"));
+    fr.setFeatureFilter("type1", filter2);
+    fr.setFeatureFilter("type2", filter2);
+    fr.setFeatureFilter("type3", filter2);
+    fr.setFeatureFilter("type4", filter2);
+    fr.setFeatureFilter("type5", filter2);
+
+    /*
+     * reload colours and filters from file and verify they are restored
+     */
+    fs.load(coloursFile);
+    FeatureColourI fc = fr.getFeatureStyle("type1");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(fc.getColour(), Color.red);
+    fc = fr.getFeatureStyle("type2");
+    assertTrue(fc.isColourByLabel());
+    fc = fr.getFeatureStyle("type3");
+    assertTrue(fc.isGraduatedColour());
+    assertNull(fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(fc.getThreshold(), 2f);
+    fc = fr.getFeatureStyle("type4");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "AF" });
+    fc = fr.getFeatureStyle("type5");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" });
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(fc.getThreshold(), 3f);
+
+    assertEquals(fr.getFeatureFilter("type1").toStableString(), "Label Contains x");
+    assertEquals(fr.getFeatureFilter("type2").toStableString(),
+            "(Score LE 2.4) AND (Score GT 1.1)");
+    assertEquals(fr.getFeatureFilter("type3").toStableString(),
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+  }
+
+  /**
+   * Adds two features of the given type to the given sequence, also setting the
+   * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen"
+   * 
+   * @param seq
+   * @param featureType
+   * @param score
+   */
+  private void addFeatures(SequenceI seq, String featureType, int score)
+  {
+    addFeature(seq, featureType, score++);
+    addFeature(seq, featureType, score);
+  }
+
+  private void addFeature(SequenceI seq, String featureType, int score)
+  {
+    SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2,
+            score, "grp");
+    sf.setValue("AF", score);
+    sf.setValue("CSQ", new HashMap<String, String>()
+    {
+      {
+        put("PolyPhen", Integer.toString(score));
+      }
+    });
+    seq.addSequenceFeature(sf);
+  }
+}
diff --git a/test/jalview/gui/FreeUpMemoryTest.java b/test/jalview/gui/FreeUpMemoryTest.java
new file mode 100644 (file)
index 0000000..e93bfac
--- /dev/null
@@ -0,0 +1,216 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import jalview.analysis.AlignmentGenerator;
+import jalview.bin.Cache;
+import jalview.bin.Jalview;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceGroup;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class FreeUpMemoryTest
+{
+  private static final int ONE_MB = 1000 * 1000;
+
+  /**
+   * Configure (read-only) Jalview property settings for test
+   */
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Jalview.main(new String[] { "-nonews", "-props",
+        "test/jalview/testProps.jvprops" });
+    Cache.applicationProperties.setProperty("SHOW_ANNOTATIONS",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty("SHOW_QUALITY",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty("SHOW_CONSERVATION",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty("SHOW_OCCUPANCY",
+            Boolean.TRUE.toString());
+    Cache.applicationProperties.setProperty("SHOW_IDENTITY",
+            Boolean.TRUE.toString());
+  }
+
+  /**
+   * A simple test that memory is released when all windows are closed.
+   * <ul>
+   * <li>generates a reasonably large alignment and loads it</li>
+   * <li>performs various operations on the alignment</li>
+   * <li>closes all windows</li>
+   * <li>requests garbage collection</li>
+   * <li>asserts that the remaining memory footprint (heap usage) is 'not large'
+   * </li>
+   * </ul>
+   * If the test fails, this suggests that a reference to some large object
+   * (perhaps the alignment data, or some annotation / Tree / PCA data) has
+   * failed to be garbage collected. If this is the case, the heap will need to
+   * be inspected manually (suggest using jvisualvm) in order to track down
+   * where large objects are still referenced. The code (for example
+   * AlignmentViewport.dispose()) should then be updated to ensure references to
+   * large objects are set to null when they are no longer required.
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Memory")
+  public void testFreeMemoryOnClose() throws IOException
+  {
+    File f = generateAlignment();
+    f.deleteOnExit();
+
+    doStuffInJalview(f);
+
+    Desktop.instance.closeAll_actionPerformed(null);
+
+    checkUsedMemory(35L);
+  }
+
+  /**
+   * Requests garbage collection and then checks whether remaining memory in use
+   * is less than the expected value (in Megabytes)
+   * 
+   * @param expectedMax
+   */
+  protected void checkUsedMemory(long expectedMax)
+  {
+    /*
+     * request garbage collection and wait briefly for it to run;
+     * NB there is no guarantee when, or whether, it will do so
+     */
+    System.gc();
+    waitFor(100);
+
+    /*
+     * a second gc() call should not be necessary - but it is!
+     * the test passes with it, and fails without it
+     */
+    System.gc();
+    waitFor(100);
+
+    /*
+     * check used memory is 'reasonably low'
+     */
+    long availableMemory = Runtime.getRuntime().totalMemory() / ONE_MB;
+    long freeMemory = Runtime.getRuntime().freeMemory() / ONE_MB;
+    long usedMemory = availableMemory - freeMemory;
+
+    /*
+     * sanity check - fails if any frame was added after
+     * closeAll_actionPerformed
+     */
+    assertEquals(Desktop.instance.getAllFrames().length, 0);
+
+    /*
+     * if this assertion fails
+     * - set a breakpoint here
+     * - run jvisualvm to inspect a heap dump of Jalview
+     * - identify large objects in the heap and their referers
+     * - fix code as necessary to null the references on close
+     */
+    System.out.println("Used memory after gc = " + usedMemory + "MB");
+    assertTrue(usedMemory < expectedMax, String.format(
+            "Used memory %d should be less than %d (Recommend running test manually to verify)",
+            usedMemory,
+            expectedMax));
+  }
+
+  /**
+   * Loads an alignment from file and exercises various operations in Jalview
+   * 
+   * @param f
+   */
+  protected void doStuffInJalview(File f)
+  {
+    /*
+     * load alignment, wait for consensus and other threads to complete
+     */
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(f.getPath(),
+            DataSourceType.FILE);
+    while (af.getViewport().isCalcInProgress())
+    {
+      waitFor(200);
+    }
+
+    /*
+     * set a selection group - potential memory leak if it retains
+     * a reference to the alignment
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(0);
+    sg.setEndRes(100);
+    AlignmentI al = af.viewport.getAlignment();
+    for (int i = 0; i < al.getHeight(); i++)
+    {
+      sg.addSequence(al.getSequenceAt(i), false);
+    }
+    af.viewport.setSelectionGroup(sg);
+
+    /*
+     * compute Tree and PCA (on all sequences, 100 columns)
+     */
+    af.openTreePcaDialog();
+    CalculationChooser dialog = af.alignPanel.getCalculationDialog();
+    dialog.openPcaPanel("BLOSUM62", dialog.getSimilarityParameters(true));
+    dialog.openTreePanel("BLOSUM62", dialog.getSimilarityParameters(false));
+
+    /*
+     * wait until Tree and PCA have been computed
+     */
+    while (af.viewport.getCurrentTree() == null
+            && dialog.getPcaPanel().isWorking())
+    {
+      waitFor(10);
+    }
+
+    /*
+     * give Swing time to add the PCA panel (?!?)
+     */
+    waitFor(100);
+  }
+
+  /**
+   * Wait for waitMs miliseconds
+   * 
+   * @param waitMs
+   */
+  protected void waitFor(int waitMs)
+  {
+    try
+    {
+      Thread.sleep(waitMs);
+    } catch (InterruptedException e)
+    {
+    }
+  }
+
+  /**
+   * Generates an alignment and saves it in a temporary file, to be loaded by
+   * Jalview. We use a peptide alignment (so Conservation and Quality are
+   * calculated), which is wide enough to ensure Consensus, Conservation and
+   * Occupancy have a significant memory footprint (if not removed from the
+   * heap).
+   * 
+   * @return
+   * @throws IOException
+   */
+  private File generateAlignment() throws IOException
+  {
+    File f = File.createTempFile("MemoryTest", "fa");
+    PrintStream ps = new PrintStream(f);
+    AlignmentGenerator ag = new AlignmentGenerator(false, ps);
+    int width = 100000;
+    int height = 100;
+    ag.generate(width, height, 0, 10, 15);
+    return f;
+  }
+}
diff --git a/test/jalview/gui/PairwiseAlignmentPanelTest.java b/test/jalview/gui/PairwiseAlignmentPanelTest.java
new file mode 100644 (file)
index 0000000..3322ee8
--- /dev/null
@@ -0,0 +1,73 @@
+package jalview.gui;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceGroup;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import javax.swing.JTextArea;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class PairwiseAlignmentPanelTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor_withSelectionGroup()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport viewport = af.getViewport();
+    AlignmentI al = viewport.getAlignment();
+
+    /*
+     * select columns 29-36 of sequences 4 and 5 for alignment
+     * Q93XJ9_SOLTU/23-29 L-KAISNV
+     * FER1_PEA/26-32     V-TTTKAF
+     */
+    SequenceGroup sg = new SequenceGroup();
+    sg.addSequence(al.getSequenceAt(3), false);
+    sg.addSequence(al.getSequenceAt(4), false);
+    sg.setStartRes(28);
+    sg.setEndRes(35);
+    viewport.setSelectionGroup(sg);
+
+    PairwiseAlignPanel testee = new PairwiseAlignPanel(viewport);
+
+    String text = ((JTextArea) PA.getValue(testee, "textarea")).getText();
+    String expected = "Score = 80.0\n" + "Length of alignment = 4\n"
+            + "Sequence     FER1_PEA/29-32 (Sequence length = 7)\n"
+            + "Sequence Q93XJ9_SOLTU/23-26 (Sequence length = 7)\n\n"
+            + "    FER1_PEA/29-32 TKAF\n" + "                    ||.\n"
+            + "Q93XJ9_SOLTU/23-26 LKAI\n\n" + "Percentage ID = 50.00\n\n";
+    assertEquals(text, expected);
+  }
+
+  /**
+   * This test aligns the same sequences as testConstructor_withSelectionGroup
+   * but as a complete alignment (no selection). Note that in fact the user is
+   * currently required to make a selection in order to calculate pairwise
+   * alignments, so this case does not arise.
+   */
+  @Test(groups = "Functional")
+  public void testConstructor_noSelectionGroup()
+  {
+    String seqs = ">Q93XJ9_SOLTU/23-29\nL-KAISNV\n>FER1_PEA/26-32\nV-TTTKAF\n";
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(seqs,
+            DataSourceType.PASTE);
+    AlignViewport viewport = af.getViewport();
+
+    PairwiseAlignPanel testee = new PairwiseAlignPanel(viewport);
+
+    String text = ((JTextArea) PA.getValue(testee, "textarea")).getText();
+    String expected = "Score = 80.0\n" + "Length of alignment = 4\n"
+            + "Sequence     FER1_PEA/29-32 (Sequence length = 7)\n"
+            + "Sequence Q93XJ9_SOLTU/23-26 (Sequence length = 7)\n\n"
+            + "    FER1_PEA/29-32 TKAF\n" + "                    ||.\n"
+            + "Q93XJ9_SOLTU/23-26 LKAI\n\n" + "Percentage ID = 50.00\n\n";
+    assertEquals(text, expected);
+  }
+}
index 335240b..6f60588 100644 (file)
@@ -26,21 +26,31 @@ import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.bin.Cache;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
 import jalview.datamodel.Annotation;
+import jalview.datamodel.ColumnSelection;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.DBRefSource;
+import jalview.datamodel.HiddenColumns;
 import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
 import jalview.io.DataSourceType;
 import jalview.io.FileFormat;
 import jalview.io.FormatAdapter;
+import jalview.urls.api.UrlProviderFactoryI;
+import jalview.urls.desktop.DesktopUrlProviderFactory;
 import jalview.util.MessageManager;
+import jalview.util.UrlConstants;
 
 import java.awt.Component;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 
 import javax.swing.JMenu;
@@ -80,6 +90,25 @@ public class PopupMenuTest
   @BeforeMethod(alwaysRun = true)
   public void setUp() throws IOException
   {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    String inMenuString = ("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
+            + SEQUENCE_ID
+            + "$"
+            + "|"
+            + "UNIPROT | http://www.uniprot.org/uniprot/$" + DB_ACCESSION + "$")
+            + "|"
+            + ("INTERPRO | http://www.ebi.ac.uk/interpro/entry/$"
+                    + DB_ACCESSION + "$")
+            + "|"
+            +
+            // Gene3D entry tests for case (in)sensitivity
+            ("Gene3D | http://gene3d.biochem.ucl.ac.uk/Gene3D/search?sterm=$"
+                    + DB_ACCESSION + "$&mode=protein");
+
+    UrlProviderFactoryI factory = new DesktopUrlProviderFactory(
+            UrlConstants.DEFAULT_LABEL, inMenuString, "");
+    Preferences.sequenceUrlLinks = factory.createUrlProvider();
+
     alignment = new FormatAdapter().readFile(TEST_DATA,
             DataSourceType.PASTE, FileFormat.Fasta);
     AlignFrame af = new AlignFrame(alignment, 700, 500);
@@ -100,7 +129,7 @@ public class PopupMenuTest
   public void testConfigureReferenceAnnotationsMenu_noSequenceSelected()
   {
     JMenuItem menu = new JMenuItem();
-    List<SequenceI> seqs = new ArrayList<SequenceI>();
+    List<SequenceI> seqs = new ArrayList<>();
     testee.configureReferenceAnnotationsMenu(menu, seqs);
     assertFalse(menu.isEnabled());
     // now try null list
@@ -469,8 +498,8 @@ public class PopupMenuTest
     List<SequenceI> seqs = parentPanel.getAlignment().getSequences();
 
     // create list of links and list of DBRefs
-    List<String> links = new ArrayList<String>();
-    List<DBRefEntry> refs = new ArrayList<DBRefEntry>();
+    List<String> links = new ArrayList<>();
+    List<DBRefEntry> refs = new ArrayList<>();
 
     // links as might be added into Preferences | Connections dialog
     links.add("EMBL-EBI Search | http://www.ebi.ac.uk/ebisearch/search.ebi?db=allebi&query=$"
@@ -495,17 +524,19 @@ public class PopupMenuTest
 
     // add all the dbrefs to the sequences: Uniprot 1 each, Interpro all 3 to
     // seq0, Gene3D to seq1
-    seqs.get(0).addDBRef(refs.get(0));
+    SequenceI seq = seqs.get(0);
+    seq.addDBRef(refs.get(0));
 
-    seqs.get(0).addDBRef(refs.get(1));
-    seqs.get(0).addDBRef(refs.get(2));
-    seqs.get(0).addDBRef(refs.get(3));
+    seq.addDBRef(refs.get(1));
+    seq.addDBRef(refs.get(2));
+    seq.addDBRef(refs.get(3));
     
     seqs.get(1).addDBRef(refs.get(4));
     seqs.get(1).addDBRef(refs.get(5));
     
     // get the Popup Menu for first sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0), links);
+    List<SequenceFeature> noFeatures = Collections.<SequenceFeature> emptyList();
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     Component[] seqItems = testee.sequenceMenu.getMenuComponents();
     JMenu linkMenu = (JMenu) seqItems[6];
     Component[] linkItems = linkMenu.getMenuComponents();
@@ -519,15 +550,18 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 4; i++)
     {
-      assertEquals(refs.get(i - 1).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i - 1).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0]);
-      assertEquals(refs.get(i - 1).getAccessionId(),
+      assertEquals(msg, refs.get(i - 1).getAccessionId(),
               ((JMenuItem) linkItems[i])
               .getText().split("\\|")[1]);
     }
 
     // get the Popup Menu for second sequence
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(1), links);
+    seq = seqs.get(1);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     linkItems = linkMenu.getMenuComponents();
@@ -541,22 +575,136 @@ public class PopupMenuTest
     // sequence id for each link should match corresponding DB accession id
     for (int i = 1; i < 3; i++)
     {
-      assertEquals(refs.get(i + 3).getSource(), ((JMenuItem) linkItems[i])
+      String msg = seq.getName() + " link[" + i + "]";
+      assertEquals(msg, refs.get(i + 3).getSource(),
+              ((JMenuItem) linkItems[i])
               .getText().split("\\|")[0].toUpperCase());
-      assertEquals(refs.get(i + 3).getAccessionId(),
+      assertEquals(msg, refs.get(i + 3).getAccessionId(),
               ((JMenuItem) linkItems[i]).getText().split("\\|")[1]);
     }
 
     // if there are no valid links the Links submenu is disabled
-    List<String> nomatchlinks = new ArrayList<String>();
+    List<String> nomatchlinks = new ArrayList<>();
     nomatchlinks.add("NOMATCH | http://www.uniprot.org/uniprot/$"
             + DB_ACCESSION + "$");
 
-    testee = new PopupMenu(parentPanel, (Sequence) seqs.get(0),
-            nomatchlinks);
+    testee = new PopupMenu(parentPanel, seq, noFeatures);
     seqItems = testee.sequenceMenu.getMenuComponents();
     linkMenu = (JMenu) seqItems[6];
     assertFalse(linkMenu.isEnabled());
 
   }
+
+  /**
+   * Test for adding feature links
+   */
+  @Test(groups = { "Functional" })
+  public void testHideInsertions()
+  {
+    // get sequences from the alignment
+    List<SequenceI> seqs = parentPanel.getAlignment().getSequences();
+    
+    // add our own seqs to avoid problems with changes to existing sequences
+    // (gap at end of sequences varies depending on how tests are run!)
+    Sequence seqGap1 = new Sequence("GappySeq",
+            "AAAA----AA-AAAAAAA---AAA-----------AAAAAAAAAA--");
+    seqGap1.createDatasetSequence();
+    seqs.add(seqGap1);
+    Sequence seqGap2 = new Sequence("LessGappySeq",
+            "AAAAAA-AAAAA---AAA--AAAAA--AAAAAAA-AAAAAA");
+    seqGap2.createDatasetSequence();
+    seqs.add(seqGap2);
+    Sequence seqGap3 = new Sequence("AnotherGapSeq",
+            "AAAAAA-AAAAAA--AAAAAA-AAAAAAAAAAA---AAAAAAAA");
+    seqGap3.createDatasetSequence();
+    seqs.add(seqGap3);
+    Sequence seqGap4 = new Sequence("NoGaps",
+            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+    seqGap4.createDatasetSequence();
+    seqs.add(seqGap4);
+
+    ColumnSelection sel = new ColumnSelection();
+    parentPanel.av.getAlignment().getHiddenColumns()
+            .revealAllHiddenColumns(sel);
+
+    // get the Popup Menu for 7th sequence - no insertions
+    testee = new PopupMenu(parentPanel, seqs.get(7), null);
+    testee.hideInsertions_actionPerformed(null);
+    
+    HiddenColumns hidden = parentPanel.av.getAlignment().getHiddenColumns();
+    Iterator<int[]> it = hidden.iterator();
+    assertFalse(it.hasNext());
+
+    // get the Popup Menu for GappySeq - this time we have insertions
+    testee = new PopupMenu(parentPanel, seqs.get(4), null);
+    testee.hideInsertions_actionPerformed(null);
+    hidden = parentPanel.av.getAlignment().getHiddenColumns();
+    it = hidden.iterator();
+
+    assertTrue(it.hasNext());
+    int[] region = it.next();
+    assertEquals(region[0], 4);
+    assertEquals(region[1], 7);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 10);
+    assertEquals(region[1], 10);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 18);
+    assertEquals(region[1], 20);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 24);
+    assertEquals(region[1], 34);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 45);
+    assertEquals(region[1], 46);
+
+    assertFalse(it.hasNext());
+
+    sel = new ColumnSelection();
+    hidden.revealAllHiddenColumns(sel);
+
+    // make a sequence group and hide insertions within the group
+    SequenceGroup sg = new SequenceGroup();
+    sg.setStartRes(8);
+    sg.setEndRes(42);
+    sg.addSequence(seqGap2, false);
+    sg.addSequence(seqGap3, false);
+    parentPanel.av.setSelectionGroup(sg);
+
+    // hide columns outside and within selection
+    // only hidden columns outside the collection will be retained (unless also
+    // gaps in the selection)
+    hidden.hideColumns(1, 10);
+    hidden.hideColumns(31, 40);
+
+    // get the Popup Menu for LessGappySeq in the sequence group
+    testee = new PopupMenu(parentPanel, seqs.get(5), null);
+    testee.hideInsertions_actionPerformed(null);
+    hidden = parentPanel.av.getAlignment().getHiddenColumns();
+    it = hidden.iterator();
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 1);
+    assertEquals(region[1], 7);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 13);
+    assertEquals(region[1], 14);
+
+    assertTrue(it.hasNext());
+    region = it.next();
+    assertEquals(region[0], 34);
+    assertEquals(region[1], 34);
+  }
+
 }
index a1715e9..72a288b 100644 (file)
@@ -29,6 +29,7 @@ import java.awt.GridLayout;
 
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -119,8 +120,15 @@ public class ProgressBarTest
    * @param layout
    * @param msgs
    */
-  private void verifyProgress(GridLayout layout, String[] msgs)
+  private void verifyProgress(final GridLayout layout, final String[] msgs)
   {
+    try
+    {
+    SwingUtilities.invokeAndWait(new Runnable()
+    {
+      @Override
+      public void run()
+      {
     int msgCount = msgs.length;
     assertEquals(1 + msgCount, layout.getRows());
     assertEquals(msgCount, statusPanel.getComponentCount());
@@ -132,5 +140,13 @@ public class ProgressBarTest
       assertEquals(msgs[i++],
               ((JLabel) ((JPanel) c).getComponent(0)).getText());
     }
+      }
+    });
+    } catch (Exception e)
+    {
+      throw new AssertionError(
+              "Unexpected exception waiting for progress bar validation",
+              e);
+    }
   }
 }
diff --git a/test/jalview/gui/SeqCanvasTest.java b/test/jalview/gui/SeqCanvasTest.java
new file mode 100644 (file)
index 0000000..68ee98c
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.AlignmentI;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+
+import java.awt.Font;
+import java.awt.FontMetrics;
+
+import junit.extensions.PA;
+
+import org.testng.annotations.Test;
+
+public class SeqCanvasTest
+{
+  /**
+   * Test the method that computes wrapped width in residues, height of wrapped
+   * widths in pixels, and the number of widths visible
+   */
+  @Test(groups = "Functional")
+  public void testCalculateWrappedGeometry_noAnnotations()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+    assertEquals(al.getWidth(), 157);
+    assertEquals(al.getHeight(), 15);
+
+    av.setWrapAlignment(true);
+    av.getRanges().setStartEndSeq(0, 14);
+    av.setFont(new Font("SansSerif", Font.PLAIN, 14), true);
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+
+    SeqCanvas testee = af.alignPanel.getSeqPanel().seqCanvas;
+
+    /*
+     * first with scales above, left, right
+     */
+    av.setShowAnnotation(false);
+    av.setScaleAboveWrapped(true);
+    av.setScaleLeftWrapped(true);
+    av.setScaleRightWrapped(true);
+    FontMetrics fm = testee.getFontMetrics(av.getFont());
+    int labelWidth = fm.stringWidth("000") + charWidth;
+    assertEquals(labelWidth, 39); // 3 x 9 + charWidth
+
+    /*
+     * width 400 pixels leaves (400 - 2*labelWidth) for residue columns
+     * take the whole multiple of character widths
+     */
+    int canvasWidth = 400;
+    int canvasHeight = 300;
+    int residueColumns = (canvasWidth - 2 * labelWidth) / charWidth;
+    int wrappedWidth = testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(wrappedWidth, residueColumns);
+    assertEquals(PA.getValue(testee, "labelWidthWest"), labelWidth);
+    assertEquals(PA.getValue(testee, "labelWidthEast"), labelWidth);
+    assertEquals(PA.getValue(testee, "wrappedSpaceAboveAlignment"),
+            2 * charHeight);
+    int repeatingHeight = (int) PA.getValue(testee, "wrappedRepeatHeightPx");
+    assertEquals(repeatingHeight, charHeight * (2 + al.getHeight()));
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 1);
+
+    /*
+     * repeat height is 17 * (2 + 15) = 289
+     * make canvas height 2 * 289 + 3 * charHeight so just enough to
+     * draw 2 widths and the first sequence of a third
+     */
+    canvasHeight = charHeight * (17 * 2 + 3);
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
+
+    /*
+     * reduce canvas height by 1 pixel - should not be enough height
+     * to draw 3 widths
+     */
+    canvasHeight -= 1;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 2);
+
+    /*
+     * turn off scale above - can now fit in 2 and a bit widths
+     */
+    av.setScaleAboveWrapped(false);
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
+
+    /*
+     * reduce height to enough for 2 widths and not quite a third
+     * i.e. two repeating heights + spacer + sequence - 1 pixel
+     */
+    canvasHeight = charHeight * (16 * 2 + 2) - 1;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 2);
+
+    /*
+     * make canvas width enough for scales and 20 residues
+     */
+    canvasWidth = 2 * labelWidth + 20 * charWidth;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 20);
+
+    /*
+     * reduce width by 1 pixel - rounds down to 19 residues
+     */
+    canvasWidth -= 1;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 19);
+
+    /*
+     * turn off West scale - adds labelWidth (39) to available for residues
+     * which with the 11 remainder makes 50 which is 4 more charWidths rem 2
+     */
+    av.setScaleLeftWrapped(false);
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 23);
+
+    /*
+     * add 10 pixels to width to fit in another whole residue column
+     */
+    canvasWidth += 9;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 23);
+    canvasWidth += 1;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 24);
+
+    /*
+     * turn off East scale to gain 39 more pixels (3 columns remainder 3)
+     */
+    av.setScaleRightWrapped(false);
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 27);
+
+    /*
+     * add 9 pixels to width to gain a residue column
+     */
+    canvasWidth += 8;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 27);
+    canvasWidth += 1;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 28);
+
+    /*
+     * now West but not East scale - lose 39 pixels or 4 columns
+     */
+    av.setScaleLeftWrapped(true);
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 24);
+
+    /*
+     * adding 3 pixels to width regains one column
+     */
+    canvasWidth += 2;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 24);
+    canvasWidth += 1;
+    wrappedWidth = testee.calculateWrappedGeometry(canvasWidth,
+            canvasHeight);
+    assertEquals(wrappedWidth, 25);
+
+    /*
+     * turn off scales left and right, make width exactly 157 columns
+     */
+    av.setScaleLeftWrapped(false);
+    canvasWidth = al.getWidth() * charWidth;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 1);
+  }
+
+  /**
+   * Test the method that computes wrapped width in residues, height of wrapped
+   * widths in pixels, and the number of widths visible
+   */
+  @Test(groups = "Functional")
+  public void testCalculateWrappedGeometry_withAnnotations()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/uniref50.fa", DataSourceType.FILE);
+    AlignViewport av = af.getViewport();
+    AlignmentI al = av.getAlignment();
+    assertEquals(al.getWidth(), 157);
+    assertEquals(al.getHeight(), 15);
+  
+    av.setWrapAlignment(true);
+    av.getRanges().setStartEndSeq(0, 14);
+    av.setFont(new Font("SansSerif", Font.PLAIN, 14), true);
+    int charHeight = av.getCharHeight();
+    int charWidth = av.getCharWidth();
+    assertEquals(charHeight, 17);
+    assertEquals(charWidth, 12);
+  
+    SeqCanvas testee = af.alignPanel.getSeqPanel().seqCanvas;
+  
+    /*
+     * first with scales above, left, right
+     */
+    av.setShowAnnotation(true);
+    av.setScaleAboveWrapped(true);
+    av.setScaleLeftWrapped(true);
+    av.setScaleRightWrapped(true);
+    FontMetrics fm = testee.getFontMetrics(av.getFont());
+    int labelWidth = fm.stringWidth("000") + charWidth;
+    assertEquals(labelWidth, 39); // 3 x 9 + charWidth
+    int annotationHeight = testee.getAnnotationHeight();
+
+    /*
+     * width 400 pixels leaves (400 - 2*labelWidth) for residue columns
+     * take the whole multiple of character widths
+     */
+    int canvasWidth = 400;
+    int canvasHeight = 300;
+    int residueColumns = (canvasWidth - 2 * labelWidth) / charWidth;
+    int wrappedWidth = testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(wrappedWidth, residueColumns);
+    assertEquals(PA.getValue(testee, "labelWidthWest"), labelWidth);
+    assertEquals(PA.getValue(testee, "labelWidthEast"), labelWidth);
+    assertEquals(PA.getValue(testee, "wrappedSpaceAboveAlignment"),
+            2 * charHeight);
+    int repeatingHeight = (int) PA.getValue(testee, "wrappedRepeatHeightPx");
+    assertEquals(repeatingHeight, charHeight * (2 + al.getHeight())
+            + annotationHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 1);
+  
+    /*
+     * repeat height is 17 * (2 + 15) = 289 + annotationHeight = 507
+     * make canvas height 2 * 289 + 3 * charHeight so just enough to
+     * draw 2 widths and the first sequence of a third
+     */
+    canvasHeight = charHeight * (17 * 2 + 3) + 2 * annotationHeight;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
+  
+    /*
+     * reduce canvas height by 1 pixel - should not be enough height
+     * to draw 3 widths
+     */
+    canvasHeight -= 1;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 2);
+  
+    /*
+     * turn off scale above - can now fit in 2 and a bit widths
+     */
+    av.setScaleAboveWrapped(false);
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
+  
+    /*
+     * reduce height to enough for 2 widths and not quite a third
+     * i.e. two repeating heights + spacer + sequence - 1 pixel
+     */
+    canvasHeight = charHeight * (16 * 2 + 2) + 2 * annotationHeight - 1;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 2);
+
+    /*
+     * add 1 pixel to height - should now get 3 widths drawn
+     */
+    canvasHeight += 1;
+    testee.calculateWrappedGeometry(canvasWidth, canvasHeight);
+    assertEquals(PA.getValue(testee, "wrappedVisibleWidths"), 3);
+  }
+}
index 4535c93..f69e6b5 100644 (file)
@@ -21,6 +21,7 @@
 package jalview.gui;
 
 import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNotNull;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.datamodel.DBRefEntry;
@@ -28,8 +29,10 @@ import jalview.datamodel.DBRefSource;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceI;
+import jalview.fts.api.FTSData;
 import jalview.jbgui.GStructureChooser.FilterOption;
 
+import java.util.Collection;
 import java.util.Vector;
 
 import org.testng.annotations.AfterMethod;
@@ -37,6 +40,8 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class StructureChooserTest
 {
 
@@ -65,7 +70,7 @@ public class StructureChooserTest
     PDBEntry dbRef = new PDBEntry();
     dbRef.setId("1tim");
 
-    Vector<PDBEntry> pdbIds = new Vector<PDBEntry>();
+    Vector<PDBEntry> pdbIds = new Vector<>();
     pdbIds.add(dbRef);
 
     seq.setPDBId(pdbIds);
@@ -133,7 +138,7 @@ public class StructureChooserTest
     assertTrue(sc.getCmbFilterOption().getSelectedItem() != null);
     FilterOption filterOpt = (FilterOption) sc.getCmbFilterOption()
             .getSelectedItem();
-    assertEquals("Cached PDB Entries", filterOpt.getName());
+    assertEquals("Cached Structures", filterOpt.getName());
   }
 
   @Test(groups = { "Network" })
@@ -142,8 +147,10 @@ public class StructureChooserTest
     SequenceI[] selectedSeqs = new SequenceI[] { seq };
     StructureChooser sc = new StructureChooser(selectedSeqs, seq, null);
     sc.fetchStructuresMetaData();
-    assertTrue(sc.getDiscoveredStructuresSet() != null);
-    assertTrue(sc.getDiscoveredStructuresSet().size() > 0);
+    Collection<FTSData> ss = (Collection<FTSData>) PA.getValue(sc,
+            "discoveredStructuresSet");
+    assertNotNull(ss);
+    assertTrue(ss.size() > 0);
 
   }
 
index c1c1d5c..86342d2 100644 (file)
@@ -1,10 +1,17 @@
 package jalview.gui;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
 
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+
+import java.util.Map;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -20,9 +27,11 @@ public class StructureViewerTest
   }
 
   @Test(groups = "Functional")
-  public void testGetUniquePdbFiles()
+  public void testGetSequencesForPdbs()
   {
-    assertNull(StructureViewer.getUniquePdbFiles(null));
+    StructureViewer sv = new StructureViewer(null);
+
+    assertNull(sv.getSequencesForPdbs(null, null));
 
     PDBEntry pdbe1 = new PDBEntry("1A70", "A", Type.PDB, "path1");
     PDBEntry pdbe2 = new PDBEntry("3A6S", "A", Type.PDB, "path2");
@@ -30,13 +39,53 @@ public class StructureViewerTest
     PDBEntry pdbe4 = new PDBEntry("1GAQ", "A", Type.PDB, null);
     PDBEntry pdbe5 = new PDBEntry("3A6S", "B", Type.PDB, "path2");
     PDBEntry pdbe6 = new PDBEntry("1GAQ", "B", Type.PDB, null);
+    PDBEntry pdbe7 = new PDBEntry("1FOO", "Q", Type.PDB, null);
+
+    PDBEntry[] pdbs = new PDBEntry[] { pdbe1, pdbe2, pdbe3, pdbe4, pdbe5,
+        pdbe6, pdbe7 };
+
+    /*
+     * seq1 ... seq6 associated with pdbe1 ... pdbe6
+     */
+    SequenceI[] seqs = new SequenceI[pdbs.length];
+    for (int i = 0; i < seqs.length; i++)
+    {
+      seqs[i] = new Sequence("Seq" + i, "abc");
+    }
 
     /*
-     * pdbe2 and pdbe5 get removed as having a duplicate file path
+     * pdbe3/5/6 should get removed as having a duplicate file path
      */
-    PDBEntry[] uniques = StructureViewer.getUniquePdbFiles(new PDBEntry[] {
-        pdbe1, pdbe2, pdbe3, pdbe4, pdbe5, pdbe6 });
-    assertEquals(uniques,
- new PDBEntry[] { pdbe1, pdbe2, pdbe4, pdbe6 });
+    Map<PDBEntry, SequenceI[]> uniques = sv.getSequencesForPdbs(pdbs, seqs);
+    assertTrue(uniques.containsKey(pdbe1));
+    assertTrue(uniques.containsKey(pdbe2));
+    assertFalse(uniques.containsKey(pdbe3));
+    assertTrue(uniques.containsKey(pdbe4));
+    assertFalse(uniques.containsKey(pdbe5));
+    assertFalse(uniques.containsKey(pdbe6));
+    assertTrue(uniques.containsKey(pdbe7));
+
+    // 1A70 associates with seq1 and seq3
+    SequenceI[] ss = uniques.get(pdbe1);
+    assertEquals(ss.length, 2);
+    assertSame(seqs[0], ss[0]);
+    assertSame(seqs[2], ss[1]);
+
+    // 3A6S has seq2 and seq5
+    ss = uniques.get(pdbe2);
+    assertEquals(ss.length, 2);
+    assertSame(seqs[1], ss[0]);
+    assertSame(seqs[4], ss[1]);
+
+    // 1GAQ has seq4 and seq6
+    ss = uniques.get(pdbe4);
+    assertEquals(ss.length, 2);
+    assertSame(seqs[3], ss[0]);
+    assertSame(seqs[5], ss[1]);
+
+    // 1FOO has seq7
+    ss = uniques.get(pdbe7);
+    assertEquals(ss.length, 1);
+    assertSame(seqs[6], ss[0]);
   }
 }
index e14a478..c0038a1 100644 (file)
@@ -39,6 +39,7 @@ import jalview.structure.StructureImportSettings.StructureParser;
 import java.io.File;
 import java.util.List;
 
+import org.junit.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
@@ -101,18 +102,19 @@ public class AnnotatedPDBFileInputTest
     }
   }
 
-  @Test(groups = { "Functional" })
+  @Test(groups = { "Functional" }, enabled = false)
   public void checkPDBannotationSource()
   {
-
+    Assert.fail(
+            "This test is incorrect - does not verify that JmolParser's annotation rows can be recognised as generated by the Jmol parser.");
     for (SequenceI asq : al.getSequences())
     {
       for (AlignmentAnnotation aa : asq.getAnnotation())
       {
 
         System.out.println("CalcId: " + aa.getCalcId());
-        if (StructureImportSettings.getDefaultPDBFileParser().equals(
-                StructureParser.JALVIEW_PARSER))
+        if (StructureImportSettings.getDefaultPDBFileParser()
+                .equals(StructureParser.JALVIEW_PARSER))
         {
           assertTrue(MCview.PDBfile.isCalcIdForFile(aa, pdbId));
         }
diff --git a/test/jalview/io/ClustalFileTest.java b/test/jalview/io/ClustalFileTest.java
new file mode 100644 (file)
index 0000000..1da2c75
--- /dev/null
@@ -0,0 +1,67 @@
+package jalview.io;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import jalview.datamodel.SequenceI;
+
+import java.io.IOException;
+
+import org.testng.annotations.Test;
+
+public class ClustalFileTest
+{
+  @Test(groups="Functional")
+  public void testParse_withNumbering() throws IOException
+  {
+    //@formatter:off
+    String data = "CLUSTAL\n\n"
+            + "FER_CAPAA/1-8      -----------------------------------------------------------A\t1\n"
+            + "FER_CAPAN/1-55     MA------SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALFGLKS-A--NGGKVTCMA 48\n"
+            + "FER1_SOLLC/1-55    MA------SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA 48\n"
+            + "Q93XJ9_SOLTU/1-55  MA------SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA 48\n"
+            + "FER1_PEA/1-60      MATT---PALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFLGLKT-SLKRGDLAVAMA 53\n\n"
+            + "FER_CAPAA/1-8      SYKVKLI 8\n"
+            + "FER_CAPAN/1-55     SYKVKLI 55\n"
+            + "FER1_SOLLC/1-55    SYKVKLI 55\n"
+            + "Q93XJ9_SOLTU/1-55  SYKVKLI 55\n"
+            + "FER1_PEA/1-60      SYKVKLV 60\n"
+            + "                   .*     .:....*******..** ..........**  ********...*:::*  ...\n"
+            + "\t\t.:.::.  *\n";
+    //@formatter:on
+    ClustalFile cf = new ClustalFile(data, DataSourceType.PASTE);
+    cf.parse();
+    SequenceI[] seqs = cf.getSeqsAsArray();
+    assertEquals(seqs.length, 5);
+    assertEquals(seqs[0].getName(), "FER_CAPAA");
+    assertEquals(seqs[0].getStart(), 1);
+    assertEquals(seqs[0].getEnd(), 8);
+    assertTrue(seqs[0].getSequenceAsString().endsWith("ASYKVKLI"));
+  }
+
+  @Test(groups="Functional")
+  public void testParse_noNumbering() throws IOException
+  {
+    //@formatter:off
+    String data = "CLUSTAL\n\n"
+            + "FER_CAPAA/1-8      -----------------------------------------------------------A\n"
+            + "FER_CAPAN/1-55     MA------SVSATMISTSFMPRKPAVTSL-KPIPNVGE--ALFGLKS-A--NGGKVTCMA\n"
+            + "FER1_SOLLC/1-55    MA------SISGTMISTSFLPRKPAVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA\n"
+            + "Q93XJ9_SOLTU/1-55  MA------SISGTMISTSFLPRKPVVTSL-KAISNVGE--ALFGLKS-G--RNGRITCMA\n"
+            + "FER1_PEA/1-60      MATT---PALYGTAVSTSFLRTQPMPMSV-TTTKAFSN--GFLGLKT-SLKRGDLAVAMA\n\n"
+            + "FER_CAPAA/1-8      SYKVKLI\n"
+            + "FER_CAPAN/1-55     SYKVKLI\n"
+            + "FER1_SOLLC/1-55    SYKVKLI\n"
+            + "Q93XJ9_SOLTU/1-55  SYKVKLI\n"
+            + "FER1_PEA/1-60      SYKVKLV\n";
+    //@formatter:on
+    ClustalFile cf = new ClustalFile(data, DataSourceType.PASTE);
+    cf.parse();
+    SequenceI[] seqs = cf.getSeqsAsArray();
+    assertEquals(seqs.length, 5);
+    assertEquals(seqs[0].getName(), "FER_CAPAA");
+    assertEquals(seqs[0].getStart(), 1);
+    assertEquals(seqs[0].getEnd(), 8);
+    assertTrue(seqs[0].getSequenceAsString().endsWith("ASYKVKLI"));
+  }
+}
index 0715857..2b8a62f 100644 (file)
@@ -39,9 +39,13 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import junit.extensions.PA;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 @Test(singleThreaded = true)
@@ -56,6 +60,14 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
     JvOptionPane.setMockResponse(JvOptionPane.CANCEL_OPTION);
   }
 
+  @DataProvider(name = "initialAccessions")
+  static Object[][] getAccessions()
+  {
+    return new String[][] { { "UNIPROT", "P00338" },
+        { "UNIPROT", "Q8Z9G6" },
+        { "ENSEMBLGENOMES", "CAD01290" } };
+  }
+
   /**
    * test store and recovery of all reachable cross refs from all reachable
    * crossrefs for one or more fetched db refs. Currently, this test has a known
@@ -63,8 +75,13 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
    * 
    * @throws Exception
    */
-  @Test(groups = { "Operational" }, enabled = true)
-  public void testRetrieveAndShowCrossref() throws Exception
+  @Test(
+    groups =
+    { "Operational" },
+    dataProvider = "initialAccessions",
+    enabled = true)
+  public void testRetrieveAndShowCrossref(String forSource,
+          String forAccession) throws Exception
   {
 
     List<String> failedDBRetr = new ArrayList<>();
@@ -90,12 +107,12 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
     // . codonframes
     //
     //
-    HashMap<String, String> dbtoviewBit = new HashMap<>();
+    Map<String, String> dbtoviewBit = new HashMap<>();
     List<String> keyseq = new ArrayList<>();
-    HashMap<String, File> savedProjects = new HashMap<>();
+    Map<String, File> savedProjects = new HashMap<>();
 
-    for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
-    {
+//    for (String[] did : new String[][] { { "UNIPROT", "P00338" } })
+//    {
       // pass counters - 0 - first pass, 1 means retrieve project rather than
       // perform action
       int pass1 = 0, pass2 = 0, pass3 = 0;
@@ -105,7 +122,7 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
       // { pass 2 = 0 { pass 3 = 0 } }
       do
       {
-        String first = did[0] + " " + did[1];
+        String first = forSource + " " + forAccession;//did[0] + " " + did[1];
         AlignFrame af = null;
         boolean dna;
         AlignmentI retral;
@@ -117,7 +134,8 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
           // retrieve dbref
 
           List<AlignFrame> afs = jalview.gui.SequenceFetcher.fetchAndShow(
-                  did[0], did[1]);
+                forSource, forAccession);
+        // did[0], did[1]);
           if (afs.size() == 0)
           {
             failedDBRetr.add("Didn't retrieve " + first);
@@ -186,15 +204,16 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
 
             if (pass2 == 0)
             { // retrieve and show cross-refs in this thread
-              cra = new CrossRefAction(af, seqs, dna, db);
+              cra = CrossRefAction.getHandlerFor(seqs, dna, db, af);
               cra.run();
-              if (cra.getXrefViews().size() == 0)
+              cra_views = (List<AlignmentViewPanel>) PA.getValue(cra,
+                      "xrefViews");
+              if (cra_views.size() == 0)
               {
                 failedXrefMenuItems.add("No crossrefs retrieved for "
                         + first + " -> " + db);
                 continue;
               }
-              cra_views = cra.getXrefViews();
               assertNucleotide(cra_views.get(0),
                       "Nucleotide panel included proteins for " + first
                               + " -> " + db);
@@ -286,16 +305,18 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
 
                   if (pass3 == 0)
                   {
-
                     SequenceI[] xrseqs = avp.getAlignment()
                             .getSequencesArray();
                     AlignFrame nextaf = Desktop.getAlignFrameFor(avp
                             .getAlignViewport());
 
-                    cra = new CrossRefAction(nextaf, xrseqs, avp
-                            .getAlignViewport().isNucleotide(), xrefdb);
+                    cra = CrossRefAction.getHandlerFor(xrseqs, avp
+                            .getAlignViewport().isNucleotide(), xrefdb,
+                            nextaf);
                     cra.run();
-                    if (cra.getXrefViews().size() == 0)
+                    cra_views2 = (List<AlignmentViewPanel>) PA.getValue(
+                            cra, "xrefViews");
+                    if (cra_views2.size() == 0)
                     {
                       failedXrefMenuItems
                               .add("No crossrefs retrieved for '"
@@ -303,7 +324,6 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
                                       + " via '" + nextaf.getTitle() + "'");
                       continue;
                     }
-                    cra_views2 = cra.getXrefViews();
                     assertNucleotide(cra_views2.get(0),
                             "Nucleotide panel included proteins for '"
                                     + nextxref + "' to " + xrefdb
@@ -411,7 +431,7 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
           pass1++;
         }
       } while (pass1 < 3);
-    }
+
     if (failedXrefMenuItems.size() > 0)
     {
       for (String s : failedXrefMenuItems)
@@ -541,8 +561,8 @@ public class CrossRef2xmlTests extends Jalview2xmlBase
    *          viewpanel needs to be called with a distinct xrefpath to ensure
    *          each one's strings are compared)
    */
-  private void stringify(HashMap<String, String> dbtoviewBit,
-          HashMap<String, File> savedProjects, String xrefpath,
+  private void stringify(Map<String, String> dbtoviewBit,
+          Map<String, File> savedProjects, String xrefpath,
           AlignmentViewPanel avp)
   {
     if (savedProjects != null)
index 152ab84..32ca841 100644 (file)
@@ -23,7 +23,9 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertSame;
 import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
 import jalview.api.FeatureColourI;
 import jalview.api.FeatureRenderer;
@@ -32,11 +34,17 @@ import jalview.datamodel.AlignmentI;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherI;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.datamodel.features.SequenceFeatures;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
+import jalview.schemes.FeatureColour;
 import jalview.structure.StructureSelectionManager;
+import jalview.util.matcher.Condition;
 
 import java.awt.Color;
 import java.io.File;
@@ -44,6 +52,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -467,10 +476,10 @@ public class FeaturesFileTest
      */
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
     Map<String, FeatureColourI> visible = fr.getDisplayedFeatureCols();
-    List<String> visibleGroups = new ArrayList<String>(
+    List<String> visibleGroups = new ArrayList<>(
             Arrays.asList(new String[] {}));
     String exported = featuresFile.printJalviewFormat(
-            al.getSequencesArray(), visible, visibleGroups, false);
+            al.getSequencesArray(), visible, null, visibleGroups, false);
     String expected = "No Features Visible";
     assertEquals(expected, exported);
 
@@ -479,7 +488,7 @@ public class FeaturesFileTest
      */
     visibleGroups.add("uniprot");
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, true);
+            visible, null, visibleGroups, true);
     expected = "Cath\tFER_CAPAA\t-1\t0\t0\tDomain\t0.0\n"
             + "desc1\tFER_CAPAN\t-1\t0\t0\tPfam\t1.3\n"
             + "desc3\tFER1_SOLLC\t-1\t0\t0\tPfam\n" // NaN is not output
@@ -493,9 +502,9 @@ public class FeaturesFileTest
     fr.setVisible("GAMMA-TURN");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, false);
+            visible, null, visibleGroups, false);
     expected = "METAL\tcc9900\n"
-            + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
+            + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
@@ -508,13 +517,13 @@ public class FeaturesFileTest
     fr.setVisible("Pfam");
     visible = fr.getDisplayedFeatureCols();
     exported = featuresFile.printJalviewFormat(al.getSequencesArray(),
-            visible, visibleGroups, false);
+            visible, null, visibleGroups, false);
     /*
      * features are output within group, ordered by sequence and by type
      */
     expected = "METAL\tcc9900\n"
             + "Pfam\tff0000\n"
-            + "GAMMA-TURN\tff0000|00ffff|20.0|95.0|below|66.0\n"
+            + "GAMMA-TURN\tscore|ff0000|00ffff|noValueMin|20.0|95.0|below|66.0\n"
             + "\nSTARTGROUP\tuniprot\n"
             + "Turn\tFER_CAPAA\t-1\t36\t38\tGAMMA-TURN\t0.0\n"
             + "Iron\tFER_CAPAA\t-1\t39\t39\tMETAL\t0.0\n"
@@ -539,8 +548,8 @@ public class FeaturesFileTest
      */
     FeaturesFile featuresFile = new FeaturesFile();
     FeatureRenderer fr = af.alignPanel.getFeatureRenderer();
-    Map<String, FeatureColourI> visible = new HashMap<String, FeatureColourI>();
-    List<String> visibleGroups = new ArrayList<String>(
+    Map<String, FeatureColourI> visible = new HashMap<>();
+    List<String> visibleGroups = new ArrayList<>(
             Arrays.asList(new String[] {}));
     String exported = featuresFile.printGffFormat(al.getSequencesArray(),
             visible, visibleGroups, false);
@@ -623,4 +632,79 @@ public class FeaturesFileTest
             + "FER_CAPAN\tUniprot\tPfam\t20\t20\t0.0\t+\t2\tx=y;black=white\n";
     assertEquals(expected, exported);
   }
+
+  /**
+   * Test for parsing of feature filters as represented in a Jalview features
+   * file
+   * 
+   * @throws Exception
+   */
+  @Test(groups = { "Functional" })
+  public void testParseFilters() throws Exception
+  {
+    Map<String, FeatureMatcherSetI> filters = new HashMap<>();
+    String text = "sequence_variant\tCSQ:PolyPhen NotContains 'damaging'\n"
+            + "missense_variant\t(label contains foobar) and (Score lt 1.3)";
+    FeaturesFile featuresFile = new FeaturesFile(text,
+            DataSourceType.PASTE);
+    featuresFile.parseFilters(filters);
+    assertEquals(filters.size(), 2);
+
+    FeatureMatcherSetI fm = filters.get("sequence_variant");
+    assertNotNull(fm);
+    Iterator<FeatureMatcherI> matchers = fm.getMatchers().iterator();
+    FeatureMatcherI matcher = matchers.next();
+    assertFalse(matchers.hasNext());
+    String[] attributes = matcher.getAttribute();
+    assertArrayEquals(attributes, new String[] { "CSQ", "PolyPhen" });
+    assertSame(matcher.getMatcher().getCondition(), Condition.NotContains);
+    assertEquals(matcher.getMatcher().getPattern(), "damaging");
+
+    fm = filters.get("missense_variant");
+    assertNotNull(fm);
+    matchers = fm.getMatchers().iterator();
+    matcher = matchers.next();
+    assertTrue(matcher.isByLabel());
+    assertSame(matcher.getMatcher().getCondition(), Condition.Contains);
+    assertEquals(matcher.getMatcher().getPattern(), "foobar");
+    matcher = matchers.next();
+    assertTrue(matcher.isByScore());
+    assertSame(matcher.getMatcher().getCondition(), Condition.LT);
+    assertEquals(matcher.getMatcher().getPattern(), "1.3");
+    assertEquals(matcher.getMatcher().getFloatValue(), 1.3f);
+
+    assertFalse(matchers.hasNext());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testOutputFeatureFilters()
+  {
+    FeaturesFile ff = new FeaturesFile();
+    StringBuilder sb = new StringBuilder();
+    Map<String, FeatureColourI> visible = new HashMap<>();
+    visible.put("pfam", new FeatureColour(Color.red));
+    Map<String, FeatureMatcherSetI> featureFilters = new HashMap<>();
+
+    // with no filters, nothing is output
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    assertEquals("", sb.toString());
+
+    // with filter for not visible features only, nothing is output
+    FeatureMatcherSet filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byLabel(Condition.Present, null));
+    featureFilters.put("foobar", filter);
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    assertEquals("", sb.toString());
+
+    // with filters for visible feature types
+    FeatureMatcherSet filter2 = new FeatureMatcherSet();
+    filter2.and(FeatureMatcher.byAttribute(Condition.Present, null, "CSQ",
+            "PolyPhen"));
+    filter2.and(FeatureMatcher.byScore(Condition.LE, "-2.4"));
+    featureFilters.put("pfam", filter2);
+    visible.put("foobar", new FeatureColour(Color.blue));
+    ff.outputFeatureFilters(sb, visible, featureFilters);
+    String expected = "\nSTARTFILTERS\nfoobar\tLabel Present\npfam\t(CSQ:PolyPhen Present) AND (Score LE -2.4)\nENDFILTERS\n\n";
+    assertEquals(expected, sb.toString());
+  }
 }
index 158c901..5e835bf 100644 (file)
@@ -42,6 +42,7 @@ import jalview.schemes.ResidueColourScheme;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
@@ -205,7 +206,7 @@ public class JSONFileTest
     TEST_SEQ_HEIGHT = expectedSeqs.size();
     TEST_GRP_HEIGHT = expectedGrps.size();
     TEST_ANOT_HEIGHT = expectedAnnots.size();
-    TEST_CS_HEIGHT = expectedColSel.getHiddenColumnsCopy().size();
+    TEST_CS_HEIGHT = expectedColSel.getNumberOfRegions();
 
     exportSettings = new AlignExportSettingI()
     {
@@ -325,11 +326,12 @@ public class JSONFileTest
   {
     HiddenColumns cs = testJsonFile.getHiddenColumns();
     Assert.assertNotNull(cs);
-    Assert.assertNotNull(cs.getHiddenColumnsCopy());
-    List<int[]> hiddenCols = cs.getHiddenColumnsCopy();
-    Assert.assertEquals(hiddenCols.size(), TEST_CS_HEIGHT);
-    Assert.assertEquals(hiddenCols.get(0), expectedColSel
-            .getHiddenColumnsCopy().get(0),
+
+    Iterator<int[]> it = cs.iterator();
+    Iterator<int[]> colselit = expectedColSel.iterator();
+    Assert.assertTrue(it.hasNext());
+    Assert.assertEquals(cs.getNumberOfRegions(), TEST_CS_HEIGHT);
+    Assert.assertEquals(it.next(), colselit.next(),
             "Mismatched hidden columns!");
   }
 
index 15e18e3..fbdd782 100644 (file)
@@ -76,7 +76,8 @@ public class Jalview2xmlBase
   @BeforeTest(alwaysRun = true)
   public static void clearDesktop()
   {
-    if (Desktop.instance != null && Desktop.getAlignFrames() != null)
+    if (Desktop.instance != null && Desktop.getFrames() != null
+            && Desktop.getFrames().length > 0)
     {
       Desktop.instance.closeAll_actionPerformed(null);
     }
index 6abb7e5..53bb0e7 100644 (file)
@@ -23,11 +23,13 @@ package jalview.io;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertSame;
 import static org.testng.Assert.assertTrue;
 
 import jalview.api.AlignViewportI;
 import jalview.api.AlignmentViewPanel;
+import jalview.api.FeatureColourI;
 import jalview.api.ViewStyleI;
 import jalview.datamodel.AlignmentAnnotation;
 import jalview.datamodel.AlignmentI;
@@ -35,12 +37,17 @@ import jalview.datamodel.HiddenSequences;
 import jalview.datamodel.PDBEntry;
 import jalview.datamodel.PDBEntry.Type;
 import jalview.datamodel.SequenceCollectionI;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceGroup;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.AlignFrame;
 import jalview.gui.AlignViewport;
 import jalview.gui.AlignmentPanel;
 import jalview.gui.Desktop;
+import jalview.gui.FeatureRenderer;
 import jalview.gui.Jalview2XML;
 import jalview.gui.JvOptionPane;
 import jalview.gui.PopupMenu;
@@ -50,13 +57,16 @@ import jalview.schemes.AnnotationColourGradient;
 import jalview.schemes.BuriedColourScheme;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
+import jalview.schemes.FeatureColour;
 import jalview.schemes.JalviewColourScheme;
 import jalview.schemes.RNAHelicesColour;
 import jalview.schemes.StrandColourScheme;
 import jalview.schemes.TCoffeeColourScheme;
 import jalview.structure.StructureImportSettings;
+import jalview.util.matcher.Condition;
 import jalview.viewmodel.AlignmentViewport;
 
+import java.awt.Color;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -247,6 +257,31 @@ public class Jalview2xmlTests extends Jalview2xmlBase
 
   }
 
+  /**
+   * Test for JAL-2223 - multiple mappings in View Mapping report
+   * 
+   * @throws Exception
+   */
+  @Test(groups = { "Functional" })
+  public void noDuplicatePdbMappingsMade() throws Exception
+  {
+    StructureImportSettings.setProcessSecondaryStructure(true);
+    StructureImportSettings.setVisibleChainAnnotation(true);
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            "examples/exampleFile_2_7.jar", DataSourceType.FILE);
+    assertNotNull(af, "Didn't read in the example file correctly.");
+
+    // locate Jmol viewer
+    // count number of PDB mappings the structure selection manager holds -
+    String pdbFile = af.getCurrentView().getStructureSelectionManager()
+            .findFileForPDBId("1A70");
+    assertEquals(
+            af.getCurrentView().getStructureSelectionManager()
+                    .getMapping(pdbFile).length,
+            2, "Expected only two mappings for 1A70");
+
+  }
+
   @Test(groups = { "Functional" })
   public void viewRefPdbAnnotation() throws Exception
   {
@@ -413,7 +448,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     String afid = af.getViewport().getSequenceSetId();
 
     // remember reference sequence for each panel
-    Map<String, SequenceI> refseqs = new HashMap<String, SequenceI>();
+    Map<String, SequenceI> refseqs = new HashMap<>();
 
     /*
      * mark sequence 2, 3, 4.. in panels 1, 2, 3...
@@ -551,8 +586,8 @@ public class Jalview2xmlTests extends Jalview2xmlBase
      * remember representative and hidden sequences marked 
      * on each panel
      */
-    Map<String, SequenceI> repSeqs = new HashMap<String, SequenceI>();
-    Map<String, List<String>> hiddenSeqNames = new HashMap<String, List<String>>();
+    Map<String, SequenceI> repSeqs = new HashMap<>();
+    Map<String, List<String>> hiddenSeqNames = new HashMap<>();
 
     /*
      * mark sequence 2, 3, 4.. in panels 1, 2, 3...
@@ -568,7 +603,7 @@ public class Jalview2xmlTests extends Jalview2xmlBase
       repIndex = Math.max(repIndex, 1);
       SequenceI repSeq = alignment.getSequenceAt(repIndex);
       repSeqs.put(ap.getViewName(), repSeq);
-      List<String> hiddenNames = new ArrayList<String>();
+      List<String> hiddenNames = new ArrayList<>();
       hiddenSeqNames.put(ap.getViewName(), hiddenNames);
 
       /*
@@ -841,4 +876,163 @@ public class Jalview2xmlTests extends Jalview2xmlBase
     assertTrue(rs.conservationApplied());
     assertEquals(rs.getConservationInc(), 30);
   }
+
+  /**
+   * Test save and reload of feature colour schemes and filter settings
+   * 
+   * @throws IOException
+   */
+  @Test(groups = { "Functional" })
+  public void testSaveLoadFeatureColoursAndFilters() throws IOException
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(
+            ">Seq1\nACDEFGHIKLM", DataSourceType.PASTE);
+    SequenceI seq1 = af.getViewport().getAlignment().getSequenceAt(0);
+
+    /*
+     * add some features to the sequence
+     */
+    int score = 1;
+    addFeatures(seq1, "type1", score++);
+    addFeatures(seq1, "type2", score++);
+    addFeatures(seq1, "type3", score++);
+    addFeatures(seq1, "type4", score++);
+    addFeatures(seq1, "type5", score++);
+
+    /*
+     * set colour schemes for features
+     */
+    FeatureRenderer fr = af.getFeatureRenderer();
+    fr.findAllFeatures(true);
+
+    // type1: red
+    fr.setColour("type1", new FeatureColour(Color.red));
+
+    // type2: by label
+    FeatureColourI byLabel = new FeatureColour();
+    byLabel.setColourByLabel(true);
+    fr.setColour("type2", byLabel);
+
+    // type3: by score above threshold
+    FeatureColourI byScore = new FeatureColour(Color.BLACK, Color.BLUE, 1,
+            10);
+    byScore.setAboveThreshold(true);
+    byScore.setThreshold(2f);
+    fr.setColour("type3", byScore);
+
+    // type4: by attribute AF
+    FeatureColourI byAF = new FeatureColour();
+    byAF.setColourByLabel(true);
+    byAF.setAttributeName("AF");
+    fr.setColour("type4", byAF);
+
+    // type5: by attribute CSQ:PolyPhen below threshold
+    FeatureColourI byPolyPhen = new FeatureColour(Color.BLACK, Color.BLUE,
+            1, 10);
+    byPolyPhen.setBelowThreshold(true);
+    byPolyPhen.setThreshold(3f);
+    byPolyPhen.setAttributeName("CSQ", "PolyPhen");
+    fr.setColour("type5", byPolyPhen);
+
+    /*
+     * set filters for feature types
+     */
+
+    // filter type1 features by (label contains "x")
+    FeatureMatcherSetI filterByX = new FeatureMatcherSet();
+    filterByX.and(FeatureMatcher.byLabel(Condition.Contains, "x"));
+    fr.setFeatureFilter("type1", filterByX);
+
+    // filter type2 features by (score <= 2.4 and score > 1.1)
+    FeatureMatcherSetI filterByScore = new FeatureMatcherSet();
+    filterByScore.and(FeatureMatcher.byScore(Condition.LE, "2.4"));
+    filterByScore.and(FeatureMatcher.byScore(Condition.GT, "1.1"));
+    fr.setFeatureFilter("type2", filterByScore);
+
+    // filter type3 features by (AF contains X OR CSQ:PolyPhen != 0)
+    FeatureMatcherSetI filterByXY = new FeatureMatcherSet();
+    filterByXY
+            .and(FeatureMatcher.byAttribute(Condition.Contains, "X", "AF"));
+    filterByXY.or(FeatureMatcher.byAttribute(Condition.NE, "0", "CSQ",
+            "PolyPhen"));
+    fr.setFeatureFilter("type3", filterByXY);
+
+    /*
+     * save as Jalview project
+     */
+    File tfile = File.createTempFile("JalviewTest", ".jvp");
+    tfile.deleteOnExit();
+    String filePath = tfile.getAbsolutePath();
+    assertTrue(af.saveAlignment(filePath, FileFormat.Jalview),
+            "Failed to store as a project.");
+
+    /*
+     * close current alignment and load the saved project
+     */
+    af.closeMenuItem_actionPerformed(true);
+    af = null;
+    af = new FileLoader()
+            .LoadFileWaitTillLoaded(filePath, DataSourceType.FILE);
+    assertNotNull(af, "Failed to import new project");
+
+    /*
+     * verify restored feature colour schemes and filters
+     */
+    fr = af.getFeatureRenderer();
+    FeatureColourI fc = fr.getFeatureStyle("type1");
+    assertTrue(fc.isSimpleColour());
+    assertEquals(fc.getColour(), Color.red);
+    fc = fr.getFeatureStyle("type2");
+    assertTrue(fc.isColourByLabel());
+    fc = fr.getFeatureStyle("type3");
+    assertTrue(fc.isGraduatedColour());
+    assertNull(fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(fc.getThreshold(), 2f);
+    fc = fr.getFeatureStyle("type4");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "AF" });
+    fc = fr.getFeatureStyle("type5");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertEquals(fc.getAttributeName(), new String[] { "CSQ", "PolyPhen" });
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(fc.getThreshold(), 3f);
+
+    assertEquals(fr.getFeatureFilter("type1").toStableString(),
+            "Label Contains x");
+    assertEquals(fr.getFeatureFilter("type2").toStableString(),
+            "(Score LE 2.4) AND (Score GT 1.1)");
+    assertEquals(fr.getFeatureFilter("type3").toStableString(),
+            "(AF Contains X) OR (CSQ:PolyPhen NE 0.0)");
+  }
+
+  private void addFeature(SequenceI seq, String featureType, int score)
+  {
+    SequenceFeature sf = new SequenceFeature(featureType, "desc", 1, 2,
+            score, "grp");
+    sf.setValue("AF", score);
+    sf.setValue("CSQ", new HashMap<String, String>()
+    {
+      {
+        put("PolyPhen", Integer.toString(score));
+      }
+    });
+    seq.addSequenceFeature(sf);
+  }
+
+  /**
+   * Adds two features of the given type to the given sequence, also setting the
+   * score as the value of attribute "AF" and sub-attribute "CSQ:PolyPhen"
+   * 
+   * @param seq
+   * @param featureType
+   * @param score
+   */
+  private void addFeatures(SequenceI seq, String featureType, int score)
+  {
+    addFeature(seq, featureType, score++);
+    addFeature(seq, featureType, score);
+  }
 }
index 9e61bec..87e35c7 100644 (file)
@@ -23,15 +23,18 @@ package jalview.io;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.DBRefEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 import jalview.io.gff.GffConstants;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel;
 
-import java.util.HashMap;
-import java.util.Hashtable;
+import java.awt.Color;
 import java.util.Map;
 
 import junit.extensions.PA;
@@ -95,8 +98,9 @@ public class SequenceAnnotationReportTest
     SequenceFeature sf = new SequenceFeature("METAL", "Fe2-S", 1, 3, 1.3f,
             "group");
 
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
-    sar.appendFeature(sb, 1, minmax, sf);
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    sar.appendFeature(sb, 1, fr, sf);
     /*
      * map has no entry for this feature type - score is not shown:
      */
@@ -106,7 +110,7 @@ public class SequenceAnnotationReportTest
      * map has entry for this feature type - score is shown:
      */
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     // <br> is appended to a buffer > 6 in length
     assertEquals("METAL 1 3; Fe2-S<br>METAL 1 3; Fe2-S Score=1.3",
             sb.toString());
@@ -116,7 +120,7 @@ public class SequenceAnnotationReportTest
      */
     minmax.put("METAL", new float[][] { { 2f, 2f }, null });
     sb.setLength(0);
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
@@ -132,8 +136,11 @@ public class SequenceAnnotationReportTest
     assertEquals("METAL 1 3; Fe2-S", sb.toString());
   }
 
+  /**
+   * A specific attribute value is included if it is used to colour the feature
+   */
   @Test(groups = "Functional")
-  public void testAppendFeature_clinicalSignificance()
+  public void testAppendFeature_colouredByAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -141,12 +148,35 @@ public class SequenceAnnotationReportTest
             Float.NaN, "group");
     sf.setValue("clinical_significance", "Benign");
 
-    sar.appendFeature(sb, 1, null, sf);
-    assertEquals("METAL 1 3; Fe2-S; Benign", sb.toString());
+    /*
+     * first with no colour by attribute
+     */
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString());
+
+    /*
+     * then with colour by an attribute the feature lacks
+     */
+    FeatureColourI fc = new FeatureColour(Color.white, Color.black, 5, 10);
+    fc.setAttributeName("Pfam");
+    fr.setColour("METAL", fc);
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S", sb.toString()); // no change
+
+    /*
+     * then with colour by an attribute the feature has
+     */
+    fc.setAttributeName("clinical_significance");
+    sb.setLength(0);
+    sar.appendFeature(sb, 1, fr, sf);
+    assertEquals("METAL 1 3; Fe2-S; clinical_significance=Benign",
+            sb.toString());
   }
 
   @Test(groups = "Functional")
-  public void testAppendFeature_withScoreStatusClinicalSignificance()
+  public void testAppendFeature_withScoreStatusAttribute()
   {
     SequenceAnnotationReport sar = new SequenceAnnotationReport(null);
     StringBuilder sb = new StringBuilder();
@@ -154,11 +184,17 @@ public class SequenceAnnotationReportTest
             "group");
     sf.setStatus("Confirmed");
     sf.setValue("clinical_significance", "Benign");
-    Map<String, float[][]> minmax = new Hashtable<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
+    FeatureColourI fc = new FeatureColour(Color.white, Color.blue, 12, 22);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("METAL", fc);
     minmax.put("METAL", new float[][] { { 0f, 1f }, null });
-    sar.appendFeature(sb, 1, minmax, sf);
+    sar.appendFeature(sb, 1, fr, sf);
 
-    assertEquals("METAL 1 3; Fe2-S Score=1.3; (Confirmed); Benign",
+    assertEquals(
+            "METAL 1 3; Fe2-S Score=1.3; (Confirmed); clinical_significance=Benign",
             sb.toString());
   }
 
@@ -226,7 +262,7 @@ public class SequenceAnnotationReportTest
             null));
     sb.setLength(0);
     sar.createSequenceAnnotationReport(sb, seq, true, true, null);
-    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos</i>";
+    String expected = "<i><br>SeqDesc<br>Type1 ; Nonpos Score=1.0</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -244,10 +280,13 @@ public class SequenceAnnotationReportTest
      */
     seq.addSequenceFeature(new SequenceFeature("Metal", "Desc", 0, 0, 5f,
             null));
-    Map<String, float[][]> minmax = new HashMap<String, float[][]>();
+
+    FeatureRendererModel fr = new FeatureRenderer(null);
+    Map<String, float[][]> minmax = fr.getMinMax();
     minmax.put("Metal", new float[][] { null, new float[] { 2f, 5f } });
+
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos</i>";
     assertEquals(expected, sb.toString());
     
@@ -260,19 +299,20 @@ public class SequenceAnnotationReportTest
     sf.setValue("linkonly", Boolean.TRUE);
     seq.addSequenceFeature(sf);
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
     assertEquals(expected, sb.toString()); // unchanged!
 
     /*
-     * 'clinical_significance' currently being specially included
+     * 'clinical_significance' attribute only included when
+     * used for feature colouring
      */
     SequenceFeature sf2 = new SequenceFeature("Variant", "Havana", 0, 0,
             5f, null);
     sf2.setValue(GffConstants.CLINICAL_SIGNIFICANCE, "benign");
     seq.addSequenceFeature(sf2);
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
-    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana</i>";
     assertEquals(expected, sb.toString());
 
     /*
@@ -280,18 +320,24 @@ public class SequenceAnnotationReportTest
      */
     seq.addDBRef(new DBRefEntry("PDB", "0", "3iu1"));
     seq.addDBRef(new DBRefEntry("Uniprot", "1", "P30419"));
+
     // with showDbRefs = false
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, false, true, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, false, true, fr);
     assertEquals(expected, sb.toString()); // unchanged
-    // with showDbRefs = true
+
+    // with showDbRefs = true, colour Variant features by clinical_significance
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, true, minmax);
-    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>Type1 ; Nonpos<br>Variant ; Havana; benign</i>";
+    FeatureColourI fc = new FeatureColour(Color.green, Color.pink, 2, 3);
+    fc.setAttributeName("clinical_significance");
+    fr.setColour("Variant", fc);
+    sar.createSequenceAnnotationReport(sb, seq, true, true, fr);
+    expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1<br>Metal ; Desc<br>"
+            + "Type1 ; Nonpos<br>Variant ; Havana; clinical_significance=benign</i>";
     assertEquals(expected, sb.toString());
     // with showNonPositionalFeatures = false
     sb.setLength(0);
-    sar.createSequenceAnnotationReport(sb, seq, true, false, minmax);
+    sar.createSequenceAnnotationReport(sb, seq, true, false, fr);
     expected = "<i><br>SeqDesc<br>UNIPROT P30419<br>PDB 3iu1</i>";
     assertEquals(expected, sb.toString());
 
diff --git a/test/jalview/io/gff/SequenceOntologyLiteTest.java b/test/jalview/io/gff/SequenceOntologyLiteTest.java
new file mode 100644 (file)
index 0000000..0766666
--- /dev/null
@@ -0,0 +1,37 @@
+package jalview.io.gff;
+
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class SequenceOntologyLiteTest
+{
+  @Test(groups = "Functional")
+  public void testIsA_sequenceVariant()
+  {
+    SequenceOntologyI so = new SequenceOntologyLite();
+
+    assertFalse(so.isA("CDS", "sequence_variant"));
+    assertTrue(so.isA("sequence_variant", "sequence_variant"));
+
+    /*
+     * these should all be sub-types of sequence_variant
+     */
+    assertTrue(so.isA("structural_variant", "sequence_variant"));
+    assertTrue(so.isA("feature_variant", "sequence_variant"));
+    assertTrue(so.isA("gene_variant", "sequence_variant"));
+    assertTrue(so.isA("transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("NMD_transcript_variant", "sequence_variant"));
+    assertTrue(so.isA("missense_variant", "sequence_variant"));
+    assertTrue(so.isA("synonymous_variant", "sequence_variant"));
+    assertTrue(so.isA("frameshift_variant", "sequence_variant"));
+    assertTrue(so.isA("5_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("3_prime_UTR_variant", "sequence_variant"));
+    assertTrue(so.isA("stop_gained", "sequence_variant"));
+    assertTrue(so.isA("stop_lost", "sequence_variant"));
+    assertTrue(so.isA("inframe_deletion", "sequence_variant"));
+    assertTrue(so.isA("inframe_insertion", "sequence_variant"));
+    assertTrue(so.isA("splice_region_variant", "sequence_variant"));
+  }
+}
diff --git a/test/jalview/io/vcf/VCFLoaderTest.java b/test/jalview/io/vcf/VCFLoaderTest.java
new file mode 100644 (file)
index 0000000..7e3c0b4
--- /dev/null
@@ -0,0 +1,681 @@
+package jalview.io.vcf;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.SequenceFeatures;
+import jalview.gui.AlignFrame;
+import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.gff.Gff3Helper;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.MapList;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class VCFLoaderTest
+{
+  private static final float DELTA = 0.00001f;
+
+  // columns 9717- of gene P30419 from Ensembl (much modified)
+  private static final String FASTA = ""
+          +
+          /*
+           * forward strand 'gene' and 'transcript' with two exons
+           */
+          ">gene1/1-25 chromosome:GRCh38:17:45051610:45051634:1\n"
+          + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+          + ">transcript1/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+
+          /*
+           * reverse strand gene and transcript (reverse complement alleles!)
+           */
+          + ">gene2/1-25 chromosome:GRCh38:17:45051610:45051634:-1\n"
+          + "TGTCACACTCTCGTCCGCCAGCTTG\n"
+          + ">transcript2/1-18\n" + "-GTCACACTCT----CGCCAGCT--\n"
+
+          /*
+           * 'gene' on chromosome 5 with two transcripts
+           */
+          + ">gene3/1-25 chromosome:GRCh38:5:45051610:45051634:1\n"
+          + "CAAGCTGGCGGACGAGAGTGTGACA\n"
+          + ">transcript3/1-18\n--AGCTGGCG----AGAGTGTGAC-\n"
+          + ">transcript4/1-18\n-----TGG-GGACGAGAGTGTGA-A\n";
+
+  private static final String[] VCF = { "##fileformat=VCFv4.2",
+      "##INFO=<ID=AF,Number=A,Type=Float,Description=\"Allele Frequency, for each ALT allele, in the same order as listed\">",
+      "##reference=Homo_sapiens/GRCh38",
+      "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO",
+      // A/T,C variants in position 2 of gene sequence (precedes transcript)
+      // should create 2 variant features with respective scores
+      "17\t45051611\t.\tA\tT,C\t1666.64\tRF\tAC=15;AF=5.0e-03,4.0e-03",
+      // SNP G/C in position 4 of gene sequence, position 2 of transcript
+      // insertion G/GA is transferred to nucleotide but not to peptide
+      "17\t45051613\t.\tG\tGA,C\t1666.64\tRF\tAC=15;AF=3.0e-03,2.0e-03" };
+
+  @BeforeClass
+  public void setUp()
+  {
+    /*
+     * configure to capture all available VCF and VEP (CSQ) fields
+     */
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+    Cache.setProperty("VCF_FIELDS", ".*");
+    Cache.setProperty("VEP_FIELDS", ".*");
+    Cache.initLogger();
+  }
+
+  @Test(groups = "Functional")
+  public void testDoLoad() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    File f = makeVcf();
+    VCFLoader loader = new VCFLoader(f.getPath());
+
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene
+     * NB alleles at a locus may not be processed, and features added,
+     * in the order in which they appear in the VCF record as method
+     * VariantContext.getAlternateAlleles() does not guarantee order
+     * - order of assertions here matches what we find (is not important) 
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(0)
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 4);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getScore(), 4.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,C");
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "A,T");
+
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+
+    sf = geneFeatures.get(3);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+    /*
+     * verify variant feature(s) added to transcript
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(1)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 2);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,C");
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GA");
+
+    /*
+     * verify SNP variant feature(s) computed and added to protein
+     * first codon AGC varies to ACC giving S/T
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(1).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ser1Thr");
+  }
+
+  private File makeVcf() throws IOException
+  {
+    File f = File.createTempFile("Test", ".vcf");
+    f.deleteOnExit();
+    PrintWriter pw = new PrintWriter(f);
+    for (String vcfLine : VCF)
+    {
+      pw.println(vcfLine);
+    }
+    pw.close();
+    return f;
+  }
+
+  /**
+   * Make a simple alignment with one 'gene' and one 'transcript'
+   * 
+   * @return
+   */
+  private AlignmentI buildAlignment()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(FASTA,
+            DataSourceType.PASTE);
+
+    /*
+     * map gene1 sequence to chromosome (normally done when the sequence is fetched
+     * from Ensembl and transcripts computed)
+     */
+    AlignmentI alignment = af.getViewport().getAlignment();
+    SequenceI gene1 = alignment.findName("gene1");
+    int[] to = new int[] { 45051610, 45051634 };
+    int[] from = new int[] { gene1.getStart(), gene1.getEnd() };
+    gene1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript1' to chromosome via 'gene1'
+     * transcript1/1-18 is gene1/3-10,15-24
+     * which is chromosome 45051612-45051619,45051624-45051633
+     */
+    to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+    SequenceI transcript1 = alignment.findName("transcript1");
+    from = new int[] { transcript1.getStart(), transcript1.getEnd() };
+    transcript1.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * map gene2 to chromosome reverse strand
+     */
+    SequenceI gene2 = alignment.findName("gene2");
+    to = new int[] { 45051634, 45051610 };
+    from = new int[] { gene2.getStart(), gene2.getEnd() };
+    gene2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript2' to chromosome via 'gene2'
+     * transcript2/1-18 is gene2/2-11,16-23
+     * which is chromosome 45051633-45051624,45051619-45051612
+     */
+    to = new int[] { 45051633, 45051624, 45051619, 45051612 };
+    SequenceI transcript2 = alignment.findName("transcript2");
+    from = new int[] { transcript2.getStart(), transcript2.getEnd() };
+    transcript2.setGeneLoci("homo_sapiens", "GRCh38", "17", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * add a protein product as a DBRef on transcript1
+     */
+    SequenceI peptide1 = new Sequence("ENSP001", "SWRECD");
+    MapList mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 },
+            3, 1);
+    Mapping map = new Mapping(peptide1, mapList);
+    DBRefEntry product = new DBRefEntry("", "", "ENSP001", map);
+    transcript1.addDBRef(product);
+
+    /*
+     * add a protein product as a DBRef on transcript2
+     */
+    SequenceI peptide2 = new Sequence("ENSP002", "VTLSPA");
+    mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+    map = new Mapping(peptide2, mapList);
+    product = new DBRefEntry("", "", "ENSP002", map);
+    transcript2.addDBRef(product);
+
+    /*
+     * map gene3 to chromosome 
+     */
+    SequenceI gene3 = alignment.findName("gene3");
+    to = new int[] { 45051610, 45051634 };
+    from = new int[] { gene3.getStart(), gene3.getEnd() };
+    gene3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(from, to,
+            1, 1));
+
+    /*
+     * map 'transcript3' to chromosome
+     */
+    SequenceI transcript3 = alignment.findName("transcript3");
+    to = new int[] { 45051612, 45051619, 45051624, 45051633 };
+    from = new int[] { transcript3.getStart(), transcript3.getEnd() };
+    transcript3.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * map 'transcript4' to chromosome
+     */
+    SequenceI transcript4 = alignment.findName("transcript4");
+    to = new int[] { 45051615, 45051617, 45051619, 45051632, 45051634,
+        45051634 };
+    from = new int[] { transcript4.getStart(), transcript4.getEnd() };
+    transcript4.setGeneLoci("homo_sapiens", "GRCh38", "5", new MapList(
+            from, to,
+            1, 1));
+
+    /*
+     * add a protein product as a DBRef on transcript3
+     */
+    SequenceI peptide3 = new Sequence("ENSP003", "SWRECD");
+    mapList = new MapList(new int[] { 1, 18 }, new int[] { 1, 6 }, 3, 1);
+    map = new Mapping(peptide3, mapList);
+    product = new DBRefEntry("", "", "ENSP003", map);
+    transcript3.addDBRef(product);
+
+    return alignment;
+  }
+
+  /**
+   * Test with 'gene' and 'transcript' mapped to the reverse strand of the
+   * chromosome. The VCF variant positions (in forward coordinates) should get
+   * correctly located on sequence positions.
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testDoLoad_reverseStrand() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    File f = makeVcf();
+
+    VCFLoader loader = new VCFLoader(f.getPath());
+
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene2
+     * gene2/1-25 maps to chromosome 45051634- reverse strand
+     */
+    List<SequenceFeature> geneFeatures = al.getSequenceAt(2)
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 4);
+
+    /*
+     * variant A/T at 45051611 maps to T/A at gene position 24
+     */
+    SequenceFeature sf = geneFeatures.get(3);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 5.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,A");
+
+    /*
+     * variant A/C at 45051611 maps to T/G at gene position 24
+     */
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 24);
+    assertEquals(sf.getEnd(), 24);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 4.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "T,G");
+
+    /*
+     * variant G/C at 45051613 maps to C/G at gene position 22
+     */
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 22);
+    assertEquals(sf.getEnd(), 22);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
+
+    /*
+     * insertion G/GA at 45051613 maps to an insertion at
+     * the preceding position (21) on reverse strand gene
+     * reference: CAAGC -> GCTTG/21-25
+     * genomic variant: CAAGAC (G/GA)
+     * gene variant: GTCTTG (G/GT at 21)
+     */
+    sf = geneFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 21);
+    assertEquals(sf.getEnd(), 21);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
+
+    /*
+     * verify 2 variant features added to transcript2
+     */
+    List<SequenceFeature> transcriptFeatures = al.getSequenceAt(3)
+            .getSequenceFeatures();
+    assertEquals(transcriptFeatures.size(), 2);
+
+    /*
+     * insertion G/GT at position 21 of gene maps to position 16 of transcript
+     */
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 16);
+    assertEquals(sf.getEnd(), 16);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 3.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "G,GT");
+
+    /*
+     * SNP C/G at position 22 of gene maps to position 17 of transcript
+     */
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17);
+    assertEquals(sf.getType(), SequenceOntologyI.SEQUENCE_VARIANT);
+    assertEquals(sf.getScore(), 2.0e-03, DELTA);
+    assertEquals(sf.getValue(Gff3Helper.ALLELES), "C,G");
+
+    /*
+     * verify variant feature(s) computed and added to protein
+     * last codon GCT varies to GGT giving A/G in the last peptide position
+     */
+    DBRefEntry[] dbRefs = al.getSequenceAt(3).getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    assertEquals(proteinFeatures.size(), 1);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 6);
+    assertEquals(sf.getEnd(), 6);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Ala6Gly");
+  }
+
+  /**
+   * Tests that if VEP consequence (CSQ) data is present in the VCF data, then
+   * it is added to the variant feature, but restricted where possible to the
+   * consequences for a specific transcript
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testDoLoad_vepCsq() throws IOException
+  {
+    AlignmentI al = buildAlignment();
+
+    VCFLoader loader = new VCFLoader("test/jalview/io/vcf/testVcf.vcf");
+
+    /*
+     * VCF data file with variants at gene3 positions
+     * 1 C/A
+     * 5 C/T
+     * 9 CGT/C (deletion)
+     * 13 C/G, C/T
+     * 17 A/AC (insertion), A/G
+     */
+    loader.doLoad(al.getSequencesArray(), null);
+
+    /*
+     * verify variant feature(s) added to gene3
+     */
+    List<SequenceFeature> geneFeatures = al.findName("gene3")
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(geneFeatures, true);
+    assertEquals(geneFeatures.size(), 7);
+    SequenceFeature sf = geneFeatures.get(0);
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getScore(), 0.1f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,A");
+    // gene features include Consequence for all transcripts
+    Map map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(1);
+    assertEquals(sf.getBegin(), 5);
+    assertEquals(sf.getEnd(), 5);
+    assertEquals(sf.getScore(), 0.2f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(2);
+    assertEquals(sf.getBegin(), 9);
+    assertEquals(sf.getEnd(), 11); // deletion over 3 positions
+    assertEquals(sf.getScore(), 0.3f, DELTA);
+    assertEquals(sf.getValue("alleles"), "CGG,C");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(3);
+    assertEquals(sf.getBegin(), 13);
+    assertEquals(sf.getEnd(), 13);
+    assertEquals(sf.getScore(), 0.5f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(4);
+    assertEquals(sf.getBegin(), 13);
+    assertEquals(sf.getEnd(), 13);
+    assertEquals(sf.getScore(), 0.4f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,G");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(5);
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    sf = geneFeatures.get(6);
+    assertEquals(sf.getBegin(), 17);
+    assertEquals(sf.getEnd(), 17); // insertion
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+
+    /*
+     * verify variant feature(s) added to transcript3
+     * at columns 5 (1), 17 (2), positions 3, 11
+     * note the deletion at columns 9-11 is not transferred since col 11
+     * has no mapping to transcript 3 
+     */
+    List<SequenceFeature> transcriptFeatures = al.findName("transcript3")
+            .getSequenceFeatures();
+    SequenceFeatures.sortFeatures(transcriptFeatures, true);
+    assertEquals(transcriptFeatures.size(), 3);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getBegin(), 3);
+    assertEquals(sf.getEnd(), 3);
+    assertEquals(sf.getScore(), 0.2f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    // transcript features only have Consequence for that transcripts
+    map = (Map) sf.getValue("CSQ");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    sf = transcriptFeatures.get(2);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript3");
+
+    /*
+     * verify variants computed on protein product for transcript3
+     * peptide is SWRECD
+     * codon variants are AGC/AGT position 1 which is synonymous
+     * and GAG/GGG which is E/G in position 4
+     * the insertion variant is not transferred to the peptide
+     */
+    DBRefEntry[] dbRefs = al.findName("transcript3").getDBRefs();
+    SequenceI peptide = null;
+    for (DBRefEntry dbref : dbRefs)
+    {
+      if (dbref.getMap().getMap().getFromRatio() == 3)
+      {
+        peptide = dbref.getMap().getTo();
+      }
+    }
+    List<SequenceFeature> proteinFeatures = peptide.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(proteinFeatures, true);
+    assertEquals(proteinFeatures.size(), 2);
+    sf = proteinFeatures.get(0);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 1);
+    assertEquals(sf.getEnd(), 1);
+    assertEquals(sf.getType(), SequenceOntologyI.SYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "agC/agT");
+    sf = proteinFeatures.get(1);
+    assertEquals(sf.getFeatureGroup(), "VCF");
+    assertEquals(sf.getBegin(), 4);
+    assertEquals(sf.getEnd(), 4);
+    assertEquals(sf.getType(), SequenceOntologyI.NONSYNONYMOUS_VARIANT);
+    assertEquals(sf.getDescription(), "p.Glu4Gly");
+
+    /*
+     * verify variant feature(s) added to transcript4
+     * at columns 13 (2) and 17 (2), positions 7 and 11
+     */
+    transcriptFeatures = al.findName("transcript4").getSequenceFeatures();
+    SequenceFeatures.sortFeatures(transcriptFeatures, true);
+    assertEquals(transcriptFeatures.size(), 4);
+    sf = transcriptFeatures.get(0);
+    assertEquals(sf.getBegin(), 7);
+    assertEquals(sf.getEnd(), 7);
+    assertEquals(sf.getScore(), 0.5f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,T");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(1);
+    assertEquals(sf.getBegin(), 7);
+    assertEquals(sf.getEnd(), 7);
+    assertEquals(sf.getScore(), 0.4f, DELTA);
+    assertEquals(sf.getValue("alleles"), "C,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(2);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.7f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,G");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+
+    sf = transcriptFeatures.get(3);
+    assertEquals(sf.getBegin(), 11);
+    assertEquals(sf.getEnd(), 11);
+    assertEquals(sf.getScore(), 0.6f, DELTA);
+    assertEquals(sf.getValue("alleles"), "A,AC");
+    assertEquals(map.size(), 9);
+    assertEquals(sf.getValueAsString("CSQ", "Feature"), "transcript4");
+  }
+
+  /**
+   * A test that demonstrates loading a contig sequence from an indexed sequence
+   * database which is the reference for a VCF file
+   * 
+   * @throws IOException
+   */
+  @Test(groups = "Functional")
+  public void testLoadVCFContig() throws IOException
+  {
+    VCFLoader loader = new VCFLoader(
+            "test/jalview/io/vcf/testVcf2.vcf");
+
+    SequenceI seq = loader.loadVCFContig("contig123");
+    assertEquals(seq.getLength(), 15);
+    assertEquals(seq.getSequenceAsString(), "AAAAACCCCCGGGGG");
+    List<SequenceFeature> features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 2);
+    SequenceFeature sf = features.get(0);
+    assertEquals(sf.getBegin(), 8);
+    assertEquals(sf.getEnd(), 8);
+    assertEquals(sf.getDescription(), "C,A");
+    sf = features.get(1);
+    assertEquals(sf.getBegin(), 12);
+    assertEquals(sf.getEnd(), 12);
+    assertEquals(sf.getDescription(), "G,T");
+
+    seq = loader.loadVCFContig("contig789");
+    assertEquals(seq.getLength(), 25);
+    assertEquals(seq.getSequenceAsString(), "GGGGGTTTTTAAAAACCCCCGGGGG");
+    features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 2);
+    sf = features.get(0);
+    assertEquals(sf.getBegin(), 2);
+    assertEquals(sf.getEnd(), 2);
+    assertEquals(sf.getDescription(), "G,T");
+    sf = features.get(1);
+    assertEquals(sf.getBegin(), 21);
+    assertEquals(sf.getEnd(), 21);
+    assertEquals(sf.getDescription(), "G,A");
+
+    seq = loader.loadVCFContig("contig456");
+    assertEquals(seq.getLength(), 20);
+    assertEquals(seq.getSequenceAsString(), "CCCCCGGGGGTTTTTAAAAA");
+    features = seq.getSequenceFeatures();
+    SequenceFeatures.sortFeatures(features, true);
+    assertEquals(features.size(), 1);
+    sf = features.get(0);
+    assertEquals(sf.getBegin(), 15);
+    assertEquals(sf.getEnd(), 15);
+    assertEquals(sf.getDescription(), "T,C");
+  }
+}
\ No newline at end of file
diff --git a/test/jalview/io/vcf/contigs.fasta b/test/jalview/io/vcf/contigs.fasta
new file mode 100644 (file)
index 0000000..ec839b6
--- /dev/null
@@ -0,0 +1,6 @@
+>contig123
+AAAAACCCCCGGGGG
+>contig456
+CCCCCGGGGGTTTTTAAAAA
+>contig789
+GGGGGTTTTTAAAAACCCCCGGGGG
diff --git a/test/jalview/io/vcf/contigs.fasta.fai b/test/jalview/io/vcf/contigs.fasta.fai
new file mode 100644 (file)
index 0000000..e9f5067
--- /dev/null
@@ -0,0 +1,3 @@
+contig123      15      11      15      16
+contig456      20      38      20      21
+contig789      25      70      25      26
diff --git a/test/jalview/io/vcf/testVcf.dat b/test/jalview/io/vcf/testVcf.dat
new file mode 100644 (file)
index 0000000..77e070c
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
+##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
+##reference=/Homo_sapiens/GRCh38
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
diff --git a/test/jalview/io/vcf/testVcf.vcf b/test/jalview/io/vcf/testVcf.vcf
new file mode 100644 (file)
index 0000000..77e070c
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##INFO=<ID=AF_Female,Number=R,Type=Float,Description="Allele Frequency among Female genotypes, for each ALT allele, in the same order as listed">
+##INFO=<ID=AN,Number=1,Type=Integer,Description="Total number of alleles in called genotypes">
+##INFO=<ID=CSQ,Number=.,Type=String,Description="Consequence annotations from Ensembl VEP. Format: Allele|Consequence|IMPACT|SYMBOL|Gene|Feature_type|Feature|BIOTYPE|PolyPhen">
+##reference=/Homo_sapiens/GRCh38
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+5      45051610        .       C       A       81.96   RF;AC0  AC=1;AF=0.1;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=A|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,A|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051614        .       C       T       1666.64 RF      AC=1;AF=0.2;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051618        .       CGG     C       41.94   AC0     AC=1;AF=0.3;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=C|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,C|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,CSQ=CGT|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,CGT|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051622        .       C       G,T     224.23  RF;AC0  AC=1,2;AF=0.4,0.5;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,T|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,T|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
+5      45051626        .       A       AC,G    433.35  RF;AC0  AC=3,4;AF=0.6,0.7;AN=0;AF_Female=2;AB_MEDIAN=6.00000e-01;CSQ=G|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,G|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad,AC|missense_variant|MODIFIER|WASH7P|gene3|Transcript|transcript3|rna|Benign,AC|downstream_gene_variant|MODIFIER|WASH7P|gene3|Transcript|transcript4|mrna|Bad
diff --git a/test/jalview/io/vcf/testVcf2.vcf b/test/jalview/io/vcf/testVcf2.vcf
new file mode 100644 (file)
index 0000000..aa3792a
--- /dev/null
@@ -0,0 +1,13 @@
+##fileformat=VCFv4.2
+##INFO=<ID=AC,Number=A,Type=Integer,Description="Allele count in genotypes, for each ALT allele, in the same order as listed">
+##contig=<ID=contig123,length=15>
+##contig=<ID=contig456,length=20>
+##contig=<ID=contig789,length=25>
+##INFO=<ID=AF,Number=A,Type=Float,Description="Allele Frequency, for each ALT allele, in the same order as listed">
+##reference=test/jalview/io/vcf/contigs.fasta
+#CHROM POS     ID      REF     ALT     QUAL    FILTER  INFO
+contig123      8       .       C       A       81.96   .       AC=1;AF=0.1
+contig123      12      .       G       T       1666.64 .       AC=1;AF=0.2
+contig456      15      .       T       C       41.94   .       AC=1;AF=0.3
+contig789      2       .       G       T       224.23  .       AC=1,2;AF=0
+contig789      21      .       G       A       433.35  .       AC=3;AF=0.6
diff --git a/test/jalview/renderer/OverviewRendererTest.java b/test/jalview/renderer/OverviewRendererTest.java
new file mode 100644 (file)
index 0000000..1d532f7
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * 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.renderer;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceFeature;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignViewport;
+import jalview.renderer.seqfeatures.FeatureRenderer;
+import jalview.schemes.FeatureColour;
+import jalview.schemes.ZappoColourScheme;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.OverviewDimensions;
+import jalview.viewmodel.OverviewDimensionsShowHidden;
+import jalview.viewmodel.ViewportRanges;
+
+import java.awt.Color;
+
+import org.testng.annotations.Test;
+public class OverviewRendererTest
+{
+
+  @Test
+  public void testGetColumnColourFromSequence()
+  {
+    OverviewResColourFinder cf = new OverviewResColourFinder(false,
+            Color.PINK, Color.green); // gapColour, hiddenColour
+    Sequence seq1 = new Sequence("seq1", "PQ-RL-");
+    Sequence seq2 = new Sequence("seq2", "FVE");
+    AlignmentI al = new Alignment(new SequenceI[] { seq1, seq2 });
+    AlignmentViewport av = new AlignViewport(al);
+    OverviewDimensions od = new OverviewDimensionsShowHidden(new ViewportRanges(al), false);
+    ResidueShaderI rs = new ResidueShader(new ZappoColourScheme());
+    FeatureRenderer fr = new FeatureRenderer(av);
+    OverviewRenderer or = new OverviewRenderer(fr, od, al, rs, cf);
+
+    // P is magenta (see ResidueProperties.zappo)
+    assertEquals(or.getColumnColourFromSequence(null, seq1, 0), Color.magenta.getRGB());
+    // Q is green
+    assertEquals(or.getColumnColourFromSequence(null, seq1, 1),
+            Color.green.getRGB());
+    // gap is pink (specified in OverviewResColourFinder constructor above)
+    assertEquals(or.getColumnColourFromSequence(null, seq1, 2),
+            Color.pink.getRGB());
+    // F is orange
+    assertEquals(or.getColumnColourFromSequence(null, seq2, 0),
+            Color.orange.getRGB());
+    // E is red
+    assertEquals(or.getColumnColourFromSequence(null, seq2, 2),
+            Color.red.getRGB());
+    // past end of sequence colour as gap (JAL-2929)
+    assertEquals(or.getColumnColourFromSequence(null, seq2, 3),
+            Color.pink.getRGB());
+
+    /*
+     * now add a feature on seq1
+     */
+    seq1.addSequenceFeature(
+            new SequenceFeature("Pfam", "desc", 1, 4, null));
+    fr.findAllFeatures(true);
+    av.setShowSequenceFeatures(true);
+    fr.setColour("Pfam", new FeatureColour(Color.yellow));
+    assertEquals(or.getColumnColourFromSequence(null, seq1, 0),
+            Color.yellow.getRGB());
+
+    // don't show sequence features
+    av.setShowSequenceFeatures(false);
+    assertEquals(or.getColumnColourFromSequence(null, seq1, 0),
+            Color.magenta.getRGB());
+  }
+}
index cf1039f..0af67cd 100644 (file)
@@ -26,11 +26,11 @@ public class ScaleRendererTest
     AlignViewport av = af.getViewport();
 
     /*
-     * scale has minor ticks at 5 and 15, major at 10 and 20
+     * scale has minor ticks at 5, 15, 25, major at 10 and 20
      * (these are base 1, ScaleMark holds base 0 values)
      */
     List<ScaleMark> marks = new ScaleRenderer().calculateMarks(av, 0, 25);
-    assertEquals(marks.size(), 4);
+    assertEquals(marks.size(), 5);
 
     assertFalse(marks.get(0).major);
     assertEquals(marks.get(0).column, 4);
@@ -48,6 +48,10 @@ public class ScaleRendererTest
     assertEquals(marks.get(3).column, 19);
     assertEquals(marks.get(3).text, "20");
 
+    assertFalse(marks.get(4).major);
+    assertEquals(marks.get(4).column, 24);
+    assertNull(marks.get(4).text);
+
     /*
      * now hide columns 9-11 and 18-20 (base 1)
      * scale marks are now in the same columns as before, but
@@ -56,7 +60,7 @@ public class ScaleRendererTest
     av.hideColumns(8, 10);
     av.hideColumns(17, 19);
     marks = new ScaleRenderer().calculateMarks(av, 0, 25);
-    assertEquals(marks.size(), 4);
+    assertEquals(marks.size(), 5);
     assertFalse(marks.get(0).major);
     assertEquals(marks.get(0).column, 4);
     assertNull(marks.get(0).text);
@@ -69,5 +73,8 @@ public class ScaleRendererTest
     assertTrue(marks.get(3).major);
     assertEquals(marks.get(3).column, 19);
     assertEquals(marks.get(3).text, "26"); // +6 hidden columns
+    assertFalse(marks.get(4).major);
+    assertEquals(marks.get(4).column, 24);
+    assertNull(marks.get(4).text);
   }
 }
index 7fd7abc..d8b905e 100644 (file)
@@ -2,6 +2,7 @@ package jalview.renderer.seqfeatures;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
 import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
@@ -14,6 +15,7 @@ import jalview.gui.FeatureRenderer;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
 import java.util.List;
@@ -171,9 +173,9 @@ public class FeatureColourFinderTest
      * - currently no way other than mimicking reordering of
      * table in Feature Settings
      */
-    Object[][] data = new Object[2][];
-    data[0] = new Object[] { "Metal", red, true };
-    data[1] = new Object[] { "Domain", green, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
+    data[1] = new FeatureSettingsBean("Domain", green, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.red);
@@ -181,7 +183,7 @@ public class FeatureColourFinderTest
     /*
      * ..and turn off display of Metal
      */
-    data[0][2] = false;
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.green);
@@ -215,8 +217,8 @@ public class FeatureColourFinderTest
     /*
      * turn off display of Metal - is this the easiest way to do it??
      */
-    Object[][] data = new Object[1][];
-    data[0] = new Object[] { "Metal", red, false };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.blue);
@@ -224,7 +226,7 @@ public class FeatureColourFinderTest
     /*
      * turn display of Metal back on
      */
-    data[0] = new Object[] { "Metal", red, true };
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.blue, seq, 10);
     assertEquals(c, Color.red);
@@ -285,6 +287,28 @@ public class FeatureColourFinderTest
   }
 
   @Test(groups = "Functional")
+  public void testFindFeatureAtEnd()
+  {
+    /*
+     * terminal residue feature
+     */
+    seq.addSequenceFeature(new SequenceFeature("PDBRESNUM", "pdb res 1",
+            seq.getEnd(), seq.getEnd(), Float.NaN, "1seq.pdb"));
+    fr.setColour("PDBRESNUM", new FeatureColour(Color.red));
+    fr.featuresAdded();
+    av.setShowSequenceFeatures(true);
+
+    /*
+     * final column should have PDBRESNUM feature, the others not
+     */
+    Color c = finder.findFeatureColour(Color.blue, seq,
+            seq.getLength() - 2);
+    assertNotEquals(c, Color.red);
+    c = finder.findFeatureColour(Color.blue, seq, seq.getLength() - 1);
+    assertEquals(c, Color.red);
+  }
+
+  @Test(groups = "Functional")
   public void testFindFeatureColour_graduatedFeatureColour()
   {
     seq.addSequenceFeature(new SequenceFeature("kd", "hydrophobicity", 2,
@@ -376,9 +400,9 @@ public class FeatureColourFinderTest
      * 1) 0.6 * green(0, 255, 0) + 0.4 * cyan(0, 255, 255) = (0, 255, 102)
      * 2) 0.6* red(255, 0, 0) + 0.4 * (0, 255, 102) = (153, 102, 41) rounded
      */
-    Object[][] data = new Object[2][];
-    data[0] = new Object[] { "Metal", red, true };
-    data[1] = new Object[] { "Domain", green, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean("Metal", red, null, true);
+    data[1] = new FeatureSettingsBean("Domain", green, null, true);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.cyan, seq, 10);
     assertEquals(c, new Color(153, 102, 41));
@@ -388,7 +412,7 @@ public class FeatureColourFinderTest
      * Domain (green) above background (pink)
      * 0.6 * green(0, 255, 0) + 0.4 * pink(255, 175, 175) = (102, 223, 70)
      */
-    data[0][2] = false;
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     c = finder.findFeatureColour(Color.pink, seq, 10);
     assertEquals(c, new Color(102, 223, 70));
@@ -424,8 +448,8 @@ public class FeatureColourFinderTest
     /*
      * turn off display of Metal
      */
-    Object[][] data = new Object[1][];
-    data[0] = new Object[] { "Metal", red, false };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Metal", red, null, false);
     fr.setFeaturePriority(data);
     assertTrue(finder.noFeaturesDisplayed());
 
@@ -480,9 +504,9 @@ public class FeatureColourFinderTest
     /*
      * render order is kd above Metal
      */
-    Object[][] data = new Object[2][];
-    data[0] = new Object[] { kdFeature, fc, true };
-    data[1] = new Object[] { metalFeature, green, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[2];
+    data[0] = new FeatureSettingsBean(kdFeature, fc, null, true);
+    data[1] = new FeatureSettingsBean(metalFeature, green, null, true);
     fr.setFeaturePriority(data);
 
     av.setShowSequenceFeatures(true);
index d3cddf9..11b129e 100644 (file)
@@ -1,21 +1,48 @@
+/*
+ * 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.renderer.seqfeatures;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 import jalview.api.AlignViewportI;
 import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.datamodel.features.FeatureMatcher;
+import jalview.datamodel.features.FeatureMatcherSet;
+import jalview.datamodel.features.FeatureMatcherSetI;
 import jalview.gui.AlignFrame;
 import jalview.io.DataSourceType;
 import jalview.io.FileLoader;
 import jalview.schemes.FeatureColour;
+import jalview.util.matcher.Condition;
+import jalview.viewmodel.seqfeatures.FeatureRendererModel.FeatureSettingsBean;
 
 import java.awt.Color;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -61,9 +88,8 @@ public class FeatureRendererTest
     seqs.get(2).addSequenceFeature(
             new SequenceFeature("Pfam", "Desc", 14, 22, 2f, "RfamGroup"));
     // bug in findAllFeatures - group not checked for a known feature type
-    seqs.get(2).addSequenceFeature(
-            new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN,
-                    "RfamGroup"));
+    seqs.get(2).addSequenceFeature(new SequenceFeature("Rfam", "Desc", 5, 9,
+            Float.NaN, "RfamGroup"));
     // existing feature type with null group
     seqs.get(3).addSequenceFeature(
             new SequenceFeature("Rfam", "Desc", 5, 9, Float.NaN, null));
@@ -116,13 +142,14 @@ public class FeatureRendererTest
      * change render order (todo: an easier way)
      * nb here last comes first in the data array
      */
-    Object[][] data = new Object[3][];
+    FeatureSettingsBean[] data = new FeatureSettingsBean[3];
     FeatureColourI colour = new FeatureColour(Color.RED);
-    data[0] = new Object[] { "Rfam", colour, true };
-    data[1] = new Object[] { "Pfam", colour, false };
-    data[2] = new Object[] { "Scop", colour, false };
+    data[0] = new FeatureSettingsBean("Rfam", colour, null, true);
+    data[1] = new FeatureSettingsBean("Pfam", colour, null, false);
+    data[2] = new FeatureSettingsBean("Scop", colour, null, false);
     fr.setFeaturePriority(data);
-    assertEquals(fr.getRenderOrder(), Arrays.asList("Scop", "Pfam", "Rfam"));
+    assertEquals(fr.getRenderOrder(),
+            Arrays.asList("Scop", "Pfam", "Rfam"));
     assertEquals(fr.getDisplayedFeatureTypes(), Arrays.asList("Rfam"));
 
     /*
@@ -217,12 +244,13 @@ public class FeatureRendererTest
     /*
      * make "Type2" not displayed
      */
-    Object[][] data = new Object[4][];
     FeatureColourI colour = new FeatureColour(Color.RED);
-    data[0] = new Object[] { "Type1", colour, true };
-    data[1] = new Object[] { "Type2", colour, false };
-    data[2] = new Object[] { "Type3", colour, true };
-    data[3] = new Object[] { "Disulphide Bond", colour, true };
+    FeatureSettingsBean[] data = new FeatureSettingsBean[4];
+    data[0] = new FeatureSettingsBean("Type1", colour, null, true);
+    data[1] = new FeatureSettingsBean("Type2", colour, null, false);
+    data[2] = new FeatureSettingsBean("Type3", colour, null, true);
+    data[3] = new FeatureSettingsBean("Disulphide Bond", colour, null,
+            true);
     fr.setFeaturePriority(data);
 
     features = fr.findFeaturesAtColumn(seq, 15);
@@ -252,6 +280,37 @@ public class FeatureRendererTest
     features = fr.findFeaturesAtColumn(seq, 5);
     assertEquals(features.size(), 1);
     assertTrue(features.contains(sf8));
+
+    /*
+     * give "Type3" features a graduated colour scheme
+     * - first with no threshold
+     */
+    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red, null, 0f,
+            10f);
+    fr.getFeatureColours().put("Type3", gc);
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertTrue(features.contains(sf4));
+    // now with threshold > 2f - feature score of 1f is excluded
+    gc.setAboveThreshold(true);
+    gc.setThreshold(2f);
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
+
+    /*
+     * make "Type3" graduated colour by attribute "AF"
+     * - first with no attribute held - feature should be excluded
+     */
+    gc.setAttributeName("AF");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
+    // now with the attribute above threshold - should be included
+    sf4.setValue("AF", "2.4");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertTrue(features.contains(sf4));
+    // now with the attribute below threshold - should be excluded
+    sf4.setValue("AF", "1.4");
+    features = fr.findFeaturesAtColumn(seq, 8);
+    assertFalse(features.contains(sf4));
   }
 
   @Test(groups = "Functional")
@@ -264,7 +323,7 @@ public class FeatureRendererTest
     FeatureRenderer fr = new FeatureRenderer(av);
 
     List<SequenceFeature> features = new ArrayList<>();
-    fr.filterFeaturesForDisplay(features, null); // empty list, does nothing
+    fr.filterFeaturesForDisplay(features); // empty list, does nothing
 
     SequenceI seq = av.getAlignment().getSequenceAt(0);
     SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
@@ -297,7 +356,7 @@ public class FeatureRendererTest
      * filter out duplicate (co-located) features
      * note: which gets removed is not guaranteed
      */
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    fr.filterFeaturesForDisplay(features);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf1) || features.contains(sf4));
     assertFalse(features.contains(sf1) && features.contains(sf4));
@@ -306,58 +365,174 @@ public class FeatureRendererTest
     assertTrue(features.contains(sf5));
 
     /*
-     * hide group 3 - sf3 is removed, sf2 is retained
+     * hide groups 2 and 3 makes no difference to this method
      */
+    fr.setGroupVisibility("group2", false);
     fr.setGroupVisibility("group3", false);
     features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.blue));
+    fr.filterFeaturesForDisplay(features);
     assertEquals(features.size(), 3);
     assertTrue(features.contains(sf1) || features.contains(sf4));
     assertFalse(features.contains(sf1) && features.contains(sf4));
-    assertTrue(features.contains(sf2));
-    assertFalse(features.contains(sf3));
+    assertTrue(features.contains(sf2) || features.contains(sf3));
+    assertFalse(features.contains(sf2) && features.contains(sf3));
     assertTrue(features.contains(sf5));
 
     /*
-     * hide group 2, show group 3 - sf2 is removed, sf3 is retained
+     * no filtering if transparency is applied
      */
-    fr.setGroupVisibility("group2", false);
-    fr.setGroupVisibility("group3", true);
+    fr.setTransparency(0.5f);
     features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, null);
-    assertEquals(features.size(), 3);
-    assertTrue(features.contains(sf1) || features.contains(sf4));
-    assertFalse(features.contains(sf1) && features.contains(sf4));
-    assertFalse(features.contains(sf2));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf5));
+    fr.filterFeaturesForDisplay(features);
+    assertEquals(features.size(), 5);
+  }
+
+  @Test(groups = "Functional")
+  public void testGetColour()
+  {
+    AlignFrame af = new FileLoader().LoadFileWaitTillLoaded(">s1\nABCD\n",
+            DataSourceType.PASTE);
+    AlignViewportI av = af.getViewport();
+    FeatureRenderer fr = new FeatureRenderer(av);
 
     /*
-     * no filtering of co-located features with graduated colour scheme
-     * filterFeaturesForDisplay does _not_ check colour threshold
-     * sf2 is removed as its group is hidden
+     * simple colour, feature type and group displayed
      */
-    features = seq.getSequenceFeatures();
-    fr.filterFeaturesForDisplay(features, new FeatureColour(Color.black,
-            Color.white, 0f, 1f));
-    assertEquals(features.size(), 4);
-    assertTrue(features.contains(sf1));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf4));
-    assertTrue(features.contains(sf5));
+    FeatureColourI fc = new FeatureColour(Color.red);
+    fr.getFeatureColours().put("Cath", fc);
+    SequenceFeature sf1 = new SequenceFeature("Cath", "", 6, 8, Float.NaN,
+            "group1");
+    assertEquals(fr.getColour(sf1), Color.red);
 
     /*
-     * co-located features with colour by label
-     * should not get filtered
+     * hide feature type, then unhide
+     * - feature type visibility should not affect the result
      */
-    features = seq.getSequenceFeatures();
-    FeatureColour fc = new FeatureColour(Color.black);
-    fc.setColourByLabel(true);
-    fr.filterFeaturesForDisplay(features, fc);
-    assertEquals(features.size(), 4);
-    assertTrue(features.contains(sf1));
-    assertTrue(features.contains(sf3));
-    assertTrue(features.contains(sf4));
-    assertTrue(features.contains(sf5));
+    FeatureSettingsBean[] data = new FeatureSettingsBean[1];
+    data[0] = new FeatureSettingsBean("Cath", fc, null, false);
+    fr.setFeaturePriority(data);
+    assertEquals(fr.getColour(sf1), Color.red);
+    data[0] = new FeatureSettingsBean("Cath", fc, null, true);
+    fr.setFeaturePriority(data);
+    assertEquals(fr.getColour(sf1), Color.red);
+
+    /*
+     * hide feature group, then unhide
+     */
+    fr.setGroupVisibility("group1", false);
+    assertNull(fr.getColour(sf1));
+    fr.setGroupVisibility("group1", true);
+    assertEquals(fr.getColour(sf1), Color.red);
+
+    /*
+     * graduated colour by score, no threshold, no score
+     * 
+     */
+    FeatureColourI gc = new FeatureColour(Color.yellow, Color.red,
+            Color.green, 1f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    assertEquals(fr.getColour(sf1), Color.green);
+
+    /*
+     * graduated colour by score, no threshold, with score value
+     */
+    SequenceFeature sf2 = new SequenceFeature("Cath", "", 6, 8, 6f,
+            "group1");
+    // score 6 is half way from yellow(255, 255, 0) to red(255, 0, 0)
+    Color expected = new Color(255, 128, 0);
+    assertEquals(fr.getColour(sf2), expected);
+
+    /*
+     * above threshold, score is above threshold - no change
+     */
+    gc.setAboveThreshold(true);
+    gc.setThreshold(5f);
+    assertEquals(fr.getColour(sf2), expected);
+
+    /*
+     * threshold is min-max; now score 6 is 1/6 of the way from 5 to 11
+     * or from yellow(255, 255, 0) to red(255, 0, 0)
+     */
+    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 5f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    gc.setAutoScaled(false); // this does little other than save a checkbox setting!
+    assertEquals(fr.getColour(sf2), new Color(255, 213, 0));
+
+    /*
+     * feature score is below threshold - no colour
+     */
+    gc.setAboveThreshold(true);
+    gc.setThreshold(7f);
+    assertNull(fr.getColour(sf2));
+
+    /*
+     * feature score is above threshold - no colour
+     */
+    gc.setBelowThreshold(true);
+    gc.setThreshold(3f);
+    assertNull(fr.getColour(sf2));
+
+    /*
+     * colour by feature attribute value
+     * first with no value held
+     */
+    gc = new FeatureColour(Color.yellow, Color.red, Color.green, 1f, 11f);
+    fr.getFeatureColours().put("Cath", gc);
+    gc.setAttributeName("AF");
+    assertEquals(fr.getColour(sf2), Color.green);
+
+    // with non-numeric attribute value
+    sf2.setValue("AF", "Five");
+    assertEquals(fr.getColour(sf2), Color.green);
+
+    // with numeric attribute value
+    sf2.setValue("AF", "6");
+    assertEquals(fr.getColour(sf2), expected);
+
+    // with numeric value outwith threshold
+    gc.setAboveThreshold(true);
+    gc.setThreshold(10f);
+    assertNull(fr.getColour(sf2));
+
+    // with filter on AF < 4
+    gc.setAboveThreshold(false);
+    assertEquals(fr.getColour(sf2), expected);
+    FeatureMatcherSetI filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.LT, "4.0", "AF"));
+    fr.setFeatureFilter("Cath", filter);
+    assertNull(fr.getColour(sf2));
+
+    // with filter on 'Consequence contains missense'
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Contains, "missense",
+            "Consequence"));
+    fr.setFeatureFilter("Cath", filter);
+    // if feature has no Consequence attribute, no colour
+    assertNull(fr.getColour(sf2));
+    // if attribute does not match filter, no colour
+    sf2.setValue("Consequence", "Synonymous");
+    assertNull(fr.getColour(sf2));
+    // attribute matches filter
+    sf2.setValue("Consequence", "Missense variant");
+    assertEquals(fr.getColour(sf2), expected);
+
+    // with filter on CSQ:Feature contains "ENST01234"
+    filter = new FeatureMatcherSet();
+    filter.and(FeatureMatcher.byAttribute(Condition.Matches, "ENST01234",
+            "CSQ", "Feature"));
+    fr.setFeatureFilter("Cath", filter);
+    // if feature has no CSQ data, no colour
+    assertNull(fr.getColour(sf2));
+    // if CSQ data does not include Feature, no colour
+    Map<String, String> csqData = new HashMap<>();
+    csqData.put("BIOTYPE", "Transcript");
+    sf2.setValue("CSQ", csqData);
+    assertNull(fr.getColour(sf2));
+    // if attribute does not match filter, no colour
+    csqData.put("Feature", "ENST9876");
+    assertNull(fr.getColour(sf2));
+    // attribute matches filter
+    csqData.put("Feature", "ENST01234");
+    assertEquals(fr.getColour(sf2), expected);
   }
 }
index 0b5b6bd..030a90f 100644 (file)
@@ -20,7 +20,7 @@ public class Blosum62ColourSchemeTest
    * </ul>
    * <ul>
    */
-  @Test
+  @Test(groups = "Functional")
   public void testFindColour()
   {
     ColourSchemeI blosum = new Blosum62ColourScheme();
index 7a72c15..2eb718b 100644 (file)
@@ -25,7 +25,9 @@ import static org.testng.AssertJUnit.assertFalse;
 import static org.testng.AssertJUnit.assertNull;
 import static org.testng.AssertJUnit.assertTrue;
 import static org.testng.AssertJUnit.fail;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
 
+import jalview.api.FeatureColourI;
 import jalview.datamodel.SequenceFeature;
 import jalview.gui.JvOptionPane;
 import jalview.util.ColorUtils;
@@ -36,6 +38,8 @@ import java.awt.Color;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import junit.extensions.PA;
+
 public class FeatureColourTest
 {
 
@@ -57,6 +61,8 @@ public class FeatureColourTest
     assertTrue(fc1.getColour().equals(Color.RED));
     assertFalse(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
 
     /*
      * min-max colour
@@ -68,9 +74,31 @@ public class FeatureColourTest
     assertTrue(fc1.isGraduatedColour());
     assertFalse(fc1.isColourByLabel());
     assertTrue(fc1.isAboveThreshold());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
     assertEquals(12f, fc1.getThreshold());
     assertEquals(Color.gray, fc1.getMinColour());
     assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.gray, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+
+    /*
+     * min-max-noValue colour
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
     assertEquals(10f, fc1.getMin());
     assertEquals(20f, fc1.getMax());
 
@@ -82,6 +110,127 @@ public class FeatureColourTest
     fc1 = new FeatureColour(fc);
     assertTrue(fc1.isColourByLabel());
     assertFalse(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+
+    /*
+     * colour by attribute (label)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isColourByLabel());
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+
+    /*
+     * colour by attribute (value)
+     */
+    fc = new FeatureColour(Color.gray, Color.black, Color.green, 10f, 20f);
+    fc.setAboveThreshold(true);
+    fc.setThreshold(12f);
+    fc.setAttributeName("AF");
+    fc1 = new FeatureColour(fc);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+    assertTrue(fc1.isAboveThreshold());
+    assertEquals(12f, fc1.getThreshold());
+    assertEquals(Color.gray, fc1.getMinColour());
+    assertEquals(Color.black, fc1.getMaxColour());
+    assertEquals(Color.green, fc1.getNoColour());
+    assertEquals(10f, fc1.getMin());
+    assertEquals(20f, fc1.getMax());
+  }
+
+  @Test(groups = { "Functional" })
+  public void testCopyConstructor_minMax()
+  {
+    /*
+     * graduated colour
+     */
+    FeatureColour fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds
+     */
+    FeatureColour fc1 = new FeatureColour(fc, 2f, 6f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
+    assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * update min-max bounds - high to low
+     */
+    fc1 = new FeatureColour(fc, 23f, 16f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(23f, fc1.getMin());
+    assertEquals(16f, fc1.getMax());
+    assertTrue((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * graduated colour by attribute
+     */
+    fc1.setAttributeName("AF");
+    fc1 = new FeatureColour(fc1, 13f, 36f);
+    assertTrue(fc1.isGraduatedColour());
+    assertFalse(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc1.getAttributeName());
+    assertEquals(13f, fc1.getMin());
+    assertEquals(36f, fc1.getMax());
+    assertFalse((boolean) PA.getValue(fc1, "isHighToLow"));
+
+    /*
+     * colour by label
+     */
+    fc = new FeatureColour(Color.BLUE, Color.RED, 1f, 5f);
+    fc.setColourByLabel(true);
+    assertFalse(fc.isGraduatedColour());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.isColourByAttribute());
+    assertNull(fc.getAttributeName());
+    assertEquals(1f, fc.getMin());
+    assertEquals(5f, fc.getMax());
+
+    /*
+     * update min-max bounds
+     */
+    fc1 = new FeatureColour(fc, 2f, 6f);
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByLabel());
+    assertFalse(fc1.isColourByAttribute());
+    assertNull(fc1.getAttributeName());
+    assertEquals(2f, fc1.getMin());
+    assertEquals(6f, fc1.getMax());
+
+    /*
+     * colour by attribute text
+     */
+    fc1.setAttributeName("AC");
+    fc1 = new FeatureColour(fc1, 13f, 36f);
+    assertFalse(fc1.isGraduatedColour());
+    assertTrue(fc1.isColourByLabel());
+    assertTrue(fc1.isColourByAttribute());
+    assertArrayEquals(new String[] { "AC" }, fc1.getAttributeName());
+    assertEquals(13f, fc1.getMin());
+    assertEquals(36f, fc1.getMax());
   }
 
   @Test(groups = { "Functional" })
@@ -106,8 +255,11 @@ public class FeatureColourTest
   @Test(groups = { "Functional" })
   public void testGetColor_Graduated()
   {
-    // graduated colour from score 0 to 100, gray(128, 128, 128) to red(255, 0,
-    // 0)
+    /*
+     * graduated colour from 
+     * score 0 to 100
+     * gray(128, 128, 128) to red(255, 0, 0)
+     */
     FeatureColour fc = new FeatureColour(Color.GRAY, Color.RED, 0f, 100f);
     // feature score is 75 which is 3/4 of the way from GRAY to RED
     SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
@@ -174,20 +326,29 @@ public class FeatureColourTest
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
+     * colour by attribute text (no threshold)
+     */
+    fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("CLIN_SIG");
+    assertEquals("domain\tattribute|CLIN_SIG", fc.toJalviewFormat("domain"));
+    
+    /*
      * colour by label (autoscaled) (an odd state you can reach by selecting
      * 'above threshold', then deselecting 'threshold is min/max' then 'colour
      * by label')
      */
+    fc.setAttributeName((String[]) null);
     fc.setAutoScaled(true);
     assertEquals("domain\tlabel", fc.toJalviewFormat("domain"));
 
     /*
-     * colour by label (above threshold) (min/max values are output though not
-     * used by this scheme)
+     * colour by label (above threshold) 
      */
     fc.setAutoScaled(false);
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
+    // min/max values are output though not used by this scheme
     assertEquals("domain\tlabel|||0.0|0.0|above|12.5",
             fc.toJalviewFormat("domain"));
 
@@ -199,38 +360,80 @@ public class FeatureColourTest
             fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour, no threshold
+     * colour by attributes text (below threshold)
+     */
+    fc.setBelowThreshold(true);
+    fc.setAttributeName("CSQ", "Consequence");
+    assertEquals("domain\tattribute|CSQ:Consequence|||0.0|0.0|below|12.5",
+            fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold
+     * - default constructor sets noValueColor = minColor
      */
     fc = new FeatureColour(Color.GREEN, Color.RED, 12f, 25f);
     String greenHex = Format.getHexString(Color.GREEN);
-    String expected = String.format("domain\t%s|%s|abso|12.0|25.0|none",
-            greenHex, redHex);
+    String expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
+     * graduated colour by score, no threshold, no value gets min colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.GREEN, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMin|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by score, no threshold, no value gets max colour
+     */
+    fc = new FeatureColour(Color.GREEN, Color.RED, Color.RED, 12f, 25f);
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|none", greenHex,
+            redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+    
+    /*
      * colour ranges over the actual score ranges (not min/max)
      */
     fc.setAutoScaled(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|none", greenHex,
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|none", greenHex,
             redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour below threshold
+     * graduated colour by score, below threshold
      */
     fc.setThreshold(12.5f);
     fc.setBelowThreshold(true);
-    expected = String.format("domain\t%s|%s|12.0|25.0|below|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|12.0|25.0|below|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
 
     /*
-     * graduated colour above threshold
+     * graduated colour by score, above threshold
      */
     fc.setThreshold(12.5f);
     fc.setAboveThreshold(true);
     fc.setAutoScaled(false);
-    expected = String.format("domain\t%s|%s|abso|12.0|25.0|above|12.5",
+    expected = String.format(
+            "domain\tscore|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
+            greenHex, redHex);
+    assertEquals(expected, fc.toJalviewFormat("domain"));
+
+    /*
+     * graduated colour by attribute, above threshold
+     */
+    fc.setAttributeName("CSQ", "AF");
+    fc.setAboveThreshold(true);
+    fc.setAutoScaled(false);
+    expected = String.format(
+            "domain\tattribute|CSQ:AF|%s|%s|noValueMax|abso|12.0|25.0|above|12.5",
             greenHex, redHex);
     assertEquals(expected, fc.toJalviewFormat("domain"));
   }
@@ -244,7 +447,7 @@ public class FeatureColourTest
     /*
      * simple colour by name
      */
-    FeatureColour fc = FeatureColour.parseJalviewFeatureColour("red");
+    FeatureColourI fc = FeatureColour.parseJalviewFeatureColour("red");
     assertTrue(fc.isSimpleColour());
     assertEquals(Color.RED, fc.getColour());
 
@@ -292,7 +495,28 @@ public class FeatureColourTest
     assertEquals(12.0f, fc.getThreshold());
 
     /*
-     * graduated colour (by name) (no threshold)
+     * colour by attribute text (no threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour("attribute|CLIN_SIG");
+    assertTrue(fc.isColourByAttribute());
+    assertTrue(fc.isColourByLabel());
+    assertFalse(fc.hasThreshold());
+    assertArrayEquals(new String[] { "CLIN_SIG" }, fc.getAttributeName());
+
+    /*
+     * colour by attributes text (with score threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:Consequence|||0.0|0.0|above|12.0");
+    assertTrue(fc.isColourByLabel());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "Consequence" },
+            fc.getAttributeName());
+    assertTrue(fc.isAboveThreshold());
+    assertEquals(12.0f, fc.getThreshold());
+
+    /*
+     * graduated colour by score (with colour names) (no threshold)
      */
     fc = FeatureColour.parseJalviewFeatureColour("red|green|10.0|20.0");
     assertTrue(fc.isGraduatedColour());
@@ -304,7 +528,35 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
-     * graduated colour (by hex code) (above threshold)
+     * graduated colour (explicitly by 'score') (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("Score|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by attribute (no threshold)
+     */
+    fc = FeatureColour
+            .parseJalviewFeatureColour("attribute|AF|red|green|10.0|20.0");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "AF" }, fc.getAttributeName());
+    assertFalse(fc.hasThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
+     * graduated colour by score (colours by hex code) (above threshold)
      */
     String descriptor = String.format("%s|%s|10.0|20.0|above|15",
             Format.getHexString(Color.RED),
@@ -321,9 +573,26 @@ public class FeatureColourTest
     assertTrue(fc.isAutoScaled());
 
     /*
+     * graduated colour by attributes (below threshold)
+     */
+    fc = FeatureColour.parseJalviewFeatureColour(
+            "attribute|CSQ:AF|red|green|10.0|20.0|below|13");
+    assertTrue(fc.isGraduatedColour());
+    assertTrue(fc.isColourByAttribute());
+    assertArrayEquals(new String[] { "CSQ", "AF" }, fc.getAttributeName());
+    assertTrue(fc.hasThreshold());
+    assertTrue(fc.isBelowThreshold());
+    assertEquals(13f, fc.getThreshold());
+    assertEquals(Color.RED, fc.getMinColour());
+    assertEquals(Color.GREEN, fc.getMaxColour());
+    assertEquals(10f, fc.getMin());
+    assertEquals(20f, fc.getMax());
+    assertTrue(fc.isAutoScaled());
+
+    /*
      * graduated colour (by RGB triplet) (below threshold), absolute scale
      */
-    descriptor = String.format("255,0,0|0,255,0|abso|10.0|20.0|below|15");
+    descriptor = "255,0,0|0,255,0|abso|10.0|20.0|below|15";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
     assertFalse(fc.isAutoScaled());
@@ -335,9 +604,63 @@ public class FeatureColourTest
     assertEquals(10f, fc.getMin());
     assertEquals(20f, fc.getMax());
 
-    descriptor = String
-            .format("blue|255,0,255|absolute|20.0|95.0|below|66.0");
+    descriptor = "blue|255,0,255|absolute|20.0|95.0|below|66.0";
     fc = FeatureColour.parseJalviewFeatureColour(descriptor);
     assertTrue(fc.isGraduatedColour());
   }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_colourByAttributeText()
+  {
+    FeatureColour fc = new FeatureColour();
+    fc.setColourByLabel(true);
+    fc.setAttributeName("consequence");
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 1f,
+            null);
+
+    /*
+     * if feature has no such attribute, use 'no value' colour
+     */
+    assertEquals(FeatureColour.DEFAULT_NO_COLOUR, fc.getColor(sf));
+
+    /*
+     * if feature has attribute, generate colour from value
+     */
+    sf.setValue("consequence", "benign");
+    Color expected = ColorUtils.createColourFromName("benign");
+    assertEquals(expected, fc.getColor(sf));
+  }
+
+  @Test(groups = { "Functional" })
+  public void testGetColor_GraduatedByAttributeValue()
+  {
+    /*
+     * graduated colour based on attribute value for AF
+     * given a min-max range of 0-100
+     */
+    FeatureColour fc = new FeatureColour(new Color(50, 100, 150),
+            new Color(150, 200, 250), Color.yellow, 0f, 100f);
+    String attName = "AF";
+    fc.setAttributeName(attName);
+
+    /*
+     * first case: feature lacks the attribute - use 'no value' colour
+     */
+    SequenceFeature sf = new SequenceFeature("type", "desc", 0, 20, 75f,
+            null);
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * second case: attribute present but not numeric - treat as if absent
+     */
+    sf.setValue(attName, "twelve");
+    assertEquals(Color.yellow, fc.getColor(sf));
+
+    /*
+     * third case: valid attribute value
+     */
+    sf.setValue(attName, "20.0");
+    Color expected = new Color(70, 120, 170);
+    assertEquals(expected, fc.getColor(sf));
+  }
 }
index 85aea40..4bee3f5 100644 (file)
@@ -260,6 +260,8 @@ public class Mapping
   @Test(groups = { "Functional" })
   public void compareTransferredToRefPDBAnnot() throws Exception
   {
+    StructureImportSettings.setProcessSecondaryStructure(true);
+    StructureImportSettings.setVisibleChainAnnotation(true);
     StructureImportSettings.setShowSeqFeatures(true);
     AlignFrame ref = new FileLoader(false)
             .LoadFileWaitTillLoaded("test/jalview/ext/jmol/1QCF.pdb",
index f26c5f1..808e662 100644 (file)
@@ -1,8 +1,34 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.structure;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
+import jalview.datamodel.Mapping;
+import jalview.datamodel.Sequence;
+import jalview.datamodel.SequenceI;
+import jalview.util.MapList;
+
 import java.util.HashMap;
 import java.util.List;
 
@@ -11,9 +37,9 @@ import org.testng.annotations.Test;
 public class StructureMappingTest
 {
   @Test(groups = "Functional")
-  public void testgetPDBResNumRanges()
+  public void testGetPDBResNumRanges()
   {
-    HashMap<Integer, int[]> map = new HashMap<Integer, int[]>();
+    HashMap<Integer, int[]> map = new HashMap<>();
 
     StructureMapping mapping = new StructureMapping(null, null, null, null,
             map, null);
@@ -43,4 +69,75 @@ public class StructureMappingTest
     assertEquals(ranges.get(1)[0], 15);
     assertEquals(ranges.get(1)[1], 15);
   }
+
+  @Test(groups = "Functional")
+  public void testEquals()
+  {
+    SequenceI seq1 = new Sequence("seq1", "ABCDE");
+    SequenceI seq2 = new Sequence("seq1", "ABCDE");
+    String pdbFile = "a/b/file1.pdb";
+    String pdbId = "1a70";
+    String chain = "A";
+    String mappingDetails = "these are the mapping details, honest";
+    HashMap<Integer, int[]> map = new HashMap<>();
+
+    Mapping seqToPdbMapping = new Mapping(seq1,
+            new MapList(new int[]
+            { 1, 5 }, new int[] { 2, 6 }, 1, 1));
+    StructureMapping sm1 = new StructureMapping(seq1, pdbFile, pdbId, chain,
+            map, mappingDetails, seqToPdbMapping);
+    assertFalse(sm1.equals(null));
+    assertFalse(sm1.equals("x"));
+
+    StructureMapping sm2 = new StructureMapping(seq1, pdbFile, pdbId, chain,
+            map, mappingDetails, seqToPdbMapping);
+    assertTrue(sm1.equals(sm2));
+    assertTrue(sm2.equals(sm1));
+    assertEquals(sm1.hashCode(), sm2.hashCode());
+
+    // with different sequence
+    sm2 = new StructureMapping(seq2, pdbFile, pdbId, chain, map,
+            mappingDetails, seqToPdbMapping);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+
+    // with different file
+    sm2 = new StructureMapping(seq1, "a/b/file2.pdb", pdbId, chain, map,
+            mappingDetails, seqToPdbMapping);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+
+    // with different pdbid (case sensitive)
+    sm2 = new StructureMapping(seq1, pdbFile, "1A70", chain, map,
+            mappingDetails, seqToPdbMapping);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+
+    // with different chain
+    sm2 = new StructureMapping(seq1, pdbFile, pdbId, "B", map,
+            mappingDetails, seqToPdbMapping);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+
+    // map is ignore for this test
+    sm2 = new StructureMapping(seq1, pdbFile, pdbId, chain, null,
+            mappingDetails, seqToPdbMapping);
+    assertTrue(sm1.equals(sm2));
+    assertTrue(sm2.equals(sm1));
+
+    // with different mapping details
+    sm2 = new StructureMapping(seq1, pdbFile, pdbId, chain, map,
+            "different details!", seqToPdbMapping);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+
+    // with different seq to pdb mapping
+    Mapping map2 = new Mapping(seq1,
+            new MapList(new int[]
+            { 1, 5 }, new int[] { 3, 7 }, 1, 1));
+    sm2 = new StructureMapping(seq1, pdbFile, pdbId, chain, map,
+            mappingDetails, map2);
+    assertFalse(sm1.equals(sm2));
+    assertFalse(sm2.equals(sm1));
+  }
 }
index a59fbde..286be1b 100644 (file)
  */
 package jalview.structure;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.AssertJUnit.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
+import jalview.analysis.AlignmentUtils;
+import jalview.api.structures.JalviewStructureDisplayI;
+import jalview.bin.Cache;
 import jalview.datamodel.AlignedCodonFrame;
+import jalview.datamodel.AlignmentAnnotation;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.Annotation;
+import jalview.datamodel.PDBEntry;
 import jalview.datamodel.Sequence;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
+import jalview.ext.jmol.JmolCommands;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
 import jalview.gui.JvOptionPane;
+import jalview.gui.SequenceRenderer;
+import jalview.gui.StructureChooser;
 import jalview.io.DataSourceType;
+import jalview.io.FileLoader;
+import jalview.io.Jalview2xmlBase;
 import jalview.io.StructureFile;
 import jalview.util.MapList;
+import jalview.ws.DBRefFetcher;
+import jalview.ws.sifts.SiftsSettings;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-public class StructureSelectionManagerTest
+@Test(singleThreaded = true)
+public class StructureSelectionManagerTest extends Jalview2xmlBase
 {
 
+  @Override
   @BeforeClass(alwaysRun = true)
   public void setUpJvOptionPane()
   {
@@ -160,4 +185,313 @@ public class StructureSelectionManagerTest
     assertEquals("1gaq", sf.getFeatureGroup());
     assertEquals("ALA:   1  1gaqB", sf.getDescription());
   }
+
+  /**
+   * Verify that RESNUM sequence features are present after creating a PDB
+   * mapping from a local file, then that everything stays in the same place
+   * when the file is viewed. The corner case is that 4IM2 is a fragment of a
+   * PDB file, which still includes the 'ID' field - a bug in Jalview 2.10.3
+   * causes features, annotation and positions to be remapped to the wrong place
+   * on viewing the structure
+   */
+  @Test(groups = { "Network" })
+  public void testMapping_EqualsFeatures()
+  {
+    // for some reason 'BeforeMethod' (which should be inherited from
+    // Jalview2XmlBase isn't always called)...
+    Desktop.instance.closeAll_actionPerformed(null);
+    try { 
+      Thread.sleep(200);
+    } catch (Exception foo) {}; 
+    SequenceI seq = new Sequence("4IM2|A",
+            "LDFCIRNIEKTVMGEISDIHTKLLRLSSSQGTIE");
+    String P4IM2_MISSING = "examples/testdata/4IM2_missing.pdb";
+    StructureSelectionManager sm = new StructureSelectionManager();
+    sm.setProcessSecondaryStructure(true);
+    sm.setAddTempFacAnnot(true);
+    StructureFile pmap = sm.setMapping(true, new SequenceI[] { seq },
+            new String[]
+            { null }, P4IM2_MISSING,
+            DataSourceType.FILE);
+    assertTrue(pmap != null);
+
+    assertEquals(1, pmap.getSeqs().size());
+    assertEquals("4IM2|A", pmap.getSeqs().get(0).getName());
+
+    List<int[]> structuremap1 = new ArrayList(
+            sm.getMapping(P4IM2_MISSING)[0]
+                    .getPDBResNumRanges(seq.getStart(), seq.getEnd()));
+
+    /*
+     * Verify a RESNUM sequence feature in the PDBfile sequence
+     * LEU468 - start+0 
+     * VAL479 - start+11
+     * MET486 - start+12
+     * GLY496 - start+13
+     * GLU516 - start+33 (last)
+     * 
+     * Expect features and mapping to resolve to same residues.
+     * Also try creating a view and test again
+     *   
+     */
+    String[] feats = new String[] { "LEU", "468", "VAL", "479", "MET",
+        "486", "GLY", "496", "GLU", "516" };
+    int[] offset = new int[] { 0, 11, 12, 13, 33 };
+
+    List<String> fdesc = new ArrayList<>();
+    for (int f = 0; f < feats.length; f += 2)
+    {
+      fdesc.add(feats[f] + ": " + feats[f + 1] + "  4im2A");
+    }
+    SequenceI pdbseq = pmap.getSeqs().get(0);
+    verifySeqFeats(pdbseq, offset, fdesc);
+
+    /// Now load as a view
+
+    AlignFrame alf = new FileLoader(false).LoadFileWaitTillLoaded(
+            "examples/testdata/4IM2_missing.pdb", DataSourceType.FILE);
+    Desktop.addInternalFrame(alf, "examples/testdata/4IM2_missing.pdb", 800,
+            400);
+    AlignmentI pdbal = alf.getViewport().getAlignment();
+    SequenceI pdb_viewseq = pdbal.getSequenceAt(0);
+    assertEquals(pdb_viewseq.getSequenceAsString(),
+            seq.getSequenceAsString());
+    // verify the feature location on the sequence when pdb imported as an
+    // alignment
+    verifySeqFeats(pdb_viewseq, offset, fdesc);
+
+
+    JalviewStructureDisplayI viewr = openStructureViaChooser(alf,
+            pdb_viewseq, "4IM2");
+
+    // and check all is good with feature location still
+    verifySeqFeats(pdb_viewseq, offset, fdesc);
+
+    // finally check positional mapping for sequence and structure
+    PDBEntry pdbe = seq.getPDBEntry("4IM2");
+    StructureSelectionManager apssm = alf.alignPanel
+            .getStructureSelectionManager();
+    StructureMapping[] smap = apssm
+            .getMapping(pdbe.getFile());
+    assertNotNull(smap);
+    assertNotNull(smap[0]);
+    // find the last position in the alignment sequence - this is not
+    // 'SequenceI.getEnd()' - which gets the last PDBRESNUM rather than
+    // SequenceI.getStart() + number of residues in file...
+    int realSeqEnd = pdb_viewseq.findPosition(pdb_viewseq.getLength());
+    List<int[]> ranges = smap[0].getPDBResNumRanges(pdb_viewseq.getStart(),
+            realSeqEnd);
+    assertEquals(structuremap1.size(), ranges.size());
+    int tot_mapped = 0;
+    for (int p = 0; p < ranges.size(); p++)
+    {
+      assertArrayEquals(structuremap1.get(p), ranges.get(p));
+      tot_mapped += 1 + (structuremap1.get(p)[1] - structuremap1.get(p)[0]);
+    }
+
+    assertEquals(pdb_viewseq.getLength(), tot_mapped);
+
+    int lastmappedp = StructureMapping.UNASSIGNED_VALUE;
+    for (int rp = pdb_viewseq.getStart(), rpEnd = pdb_viewseq
+            .findPosition(pdb_viewseq.getLength() - 1); rp <= rpEnd; rp++)
+    {
+      int mappedp = smap[0].getPDBResNum(rp);
+      if (mappedp != StructureMapping.UNASSIGNED_VALUE)
+      {
+        tot_mapped--;
+        if (lastmappedp == mappedp)
+        {
+          Assert.fail("Duplicate mapped position at " + rp + " (dupe = "
+                  + mappedp + ")");
+        }
+      }
+    }
+
+    Assert.assertEquals(tot_mapped, 0,
+            "Different number of mapped residues compared to ranges of mapped residues");
+
+    // positional mapping to atoms for color by structure is still wrong, even
+    // though panel looks correct.
+
+    StructureMappingcommandSet smcr[] = JmolCommands
+            .getColourBySequenceCommand(apssm,
+            new String[]
+            { pdbe.getFile() },
+            new SequenceI[][]
+            { new SequenceI[] { pdb_viewseq } },
+                    new SequenceRenderer(alf.alignPanel.getAlignViewport()),
+                    alf.alignPanel);
+    // Expected - all residues are white
+    for (StructureMappingcommandSet smm : smcr)
+    {
+      for (String c : smm.commands)
+      {
+        System.out.println(c);
+      }
+    }
+  }
+
+  private void verifySeqFeats(SequenceI pdbseq, int[] offset,
+          List<String> fdesc)
+  {
+    for (int o = 0; o < offset.length; o++)
+    {
+      int res = pdbseq.findPosition(offset[o]);
+      List<SequenceFeature> sf = pdbseq.getFeatures().findFeatures(res, res,
+              "RESNUM");
+      assertEquals("Expected sequence feature at position " + res + "("
+              + offset[o] + ")", 1, sf.size());
+      assertEquals("Wrong description at " + res + "(" + offset[o] + ")",
+              fdesc.get(o), sf.get(0).getDescription());
+    }
+
+  }
+
+  @Test(groups = { "Network" })
+  public void testAssociatedMappingToSubSeq() throws Exception
+  {
+
+    // currently this test fails if trimming is enabled
+    Cache.setProperty(DBRefFetcher.TRIM_RETRIEVED_SEQUENCES,
+            Boolean.FALSE.toString());
+    String TEMP_FACTOR_AA="Temperature Factor";
+    String PDBID = "4IM2";
+    String FullLengthSeq = ">TBK1_HUMAN Serine/threonine-protein kinase TBK1\n" + 
+            "MQSTSNHLWLLSDILGQGATANVFRGRHKKTGDLFAIKVFNNISFLRPVDVQMREFEVLKKLNHKNIVKLFA\n" + 
+            "IEEETTTRHKVLIMEFCPCGSLYTVLEEPSNAYGLPESEFLIVLRDVVGGMNHLRENGIVHRDIKPGNIMRV\n" + 
+            "IGEDGQSVYKLTDFGAARELEDDEQFVSLYGTEEYLHPDMYERAVLRKDHQKKYGATVDLWSIGVTFYHAAT\n" + 
+            "GSLPFRPFEGPRRNKEVMYKIITGKPSGAISGVQKAENGPIDWSGDMPVSCSLSRGLQVLLTPVLANILEAD\n" + 
+            "QEKCWGFDQFFAETSDILHRMVIHVFSLQQMTAHKIYIHSYNTATIFHELVYKQTKIISSNQELIYEGRRLV\n" + 
+            "LEPGRLAQHFPKTTEENPIFVVSREPLNTIGLIYEKISLPKVHPRYDLDGDASMAKAITGVVCYACRIASTL\n" + 
+            "LLYQELMRKGIRWLIELIKDDYNETVHKKTEVVITLDFCIRNIEKTVKVYEKLMKINLEAAELGEISDIHTK\n" + 
+            "LLRLSSSQGTIETSLQDIDSRLSPGGSLADAWAHQEGTHPKDRNVEKLQVLLNCMTEIYYQFKKDKAERRLA\n" + 
+            "YNEEQIHKFDKQKLYYHATKAMTHFTDECVKKYEAFLNKSEEWIRKMLHLRKQLLSLTNQCFDIEEEVSKYQ\n" + 
+            "EYTNELQETLPQKMFTASSGIKHTMTPIYPSSNTLVEMTLGMKKLKEEMEGVVKELAENNHILERFGSLTMD\n" + 
+            "GGLRNVDCL";
+    /*
+     * annotation exported after importing full length sequence to desktop, opening 4IM2 and selecting 'Add Reference Annotation'.
+     * 
+     * Note - tabs must be replaced with \t - Eclipse expands them to spaces otherwise.
+     */
+    String FullLengthAnnot = "JALVIEW_ANNOTATION\n" + 
+            "# Created: Mon Feb 05 15:30:20 GMT 2018\n" + 
+            "# Updated: Fri Feb 09 17:05:17 GMT 2018\n" + 
+            "\n" + 
+            "\n" + 
+            "SEQUENCE_REF\tTBK1_HUMAN\n"
+            + "LINE_GRAPH\tTemperature Factor\tTemperature Factor for 4im2A\t125.22|128.51|120.35|113.12|122.6|114.44|91.49|102.53|98.22|111.41|111.32|116.64|103.55|100.53|95.07|105.55|114.76|128.29|133.55|142.14|121.12|110.36|95.79|95.39|87.14|99.56|93.55|94.21|100.33|110.68|97.85|82.37|75.87|76.53|77.85|82.49|80.92|96.88|122.58|133.31|160.15|180.51|||||242.88|258.97|247.01|227.12|223.24|211.62|184.65|183.51|168.96|160.04|150.88|131.68|130.43|139.87|148.59|136.57|125.7|96.51|74.49|74.08|85.87|70.93|86.47|101.59|97.51|97.39|117.19|114.27|129.5|112.98|147.52|170.26|154.98|168.18|157.51|131.95|105.85|97.78|97.35|76.51|76.31|72.55|71.43|78.82|79.94|75.04|79.54|77.95|83.56|88.5|71.51|71.73|75.96|82.36|81.75|66.51|67.23|69.35|67.92|54.75|71.19|61.85|65.34|67.97|64.51|67.41|62.28|72.85|72.76|70.64|65.23|71.07|67.73|87.72|64.93|75.92|94.02|99.35|93.71|103.59|106.29|115.46|118.69|147.18|130.62|171.64|158.95|164.11||107.42|88.53|83.52|88.06|94.06|80.82|59.01|59.73|78.89|69.21|70.34|81.95|74.53|60.92|64.65|55.79|75.71|68.86|70.95|75.08|87.76|85.43|105.84|||||||||||||||||137.46|151.33|145.17|122.79|111.56|126.72|124.06|161.75|176.84|180.51|198.49|196.75|187.41||195.23|202.27|203.16|226.55|221.75|193.83||||||172.33|177.97|151.47|132.65|99.22|93.7|91.15|88.24|72.35|70.05|70.0|74.92|66.51|68.37|65.76|70.12|74.97|76.89|80.83|70.21|69.48|79.54|82.65|96.54|114.31|140.46|168.51|176.99|205.08|209.27|155.83|139.41|151.3|129.33|111.31|119.62|121.37|102.26|115.39|129.97|128.65|110.38|110.66|116.1|82.53|84.02|82.17|87.63|86.42|77.23|91.23|95.53|102.21|120.73|133.26|109.67|108.49|93.25|92.85|86.39|95.66|94.92|85.82|80.13|76.17|86.61|78.9|77.97|105.6|70.66|69.35|78.94|66.68|63.03|69.91|79.05|75.43|70.73|70.02|80.57|81.74|77.99|84.1|91.66|92.42|94.03|116.47|132.01|154.55|163.99|161.37|155.23|132.78|109.3|90.38|101.83|99.61|91.68|82.77|86.12|82.73|90.13|85.14|79.54|74.27|74.06|72.88|86.34|72.0|69.32|60.9|68.15|52.99|63.53|61.3|66.01|68.28|77.41|71.52|67.18|66.17|71.51|65.47|52.63|65.08|66.37|73.76|77.79|67.58|79.53|84.75|87.42|78.9|79.19|85.57|73.67|80.56|86.19|72.17|66.27|72.8|86.28|78.89|74.5|90.6|80.42|92.5|92.84|96.18|92.08|88.5|87.25|64.6|68.95|65.56|67.55|71.62|78.24|84.95|71.35|86.41|84.73|94.41|95.09|84.74|87.64|88.85|75.1|86.42|79.28|73.14|78.54|80.81|60.66|67.93|71.64|59.85|64.7|61.22|63.84|65.9|62.18|74.95|72.92|93.37|90.47|96.0|93.8|88.46|79.78|83.4|66.55|68.7|73.2|78.76|85.67|84.8|89.59|96.52|79.53|103.51|134.72|126.7|145.31|156.17|149.35|128.48|117.29|118.98|131.59|109.36|90.39|87.68|91.81|78.77|80.11|91.39|75.57|78.98|71.53|76.85|70.9|64.71|73.55|73.45|60.0|69.92|57.89|69.07|66.45|62.85|57.83|57.89|66.4|61.61|60.85|66.47|63.53|63.84|65.96|73.06|70.82|64.51|63.66|73.37|73.59|68.09|78.93|76.99|75.05|71.32|88.4|78.88|93.08|110.61|94.32|99.24|128.99|129.49|132.74|124.21|120.32|142.06|166.41|149.87|153.29|172.19|165.89|181.6|223.11|237.73|176.41|171.09|189.65|188.61|154.84|142.72|154.25|170.99|175.65|||||||110.61||||||||||158.07|170.73|167.93|198.47|212.36|181.71|157.69|163.31|138.96|120.29|131.63|152.26|125.06|136.66|148.97|129.68|120.52|135.31|136.05|119.39|124.18|128.94|123.02|103.37|128.44|134.12|118.88|120.94|130.38|124.67|112.21|113.69|123.65|132.06|114.97|110.75|92.38|101.2|103.25|94.84|85.3|82.19|89.81|98.81|83.03|68.91|65.24|70.31|63.49|86.38|71.07|62.65|63.95|66.98|58.06|68.28|62.11|63.86|67.4|68.69|69.57|68.03|74.23|75.66|70.67|81.08|81.31|82.49|88.15|95.99|92.97|100.01|113.18|122.37|110.99|122.19|159.27|147.74|133.96|111.2|115.64|126.55|107.15|102.85|117.06|116.56|109.55|96.82|98.92|96.53|86.0|88.11|92.76|85.77|79.41|93.06|86.96|76.35|72.37|74.19|68.6|67.46|74.47|76.25|66.73|73.18|75.2|88.21|84.93|75.04|71.09|82.6|80.03|76.22|75.76|83.72|75.85|79.36|90.35|86.9|78.24|95.64|97.38|86.41|85.02|91.87|87.36|77.56|81.25|91.66|83.65|77.67|85.07|89.21|92.66|92.46|89.0|100.83|96.71|94.81|101.37|111.28|124.48|119.73|127.81|134.41|132.4|140.32|140.86|166.52|160.16|168.39|176.74|174.63|172.86|168.55|155.9|132.71|113.44|113.49|123.9|151.11|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||\n"
+            + 
+            "\n" + 
+            "";
+    AlignFrame alf_full=new
+     FileLoader(false).LoadFileWaitTillLoaded(FullLengthSeq,DataSourceType.PASTE);
+    alf_full.loadJalviewDataFile(FullLengthAnnot, DataSourceType.PASTE, null, null);
+    AlignmentI al_full = alf_full.getViewport().getAlignment();
+    AlignmentAnnotation fullseq_tf = al_full.findAnnotations(al_full.getSequences().get(0), null, TEMP_FACTOR_AA).iterator()
+            .next();
+    assertNotNull(fullseq_tf);
+    
+    // getMappingFor
+    // AlignmentI al_full=alf_full.getViewport().getAlignment();
+    //
+    // // load 4IM2 (full length, SIFTS onto full alingnment)
+    // SiftsSettings.setMapWithSifts(true);
+    // StructureChooser schoose = new StructureChooser(selectedSeqs_full,
+    // seq_full,
+    // alf_full.getViewport().getAlignPanel());
+    // schoose.selectStructure(PDBID);
+    // schoose.ok_ActionPerformed();
+
+    AlignFrame alf = new FileLoader(false).LoadFileWaitTillLoaded(
+            ">TBK1_HUMAN/470-502 Serine/threonine-protein kinase TBK1\nFCIRNIEKTVKVYEKLMKINLEAAELGEISDIH",
+            DataSourceType.PASTE);
+    Desktop.addInternalFrame(alf, "Foo", 800, 600);
+    ;
+    AlignmentI al = alf.getViewport().getAlignment();
+    SequenceI seq = al.getSequenceAt(0);
+    assertEquals(470, seq.getStart());
+    // load 4IM2 (full length, SIFTS)
+    SiftsSettings.setMapWithSifts(true);
+    StructureImportSettings.setProcessSecondaryStructure(true);
+    StructureImportSettings.setVisibleChainAnnotation(true);
+    JalviewStructureDisplayI sview = openStructureViaChooser(alf, seq,
+            PDBID);
+
+    AlignmentAnnotation subseq_tf=null;
+    assertTrue(seq.getDBRefs() != null && seq.getDBRefs().length > 0);
+    
+    if (!al.findAnnotations(seq, null, TEMP_FACTOR_AA).iterator().hasNext())
+    {
+      // FIXME JAL-2321 - don't see reference annotation on alignment the first
+      // time
+      // around
+      SortedMap<String, String> tipEntries = new TreeMap<>();
+      final Map<SequenceI, List<AlignmentAnnotation>> candidates = new LinkedHashMap<>();
+
+      AlignmentUtils.findAddableReferenceAnnotations(al.getSequences(),
+              tipEntries, candidates, al);
+      AlignmentUtils.addReferenceAnnotations(candidates, al, null);
+
+      if (!al.findAnnotations(seq, null, TEMP_FACTOR_AA).iterator()
+              .hasNext())
+      {
+        Assert.fail(
+                "JAL-2321 or worse has occured. No secondary structure added to alignment.");
+      }
+    }
+    subseq_tf = al.findAnnotations(seq, null, TEMP_FACTOR_AA).iterator()
+            .next();
+    // verify against annotation after loading 4IM2 to full length TBK1_HUMAN
+    // verify location of mapped residues
+    // verify location of secondary structure annotation
+    // Specific positions: LYS477 (h),THR478 (no helix), ... GLY496(no helix),
+    // GLU497 (helix),
+    
+    // check there is or is not a tempfactor for each mapped position, and that
+    // values are equal for those positions.
+    for (int p=seq.getStart();p<=seq.getEnd();p++)
+    {
+      Annotation orig,subseq;
+      orig = fullseq_tf.getAnnotationForPosition(p);
+      subseq = subseq_tf.getAnnotationForPosition(p);
+      if (orig == null)
+      {
+        Assert.assertNull(subseq,
+                "Expected no annotation transferred at position " + p);
+      }
+      ;
+      if (orig != null)
+      {
+        Assert.assertNotNull(subseq,
+                "Expected annotation transfer at position " + p);
+        assertEquals(orig.value, subseq.value);
+      }
+      ;
+
+    }
+  }
+
+  private JalviewStructureDisplayI openStructureViaChooser(AlignFrame alf,
+          SequenceI seq,
+          String pDBID)
+  {
+
+    SequenceI[] selectedSeqs = new SequenceI[] { seq };
+
+    StructureChooser schoose = new StructureChooser(selectedSeqs, seq,
+            alf.getViewport().getAlignPanel());
+
+    try
+    {
+      Thread.sleep(5000);
+    } catch (InterruptedException q)
+    {
+    }
+    ;
+    Assert.assertTrue(schoose.selectStructure(pDBID),
+            "Couldn't select structure via structure chooser: " + pDBID);
+    schoose.showStructures(true);
+    return schoose.getOpenedStructureViewer();
+  }
+
 }
index aea3687..af02d5e 100644 (file)
@@ -275,11 +275,11 @@ public class AAStructureBindingModelTest
     StructureSelectionManager ssm = new StructureSelectionManager();
 
     ssm.setMapping(new SequenceI[] { seq1a, seq1b }, null, PDB_1,
-            DataSourceType.PASTE);
+            DataSourceType.PASTE, null);
     ssm.setMapping(new SequenceI[] { seq2 }, null, PDB_2,
-            DataSourceType.PASTE);
+            DataSourceType.PASTE, null);
     ssm.setMapping(new SequenceI[] { seq3 }, null, PDB_3,
-            DataSourceType.PASTE);
+            DataSourceType.PASTE, null);
 
     testee = new AAStructureBindingModel(ssm, pdbFiles, seqs, null)
     {
index a2f38e2..1acc9e1 100644 (file)
@@ -391,7 +391,9 @@ public class MapListTest
     MapList ml7 = new MapList(codons, protein, 3, 1); // toShifts differ
 
     assertTrue(ml.equals(ml));
+    assertEquals(ml.hashCode(), ml.hashCode());
     assertTrue(ml.equals(ml1));
+    assertEquals(ml.hashCode(), ml1.hashCode());
     assertTrue(ml1.equals(ml));
 
     assertFalse(ml.equals(null));
@@ -426,7 +428,7 @@ public class MapListTest
   @Test(groups = { "Functional" })
   public void testGetRanges()
   {
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     ranges.add(new int[] { 2, 3 });
     ranges.add(new int[] { 5, 6 });
     assertEquals("[2, 3, 5, 6]", Arrays.toString(MapList.getRanges(ranges)));
@@ -603,7 +605,7 @@ public class MapListTest
   public void testAddRange()
   {
     int[] range = { 1, 5 };
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
 
     // add to empty list:
     MapList.addRange(range, ranges);
@@ -702,7 +704,7 @@ public class MapListTest
   public void testCoalesceRanges()
   {
     assertNull(MapList.coalesceRanges(null));
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     assertSame(ranges, MapList.coalesceRanges(ranges));
     ranges.add(new int[] { 1, 3 });
     assertSame(ranges, MapList.coalesceRanges(ranges));
@@ -763,7 +765,7 @@ public class MapListTest
   @Test(groups = { "Functional" })
   public void testCoalesceRanges_withOverlap()
   {
-    List<int[]> ranges = new ArrayList<int[]>();
+    List<int[]> ranges = new ArrayList<>();
     ranges.add(new int[] { 1, 3 });
     ranges.add(new int[] { 2, 5 });
 
@@ -814,4 +816,155 @@ public class MapListTest
     assertEquals(1, merged.size());
     assertArrayEquals(new int[] { 9, 0 }, merged.get(0));
   }
+
+  /**
+   * Test the method that compounds ('traverses') two mappings
+   */
+  @Test(groups = "Functional")
+  public void testTraverse()
+  {
+    /*
+     * simple 1:1 plus 1:1 forwards
+     */
+    MapList ml1 = new MapList(new int[] { 3, 4, 8, 12 }, new int[] { 5, 8,
+        11, 13 }, 1, 1);
+    MapList ml2 = new MapList(new int[] { 1, 50 }, new int[] { 40, 45, 70,
+        75, 90, 127 }, 1, 1);
+    MapList compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 1);
+    assertEquals(compound.getToRatio(), 1);
+    List<int[]> fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 2);
+    assertArrayEquals(new int[] { 3, 4 }, fromRanges.get(0));
+    assertArrayEquals(new int[] { 8, 12 }, fromRanges.get(1));
+    List<int[]> toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 2);
+    // 5-8 maps to 44-45,70-71
+    // 11-13 maps to 74-75,90
+    assertArrayEquals(new int[] { 44, 45, 70, 71 }, toRanges.get(0));
+    assertArrayEquals(new int[] { 74, 75, 90, 90 }, toRanges.get(1));
+
+    /*
+     * 1:1 over 1:1 backwards ('reverse strand')
+     */
+    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 70, 119 }, 1, 1);
+    ml2 = new MapList(new int[] { 1, 500 },
+            new int[] { 1000, 901, 600, 201 }, 1, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 1);
+    assertEquals(compound.getToRatio(), 1);
+    fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 1);
+    assertArrayEquals(new int[] { 1, 50 }, fromRanges.get(0));
+    toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 1);
+    assertArrayEquals(new int[] { 931, 901, 600, 582 }, toRanges.get(0));
+
+    /*
+     * 1:1 plus 1:3 should result in 1:3
+     */
+    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 40 }, 1, 1);
+    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 50, 91, 340 },
+            1, 3);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 1);
+    assertEquals(compound.getToRatio(), 3);
+    fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 1);
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-40 maps to 31-50,91-160
+    toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 1);
+    assertArrayEquals(new int[] { 31, 50, 91, 160 }, toRanges.get(0));
+
+    /*
+     * 3:1 plus 1:1 should result in 3:1
+     */
+    ml1 = new MapList(new int[] { 1, 30 }, new int[] { 11, 20 }, 3, 1);
+    ml2 = new MapList(new int[] { 1, 100 }, new int[] { 1, 15, 91, 175 },
+            1, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 3);
+    assertEquals(compound.getToRatio(), 1);
+    fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 1);
+    assertArrayEquals(new int[] { 1, 30 }, fromRanges.get(0));
+    // 11-20 maps to 11-15, 91-95
+    toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 1);
+    assertArrayEquals(new int[] { 11, 15, 91, 95 }, toRanges.get(0));
+
+    /*
+     * 1:3 plus 3:1 should result in 1:1
+     */
+    ml1 = new MapList(new int[] { 21, 40 }, new int[] { 13, 72 }, 1, 3);
+    ml2 = new MapList(new int[] { 1, 300 }, new int[] { 51, 70, 121, 200 },
+            3, 1);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 1);
+    assertEquals(compound.getToRatio(), 1);
+    fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 1);
+    assertArrayEquals(new int[] { 21, 40 }, fromRanges.get(0));
+    // 13-72 maps 3:1 to 55-70, 121-124
+    toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 1);
+    assertArrayEquals(new int[] { 55, 70, 121, 124 }, toRanges.get(0));
+
+    /*
+     * 3:1 plus 1:3 should result in 1:1
+     */
+    ml1 = new MapList(new int[] { 31, 90 }, new int[] { 13, 32 }, 3, 1);
+    ml2 = new MapList(new int[] { 11, 40 }, new int[] { 41, 50, 71, 150 },
+            1, 3);
+    compound = ml1.traverse(ml2);
+
+    assertEquals(compound.getFromRatio(), 1);
+    assertEquals(compound.getToRatio(), 1);
+    fromRanges = compound.getFromRanges();
+    assertEquals(fromRanges.size(), 1);
+    assertArrayEquals(new int[] { 31, 90 }, fromRanges.get(0));
+    // 13-32 maps to 47-50,71-126
+    toRanges = compound.getToRanges();
+    assertEquals(toRanges.size(), 1);
+    assertArrayEquals(new int[] { 47, 50, 71, 126 }, toRanges.get(0));
+
+    /*
+     * method returns null if not all regions are mapped through
+     */
+    ml1 = new MapList(new int[] { 1, 50 }, new int[] { 101, 150 }, 1, 1);
+    ml2 = new MapList(new int[] { 131, 180 }, new int[] { 201, 250 }, 1, 3);
+    compound = ml1.traverse(ml2);
+    assertNull(compound);
+  }
+
+  /**
+   * Test that method that inspects for the (first) forward or reverse 'to' range.
+   * Single position ranges are ignored.
+   */
+  @Test(groups = { "Functional" })
+  public void testIsToForwardsStrand()
+  {
+    // [3-9] declares forward strand
+    MapList ml = new MapList(new int[] { 20, 11 },
+            new int[]
+            { 2, 2, 3, 9, 12, 11 }, 1, 1);
+    assertTrue(ml.isToForwardStrand());
+
+    // [11-5] declares reverse strand ([13-14] is ignored)
+    ml = new MapList(new int[] { 20, 11 },
+            new int[]
+            { 2, 2, 11, 5, 13, 14 }, 1, 1);
+    assertFalse(ml.isToForwardStrand());
+
+    // all single position ranges - defaults to forward strand
+    ml = new MapList(new int[] { 3, 1 }, new int[] { 2, 2, 4, 4, 6, 6 }, 1,
+            1);
+    assertTrue(ml.isToForwardStrand());
+  }
 }
index d0ec3e8..097ccd4 100644 (file)
@@ -50,6 +50,7 @@ import java.awt.Color;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -913,9 +914,9 @@ public class MappingUtilsTest
     MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
             proteinView, dnaView, dnaSelection, dnaHidden);
     assertEquals("[]", dnaSelection.getSelected().toString());
-    List<int[]> hidden = dnaHidden.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[0, 4]", Arrays.toString(hidden.get(0)));
+    Iterator<int[]> regions = dnaHidden.iterator();
+    assertEquals(1, dnaHidden.getNumberOfRegions());
+    assertEquals("[0, 4]", Arrays.toString(regions.next()));
 
     /*
      * Column 1 in protein picks up Seq1/K which maps to cols 0-3 in dna
@@ -930,9 +931,9 @@ public class MappingUtilsTest
     proteinSelection.hideSelectedColumns(1, hiddenCols);
     MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
             proteinView, dnaView, dnaSelection, dnaHidden);
-    hidden = dnaHidden.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
+    regions = dnaHidden.iterator();
+    assertEquals(1, dnaHidden.getNumberOfRegions());
+    assertEquals("[0, 3]", Arrays.toString(regions.next()));
 
     /*
      * Column 2 in protein picks up gaps only - no mapping
@@ -944,7 +945,7 @@ public class MappingUtilsTest
     proteinSelection.hideSelectedColumns(2, hiddenCols);
     MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
             proteinView, dnaView, dnaSelection, dnaHidden);
-    assertTrue(dnaHidden.getHiddenColumnsCopy().isEmpty());
+    assertEquals(0, dnaHidden.getNumberOfRegions());
 
     /*
      * Column 3 in protein picks up Seq1/P, Seq2/Q, Seq3/S which map to columns
@@ -959,9 +960,9 @@ public class MappingUtilsTest
     MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
             proteinView, dnaView, dnaSelection, dnaHidden);
     assertEquals("[0, 1, 2, 3]", dnaSelection.getSelected().toString());
-    hidden = dnaHidden.getHiddenColumnsCopy();
-    assertEquals(1, hidden.size());
-    assertEquals("[5, 10]", Arrays.toString(hidden.get(0)));
+    regions = dnaHidden.iterator();
+    assertEquals(1, dnaHidden.getNumberOfRegions());
+    assertEquals("[5, 10]", Arrays.toString(regions.next()));
 
     /*
      * Combine hiding columns 1 and 3 to get discontiguous hidden columns
@@ -974,10 +975,10 @@ public class MappingUtilsTest
     proteinSelection.hideSelectedColumns(3, hiddenCols);
     MappingUtils.mapColumnSelection(proteinSelection, hiddenCols,
             proteinView, dnaView, dnaSelection, dnaHidden);
-    hidden = dnaHidden.getHiddenColumnsCopy();
-    assertEquals(2, hidden.size());
-    assertEquals("[0, 3]", Arrays.toString(hidden.get(0)));
-    assertEquals("[5, 10]", Arrays.toString(hidden.get(1)));
+    regions = dnaHidden.iterator();
+    assertEquals(2, dnaHidden.getNumberOfRegions());
+    assertEquals("[0, 3]", Arrays.toString(regions.next()));
+    assertEquals("[5, 10]", Arrays.toString(regions.next()));
   }
 
   @Test(groups = { "Functional" })
@@ -1149,4 +1150,138 @@ public class MappingUtilsTest
     assertEquals("[12, 11, 8, 4]", Arrays.toString(ranges));
   }
 
+  @Test(groups = { "Functional" })
+  public void testRangeContains()
+  {
+    /*
+     * both forward ranges
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        4, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        0, 9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        -10, -9 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        1, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 12 }));
+
+    /*
+     * forward range, reverse query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        5, 5 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, new int[] {
+        10, 0 }));
+
+    /*
+     * reverse range, forward query
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        1, 9 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        2, 10 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 6 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        6, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 20 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -3, -2 }));
+
+    /*
+     * both reverse
+     */
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        9, 1 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 2 }));
+    assertTrue(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        3, 3 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        11, 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        10, 0 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        12, 11 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 10, 1 }, new int[] {
+        -5, -8 }));
+
+    /*
+     * bad arguments
+     */
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10, 12 },
+            new int[] {
+        1, 10 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 },
+            new int[] { 1 }));
+    assertFalse(MappingUtils.rangeContains(new int[] { 1, 10 }, null));
+    assertFalse(MappingUtils.rangeContains(null, new int[] { 1, 10 }));
+  }
+
+  @Test(groups = "Functional")
+  public void testRemoveEndPositions()
+  {
+    List<int[]> ranges = new ArrayList<>();
+
+    /*
+     * case 1: truncate last range
+     */
+    ranges.add(new int[] { 1, 10 });
+    ranges.add(new int[] { 20, 30 });
+    MappingUtils.removeEndPositions(5, ranges);
+    assertEquals(2, ranges.size());
+    assertEquals(25, ranges.get(1)[1]);
+
+    /*
+     * case 2: remove last range
+     */
+    ranges.clear();
+    ranges.add(new int[] { 1, 10 });
+    ranges.add(new int[] { 20, 22 });
+    MappingUtils.removeEndPositions(3, ranges);
+    assertEquals(1, ranges.size());
+    assertEquals(10, ranges.get(0)[1]);
+
+    /*
+     * case 3: truncate penultimate range
+     */
+    ranges.clear();
+    ranges.add(new int[] { 1, 10 });
+    ranges.add(new int[] { 20, 21 });
+    MappingUtils.removeEndPositions(3, ranges);
+    assertEquals(1, ranges.size());
+    assertEquals(9, ranges.get(0)[1]);
+
+    /*
+     * case 4: remove last two ranges
+     */
+    ranges.clear();
+    ranges.add(new int[] { 1, 10 });
+    ranges.add(new int[] { 20, 20 });
+    ranges.add(new int[] { 30, 30 });
+    MappingUtils.removeEndPositions(3, ranges);
+    assertEquals(1, ranges.size());
+    assertEquals(9, ranges.get(0)[1]);
+  }
 }
diff --git a/test/jalview/util/MathUtilsTest.java b/test/jalview/util/MathUtilsTest.java
new file mode 100644 (file)
index 0000000..dc23472
--- /dev/null
@@ -0,0 +1,26 @@
+package jalview.util;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class MathUtilsTest
+{
+  @Test(groups = "Functional")
+  public void testGcd()
+  {
+    assertEquals(MathUtils.gcd(0, 0), 0);
+    assertEquals(MathUtils.gcd(0, 1), 1);
+    assertEquals(MathUtils.gcd(1, 0), 1);
+    assertEquals(MathUtils.gcd(1, 1), 1);
+    assertEquals(MathUtils.gcd(1, -1), 1);
+    assertEquals(MathUtils.gcd(-1, 1), 1);
+    assertEquals(MathUtils.gcd(2, 3), 1);
+    assertEquals(MathUtils.gcd(4, 2), 2);
+    assertEquals(MathUtils.gcd(2, 4), 2);
+    assertEquals(MathUtils.gcd(2, -4), 2);
+    assertEquals(MathUtils.gcd(-2, 4), 2);
+    assertEquals(MathUtils.gcd(-2, -4), 2);
+    assertEquals(MathUtils.gcd(2 * 3 * 5 * 7 * 11, 3 * 7 * 13 * 17), 3 * 7);
+  }
+}
index b6f8a25..084219a 100644 (file)
@@ -228,4 +228,26 @@ public class StringUtilsTest
     assertEquals("", StringUtils.toSentenceCase(""));
     assertNull(StringUtils.toSentenceCase(null));
   }
+
+  @Test(groups = { "Functional" })
+  public void testStripHtmlTags()
+  {
+    assertNull(StringUtils.stripHtmlTags(null));
+    assertEquals("", StringUtils.stripHtmlTags(""));
+    assertEquals(
+            "<a href=\"something\">label</href>",
+            StringUtils
+                    .stripHtmlTags("<html><a href=\"something\">label</href></html>"));
+
+    // if no "<html>" tag, < and > get html-encoded (not sure why)
+    assertEquals("&lt;a href=\"something\"&gt;label&lt;/href&gt;",
+            StringUtils.stripHtmlTags("<a href=\"something\">label</href>"));
+
+    // </body> gets removed but not <body> (is this intentional?)
+    assertEquals("<body><p>hello",
+            StringUtils.stripHtmlTags("<html><body><p>hello</body></html>"));
+
+    assertEquals("kdHydro &lt; 12.53",
+            StringUtils.stripHtmlTags("kdHydro < 12.53"));
+  }
 }
diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java
new file mode 100644 (file)
index 0000000..2a12534
--- /dev/null
@@ -0,0 +1,76 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+public class ConditionTest
+{
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.UK);
+    assertEquals(Condition.Contains.toString(), "Contains");
+    assertEquals(Condition.NotContains.toString(), "Does not contain");
+    assertEquals(Condition.Matches.toString(), "Matches");
+    assertEquals(Condition.NotMatches.toString(), "Does not match");
+    assertEquals(Condition.Present.toString(), "Is present");
+    assertEquals(Condition.NotPresent.toString(), "Is not present");
+    assertEquals(Condition.LT.toString(), "<");
+    assertEquals(Condition.LE.toString(), "<=");
+    assertEquals(Condition.GT.toString(), ">");
+    assertEquals(Condition.GE.toString(), ">=");
+    assertEquals(Condition.EQ.toString(), "=");
+    assertEquals(Condition.NE.toString(), "not =");
+
+    /*
+     * repeat call to get coverage of value caching
+     */
+    assertEquals(Condition.NE.toString(), "not =");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetStableName()
+  {
+    assertEquals(Condition.Contains.getStableName(), "Contains");
+    assertEquals(Condition.NotContains.getStableName(), "NotContains");
+    assertEquals(Condition.Matches.getStableName(), "Matches");
+    assertEquals(Condition.NotMatches.getStableName(), "NotMatches");
+    assertEquals(Condition.Present.getStableName(), "Present");
+    assertEquals(Condition.NotPresent.getStableName(), "NotPresent");
+    assertEquals(Condition.LT.getStableName(), "LT");
+    assertEquals(Condition.LE.getStableName(), "LE");
+    assertEquals(Condition.GT.getStableName(), "GT");
+    assertEquals(Condition.GE.getStableName(), "GE");
+    assertEquals(Condition.EQ.getStableName(), "EQ");
+    assertEquals(Condition.NE.getStableName(), "NE");
+  }
+
+  @Test(groups = "Functional")
+  public void testFromString()
+  {
+    assertEquals(Condition.fromString("Contains"), Condition.Contains);
+    // not case sensitive
+    assertEquals(Condition.fromString("contains"), Condition.Contains);
+    assertEquals(Condition.fromString("CONTAINS"), Condition.Contains);
+    assertEquals(Condition.fromString("NotContains"),
+            Condition.NotContains);
+    assertEquals(Condition.fromString("Matches"), Condition.Matches);
+    assertEquals(Condition.fromString("NotMatches"), Condition.NotMatches);
+    assertEquals(Condition.fromString("Present"), Condition.Present);
+    assertEquals(Condition.fromString("NotPresent"), Condition.NotPresent);
+    assertEquals(Condition.fromString("LT"), Condition.LT);
+    assertEquals(Condition.fromString("LE"), Condition.LE);
+    assertEquals(Condition.fromString("GT"), Condition.GT);
+    assertEquals(Condition.fromString("GE"), Condition.GE);
+    assertEquals(Condition.fromString("EQ"), Condition.EQ);
+    assertEquals(Condition.fromString("NE"), Condition.NE);
+
+    assertNull(Condition.fromString("Equals"));
+    assertNull(Condition.fromString(""));
+    assertNull(Condition.fromString(null));
+  }
+}
diff --git a/test/jalview/util/matcher/MatcherTest.java b/test/jalview/util/matcher/MatcherTest.java
new file mode 100644 (file)
index 0000000..a47fb60
--- /dev/null
@@ -0,0 +1,273 @@
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Locale;
+
+import org.testng.annotations.Test;
+
+import junit.extensions.PA;
+
+public class MatcherTest
+{
+  @Test(groups = "Functional")
+  public void testConstructor()
+  {
+    MatcherI m = new Matcher(Condition.Contains, "foo");
+    assertEquals(m.getCondition(), Condition.Contains);
+    assertEquals(m.getPattern(), "foo");
+    assertEquals(PA.getValue(m, "uppercasePattern"), "FOO");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GT, -2.1f);
+    assertEquals(m.getCondition(), Condition.GT);
+    assertEquals(m.getPattern(), "-2.1");
+    assertEquals(m.getFloatValue(), -2.1f);
+
+    m = new Matcher(Condition.NotContains, "-1.2f");
+    assertEquals(m.getCondition(), Condition.NotContains);
+    assertEquals(m.getPattern(), "-1.2f");
+    assertEquals(m.getFloatValue(), 0f);
+
+    m = new Matcher(Condition.GE, "-1.2f");
+    assertEquals(m.getCondition(), Condition.GE);
+    assertEquals(m.getPattern(), "-1.2");
+    assertEquals(m.getFloatValue(), -1.2f);
+
+    try
+    {
+      new Matcher(null, 0f);
+      fail("Expected exception");
+    } catch (NullPointerException e)
+    {
+      // expected
+    }
+
+    try
+    {
+      new Matcher(Condition.LT, "123,456");
+      fail("Expected exception");
+    } catch (NumberFormatException e)
+    {
+      // expected
+    }
+  }
+
+  /**
+   * Tests for float comparison conditions
+   */
+  @Test(groups = "Functional")
+  public void testMatches_float()
+  {
+    /*
+     * EQUALS test
+     */
+    MatcherI m = new Matcher(Condition.EQ, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.0"));
+    assertFalse(m.matches("2.01"));
+
+    /*
+     * NOT EQUALS test
+     */
+    m = new Matcher(Condition.NE, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.0"));
+    assertTrue(m.matches("2.01"));
+
+    /*
+     * >= test
+     */
+    m = new Matcher(Condition.GE, 2f);
+    assertTrue(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * > test
+     */
+    m = new Matcher(Condition.GT, 2f);
+    assertFalse(m.matches("2"));
+    assertTrue(m.matches("2.1"));
+    assertFalse(m.matches("1.9"));
+
+    /*
+     * <= test
+     */
+    m = new Matcher(Condition.LE, 2f);
+    assertTrue(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+
+    /*
+     * < test
+     */
+    m = new Matcher(Condition.LT, 2f);
+    assertFalse(m.matches("2"));
+    assertFalse(m.matches("2.1"));
+    assertTrue(m.matches("1.9"));
+  }
+
+  @Test(groups = "Functional")
+  public void testMatches_floatNullOrInvalid()
+  {
+    for (Condition cond : Condition.values())
+    {
+      if (cond.isNumeric())
+      {
+        MatcherI m = new Matcher(cond, 2f);
+        assertFalse(m.matches(null));
+        assertFalse(m.matches(""));
+        assertFalse(m.matches("two"));
+      }
+    }
+  }
+
+  /**
+   * Tests for string comparison conditions
+   */
+  @Test(groups = "Functional")
+  public void testMatches_pattern()
+  {
+    /*
+     * Contains
+     */
+    MatcherI m = new Matcher(Condition.Contains, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not contain
+     */
+    m = new Matcher(Condition.NotContains, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null)); // null value passes this condition
+
+    /*
+     * matches
+     */
+    m = new Matcher(Condition.Matches, "benign");
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches(" Benign ")); // trim before testing
+    assertFalse(m.matches("MOSTLY BENIGN"));
+    assertFalse(m.matches("pathogenic"));
+    assertFalse(m.matches(null));
+
+    /*
+     * does not match
+     */
+    m = new Matcher(Condition.NotMatches, "benign");
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches(" Benign ")); // trim before testing
+    assertTrue(m.matches("MOSTLY BENIGN"));
+    assertTrue(m.matches("pathogenic"));
+    assertTrue(m.matches(null));
+
+    /*
+     * value is present (is not null)
+     */
+    m = new Matcher(Condition.Present, null);
+    assertTrue(m.matches("benign"));
+    assertTrue(m.matches(""));
+    assertFalse(m.matches(null));
+
+    /*
+     * value is not present (is null)
+     */
+    m = new Matcher(Condition.NotPresent, null);
+    assertFalse(m.matches("benign"));
+    assertFalse(m.matches(""));
+    assertTrue(m.matches(null));
+
+    /*
+     * a float with a string match condition will be treated as string
+     */
+    Matcher m1 = new Matcher(Condition.Contains, "32");
+    assertFalse(m1.matches(-203f));
+    assertTrue(m1.matches(-4321.0f));
+  }
+
+  /**
+   * If a float is passed with a string condition it gets converted to a string
+   */
+  @Test(groups = "Functional")
+  public void testMatches_floatWithStringCondition()
+  {
+    MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+    assertTrue(m.matches("1.2e-6"));
+
+    m = new Matcher(Condition.Contains, 0.0000001f);
+    assertTrue(m.matches("1.0e-7"));
+    assertTrue(m.matches("1.0E-7"));
+    assertFalse(m.matches("0.0000001f"));
+  }
+
+  @Test(groups = "Functional")
+  public void testToString()
+  {
+    Locale.setDefault(Locale.ENGLISH);
+
+    MatcherI m = new Matcher(Condition.LT, 1.2e-6f);
+    assertEquals(m.toString(), "< 1.2E-6");
+
+    m = new Matcher(Condition.NotMatches, "ABC");
+    assertEquals(m.toString(), "Does not match 'ABC'");
+
+    m = new Matcher(Condition.Contains, -1.2f);
+    assertEquals(m.toString(), "Contains '-1.2'");
+  }
+
+  @Test(groups = "Functional")
+  public void testEquals()
+  {
+    /*
+     * string condition
+     */
+    MatcherI m = new Matcher(Condition.NotMatches, "ABC");
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "ABC")));
+    // not case-sensitive:
+    assertTrue(m.equals(new Matcher(Condition.NotMatches, "abc")));
+    assertFalse(m.equals(new Matcher(Condition.Matches, "ABC")));
+    assertFalse(m.equals(new Matcher(Condition.NotMatches, "def")));
+
+    /*
+     * numeric conditions
+     */
+    m = new Matcher(Condition.LT, -1f);
+    assertFalse(m.equals(null));
+    assertFalse(m.equals("foo"));
+    assertTrue(m.equals(m));
+    assertTrue(m.equals(new Matcher(Condition.LT, -1f)));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1f")));
+    assertTrue(m.equals(new Matcher(Condition.LT, "-1.00f")));
+    assertFalse(m.equals(new Matcher(Condition.LE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.GE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.NE, -1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, 1f)));
+    assertFalse(m.equals(new Matcher(Condition.LT, -1.1f)));
+  }
+
+  @Test(groups = "Functional")
+  public void testHashCode()
+  {
+    MatcherI m1 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m2 = new Matcher(Condition.NotMatches, "ABC");
+    MatcherI m3 = new Matcher(Condition.NotMatches, "AB");
+    MatcherI m4 = new Matcher(Condition.Matches, "ABC");
+    assertEquals(m1.hashCode(), m2.hashCode());
+    assertNotEquals(m1.hashCode(), m3.hashCode());
+    assertNotEquals(m1.hashCode(), m4.hashCode());
+    assertNotEquals(m3.hashCode(), m4.hashCode());
+  }
+}
index 851b1b7..41a313f 100644 (file)
@@ -85,7 +85,6 @@ public class ViewportRangesTest {
     vr.setEndSeq(al.getHeight());
     assertEquals(vr.getEndSeq(), al.getHeight() - 1);
 
-    // vr.setEndRes(al.getHeight() - 1);
     vr.setEndSeq(al.getHeight() - 1);
     assertEquals(vr.getEndSeq(), al.getHeight() - 1);
   }
@@ -169,6 +168,24 @@ public class ViewportRangesTest {
   }
 
   @Test(groups = { "Functional" })
+  public void testSetStartResAndSeq()
+  {
+    ViewportRanges vr = new ViewportRanges(al);
+    vr.setViewportHeight(10);
+    vr.setStartResAndSeq(3, 6);
+    assertEquals(vr.getStartRes(), 3);
+    assertEquals(vr.getStartSeq(), 6);
+    assertEquals(vr.getEndRes(), 3 + vr.getViewportWidth() - 1);
+    assertEquals(vr.getEndSeq(), 6 + vr.getViewportHeight() - 1);
+
+    vr.setStartResAndSeq(10, 25);
+    assertEquals(vr.getStartRes(), 10);
+    assertEquals(vr.getStartSeq(), 19);
+    assertEquals(vr.getEndRes(), 10 + vr.getViewportWidth() - 1);
+    assertEquals(vr.getEndSeq(), 19 + vr.getViewportHeight() - 1);
+  }
+
+  @Test(groups = { "Functional" })
   public void testSetViewportHeight()
   {
     ViewportRanges vr = new ViewportRanges(al);
@@ -385,14 +402,13 @@ public class ViewportRangesTest {
     assertEquals(vr.getEndRes(), 52);
   }
 
-  // leave until JAL-2388 is merged and we can do without viewport
-  /*@Test(groups = { "Functional" })
+  @Test(groups = { "Functional" })
   public void testScrollToVisible()
   {
     ViewportRanges vr = new ViewportRanges(al);
     vr.setViewportStartAndWidth(12,5);
     vr.setViewportStartAndHeight(10,6);
-    vr.scrollToVisible(13,14)
+    vr.scrollToVisible(13, 14);
     
     // no change
     assertEquals(vr.getStartRes(), 12);
@@ -403,7 +419,15 @@ public class ViewportRangesTest {
     assertEquals(vr.getStartSeq(), 6);
     
     // test for hidden columns too
-  }*/
+    al.getHiddenColumns().hideColumns(1, 3);
+    vr.scrollToVisible(13, 3);
+    assertEquals(vr.getStartRes(), 6);
+    assertEquals(vr.getStartSeq(), 3);
+
+    vr.scrollToVisible(2, 9);
+    assertEquals(vr.getStartRes(), 0);
+    assertEquals(vr.getStartSeq(), 4);
+  }
 
   @Test(groups = { "Functional" })
   public void testEventFiring()
@@ -418,7 +442,7 @@ public class ViewportRangesTest {
 
     // one event fired when startRes is called with new value
     vr.setStartRes(4);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
     l.reset();
 
     // no event fired for same value
@@ -427,7 +451,7 @@ public class ViewportRangesTest {
     l.reset();
 
     vr.setStartSeq(4);
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.setStartSeq(4);
@@ -435,7 +459,7 @@ public class ViewportRangesTest {
     l.reset();
 
     vr.setEndSeq(10);
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.setEndSeq(10);
@@ -443,7 +467,7 @@ public class ViewportRangesTest {
     l.reset();
 
     vr.setStartEndRes(2, 15);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
     l.reset();
 
     vr.setStartEndRes(2, 15);
@@ -452,16 +476,18 @@ public class ViewportRangesTest {
 
     // check new value fired by event is corrected startres
     vr.setStartEndRes(-1, 5);
-    assertTrue(l.verify(1, Arrays.asList("startres"), Arrays.asList(0)));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES),
+            Arrays.asList(0)));
     l.reset();
 
     // check new value fired by event is corrected endres
     vr.setStartEndRes(0, -1);
-    assertTrue(l.verify(1, Arrays.asList("endres"), Arrays.asList(0)));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.ENDRES),
+            Arrays.asList(0)));
     l.reset();
 
     vr.setStartEndSeq(2, 15);
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.setStartEndSeq(2, 15);
@@ -474,12 +500,14 @@ public class ViewportRangesTest {
 
     // check new value fired by event is corrected startseq
     vr.setStartEndSeq(-1, 5);
-    assertTrue(l.verify(1, Arrays.asList("startseq"), Arrays.asList(0)));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ),
+            Arrays.asList(0)));
     l.reset();
 
     // check new value fired by event is corrected endseq
     vr.setStartEndSeq(0, -1);
-    assertTrue(l.verify(1, Arrays.asList("endseq"), Arrays.asList(0)));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.ENDSEQ),
+            Arrays.asList(0)));
     l.reset();
 
     // reset for later tests
@@ -488,51 +516,52 @@ public class ViewportRangesTest {
 
     // test viewport height and width setting triggers event
     vr.setViewportHeight(10);
-    assertTrue(l.verify(1, Arrays.asList("endseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.ENDSEQ)));
     l.reset();
 
     vr.setViewportWidth(18);
-    assertTrue(l.verify(1, Arrays.asList("endres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.ENDRES)));
     l.reset();
 
     // already has seq start set to 2, so triggers endseq
     vr.setViewportStartAndHeight(2, 16);
-    assertTrue(l.verify(1, Arrays.asList("endseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.ENDSEQ)));
     l.reset();
 
     vr.setViewportStartAndWidth(1, 14);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
     l.reset();
 
     // test page up/down triggers event
     vr.pageUp();
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.pageDown();
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     // test scrolling triggers event
     vr.scrollUp(true);
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.scrollUp(false);
-    assertTrue(l.verify(1, Arrays.asList("startseq")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTSEQ)));
     l.reset();
 
     vr.scrollRight(true);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
     l.reset();
 
     vr.scrollRight(false);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
     l.reset();
 
     vr.scrollToVisible(10, 10);
     assertTrue(l.verify(4,
-            Arrays.asList("startseq", "startseq", "startseq", "startseq")));
+            Arrays.asList(ViewportRanges.STARTSEQ, ViewportRanges.STARTSEQ,
+                    ViewportRanges.STARTSEQ, ViewportRanges.STARTSEQ)));
     l.reset();
 
     /*
@@ -544,7 +573,15 @@ public class ViewportRangesTest {
     l.reset();
 
     vr.scrollToWrappedVisible(25);
-    assertTrue(l.verify(1, Arrays.asList("startres")));
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRES)));
+    l.reset();
+
+    // test setStartResAndSeq triggers one event
+    vr.setStartResAndSeq(5, 7);
+    assertTrue(l.verify(1, Arrays.asList(ViewportRanges.STARTRESANDSEQ),
+            Arrays.asList(5, 7)));
+
+    l.reset();
   }
 
   @Test(groups = { "Functional" })
@@ -763,6 +800,136 @@ public class ViewportRangesTest {
       }
     }
   }
+
+  @Test(groups = { "Functional" })
+  public void testScrollUp_wrapped()
+  {
+    /*
+     * alignment 30 tall and 45 wide
+     */
+    AlignmentI al2 = gen.generate(45, 30, 1, 0, 5);
+
+    /*
+     * wrapped view, 5 sequences high, start at sequence offset 1
+     */
+    ViewportRanges vr = new ViewportRanges(al2);
+    vr.setWrappedMode(true);
+    vr.setViewportStartAndHeight(1, 5);
+
+    /*
+     * offset wrapped view to column 3
+     */
+    vr.setStartEndRes(3, 22);
+
+    int startRes = vr.getStartRes();
+    int width = vr.getViewportWidth();
+    assertEquals(startRes, 3);
+    assertEquals(width, 20);
+
+    // in wrapped mode, we change startRes but not startSeq
+    // scroll down:
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 23);
+
+    // scroll up returns to original position
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 3);
+
+    // scroll up again returns to 'origin'
+    vr.scrollUp(true);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 0);
+
+    /*
+     * offset 3 columns once more and do some scroll downs
+     */
+    vr.setStartEndRes(3, 22);
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 23);
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 43);
+
+    /*
+     * scroll down beyond end of alignment does nothing
+     */
+    vr.scrollUp(false);
+    assertEquals(vr.getStartSeq(), 1);
+    assertEquals(vr.getStartRes(), 43);
+  }
+
+  @Test(groups = { "Functional" })
+  public void testSetViewportLocation()
+  {
+    AlignmentI al2 = gen.generate(60, 80, 1, 0, 0);
+
+    ViewportRanges vr = new ViewportRanges(al2);
+
+    // start with viewport on 5-14
+    vr.setViewportStartAndWidth(5, 10);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getEndRes(), 14);
+
+    vr.setViewportStartAndHeight(3, 13);
+    assertEquals(vr.getStartSeq(), 3);
+    assertEquals(vr.getEndSeq(), 15);
+
+    // set location to (8,5) - no change
+    vr.setViewportLocation(8, 5);
+    assertEquals(vr.getStartRes(), 5);
+    assertEquals(vr.getEndRes(), 14);
+    assertEquals(vr.getStartSeq(), 3);
+    assertEquals(vr.getEndSeq(), 15);
+
+    // set location to (40,50) - change to top left (40,50)
+    vr.setViewportLocation(40, 50);
+    assertEquals(vr.getStartRes(), 40);
+    assertEquals(vr.getEndRes(), 49);
+    assertEquals(vr.getStartSeq(), 50);
+    assertEquals(vr.getEndSeq(), 62);
+
+    // set location past end of alignment - resets to leftmost pos
+    vr.setViewportLocation(63, 85);
+    assertEquals(vr.getStartRes(), 50);
+    assertEquals(vr.getEndRes(), 59);
+    assertEquals(vr.getStartSeq(), 67);
+    assertEquals(vr.getEndSeq(), 79);
+
+    // hide some columns
+    al2.getHiddenColumns().hideColumns(20, 50);
+    vr.setViewportLocation(55, 4);
+    assertEquals(vr.getStartRes(), 19);
+    assertEquals(vr.getEndRes(), 28);
+    assertEquals(vr.getStartSeq(), 4);
+    assertEquals(vr.getEndSeq(), 16);
+
+    // hide some sequences
+    al2.getHiddenSequences().hideSequence(al2.getSequenceAt(3));
+    al2.getHiddenSequences().hideSequence(al2.getSequenceAt(4));
+    vr.setViewportLocation(17, 5);
+    assertEquals(vr.getStartRes(), 17);
+    assertEquals(vr.getEndRes(), 26);
+    assertEquals(vr.getStartSeq(), 3);
+    assertEquals(vr.getEndSeq(), 15);
+
+    // set wrapped mode
+    vr.setWrappedMode(true);
+    vr.setViewportLocation(1, 8);
+    assertEquals(vr.getStartRes(), 0);
+    assertEquals(vr.getEndRes(), 9);
+    assertEquals(vr.getStartSeq(), 3);
+    assertEquals(vr.getEndSeq(), 15);
+
+    // try further down the alignment
+    vr.setViewportLocation(57, 5);
+    assertEquals(vr.getStartRes(), 20);
+    assertEquals(vr.getEndRes(), 29);
+    assertEquals(vr.getStartSeq(), 3);
+    assertEquals(vr.getEndSeq(), 15);
+  }
 }
 
 // mock listener for property change events
@@ -784,7 +951,15 @@ class MockPropChangeListener implements ViewportListenerI
   {
     firecount++;
     events.add(evt.getPropertyName());
-    newvalues.add((Integer) evt.getNewValue());
+    if (evt.getPropertyName().equals(ViewportRanges.STARTRESANDSEQ))
+    {
+      newvalues.add(((int[]) evt.getNewValue())[0]);
+      newvalues.add(((int[]) evt.getNewValue())[1]);
+    }
+    else
+    {
+      newvalues.add((Integer) evt.getNewValue());
+    }
   }
 
   public boolean verify(int count, List<String> eventslist,
index 95863e7..cc3dca8 100644 (file)
  */
 package jalview.ws;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.AssertJUnit.assertTrue;
 
 import jalview.bin.Cache;
 import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
 import jalview.gui.JvOptionPane;
 import jalview.structure.StructureImportSettings;
 import jalview.structure.StructureImportSettings.StructureParser;
 import jalview.ws.seqfetcher.DbSourceProxy;
 
+import java.util.Arrays;
 import java.util.List;
 
+import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
@@ -106,23 +110,76 @@ public class PDBSequenceFetcherTest
     testRetrieveProteinSeqFromPDB();
   }
 
+  private class TestRetrieveObject
+  {
+    String id;
+
+    int expectedHeight;
+
+    public TestRetrieveObject(String id, int expectedHeight)
+    {
+      super();
+      this.id = id;
+      this.expectedHeight = expectedHeight;
+    }
+
+  }
+
+  private List<TestRetrieveObject> toRetrieve = Arrays.asList(
+          new TestRetrieveObject("1QIP", 4),
+          new TestRetrieveObject("4IM2", 1));
+
   private void testRetrieveProteinSeqFromPDB() throws Exception
   {
     List<DbSourceProxy> sps = sf.getSourceProxy("PDB");
-    AlignmentI response = sps.get(0).getSequenceRecords("1QIP");
-    assertTrue(response != null);
-    assertTrue(response.getHeight() == 4);
-    for (SequenceI sq : response.getSequences())
+    StringBuilder errors = new StringBuilder();
+    for (TestRetrieveObject str : toRetrieve)
     {
-      assertTrue("No annotation transfered to sequence.",
-              sq.getAnnotation().length > 0);
-      assertTrue("No PDBEntry on sequence.",
-              sq.getAllPDBEntries().size() > 0);
-      org.testng.Assert
-              .assertEquals(sq.getEnd() - sq.getStart() + 1,
-                      sq.getLength(),
-                      "Sequence start/end doesn't match number of residues in sequence");
+      AlignmentI response = sps.get(0).getSequenceRecords(str.id);
+      assertTrue("No aligment for " + str.id, response != null);
+      assertEquals(response.getHeight(), str.expectedHeight,
+              "Number of chains for " + str.id);
+      for (SequenceI sq : response.getSequences())
+      {
+        assertTrue("No annotation transfered to sequence " + sq.getName(),
+                sq.getAnnotation().length > 0);
+        assertTrue("No PDBEntry on sequence " + sq.getName(),
+                sq.getAllPDBEntries().size() > 0);
+        // FIXME: should test that all residues extracted as sequences from
+        // chains in structure have a mapping to data in the structure
+        List<SequenceFeature> prev = null;
+        int lastp = -1;
+        for (int col = 1; col <= sq.getLength(); col++)
+        {
+          List<SequenceFeature> sf = sq.findFeatures(col, col, "RESNUM");
+          if (sf.size() != 1)
+          {
+            errors.append(
+                    str.id + ": " +
+                            "Expected one feature at column (position): "
+                            + (col - 1)
+                            + " (" + sq.findPosition(col - 1) + ")"
+                            + ": saw "
+                            + sf.size());
+            errors.append("\n");
+            if (prev != null)
+            {
+              errors.append("Last Feature was at position " + lastp + ": "
+                      + prev.get(0).toString());
+              errors.append("\n");
+            }
+          }
+          else
+          {
+            prev = sf;
+            lastp = sq.findPosition(col - 1);
+          }
+        }
+      }
+    }
+    if (errors.length() > 0)
+    {
+      Assert.fail(errors.toString());
     }
   }
-
 }
diff --git a/test/jalview/ws/dbsources/PfamFullTest.java b/test/jalview/ws/dbsources/PfamFullTest.java
new file mode 100644 (file)
index 0000000..f5cc640
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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.dbsources;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class PfamFullTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetURL()
+  {
+    String path = "pfam.xfam.org/family/ABC/alignment/full";
+
+    // with default value for domain
+    String url = new PfamFull().getURL(" abc ");
+    assertEquals(url, "https://" + path);
+
+    // with override in properties
+    Cache.setProperty(Pfam.PFAM_BASEURL_KEY, "http://pfam.xfam.org");
+    url = new PfamFull().getURL(" abc ");
+    assertEquals(url, "http://" + path);
+  }
+}
diff --git a/test/jalview/ws/dbsources/PfamSeedTest.java b/test/jalview/ws/dbsources/PfamSeedTest.java
new file mode 100644 (file)
index 0000000..355ef0c
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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.dbsources;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class PfamSeedTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetURL()
+  {
+    String path = "pfam.xfam.org/family/ABC/alignment/seed";
+
+    // with default value for domain
+    String url = new PfamSeed().getURL(" abc ");
+    assertEquals(url, "https://" + path);
+
+    // with override in properties
+    Cache.setProperty(Pfam.PFAM_BASEURL_KEY, "http://pfam.xfam.org");
+    url = new PfamSeed().getURL(" abc ");
+    assertEquals(url, "http://" + path);
+  }
+}
diff --git a/test/jalview/ws/dbsources/RfamFullTest.java b/test/jalview/ws/dbsources/RfamFullTest.java
new file mode 100644 (file)
index 0000000..2d1497f
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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.dbsources;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class RfamFullTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetURL()
+  {
+    String path = "rfam.xfam.org/family/ABC/alignment/full";
+
+    // with default value for domain
+    String url = new RfamFull().getURL(" abc ");
+    assertEquals(url, "https://" + path);
+
+    // with override in properties
+    Cache.setProperty(Rfam.RFAM_BASEURL_KEY, "http://rfam.xfam.org");
+    url = new RfamFull().getURL(" abc ");
+    assertEquals(url, "http://" + path);
+  }
+}
diff --git a/test/jalview/ws/dbsources/RfamSeedTest.java b/test/jalview/ws/dbsources/RfamSeedTest.java
new file mode 100644 (file)
index 0000000..745ba2e
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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.dbsources;
+
+import static org.testng.Assert.assertEquals;
+
+import jalview.bin.Cache;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class RfamSeedTest
+{
+  @BeforeClass(alwaysRun = true)
+  public void setUp()
+  {
+    Cache.loadProperties("test/jalview/io/testProps.jvprops");
+  }
+
+  @Test(groups = "Functional")
+  public void testGetURL()
+  {
+    String path = "rfam.xfam.org/family/ABC/alignment/stockholm";
+
+    // with default value for domain
+    String url = new RfamSeed().getURL(" abc ");
+    assertEquals(url, "https://" + path);
+
+    // with override in properties
+    Cache.setProperty(Rfam.RFAM_BASEURL_KEY, "http://rfam.xfam.org");
+    url = new RfamSeed().getURL(" abc ");
+    assertEquals(url, "http://" + path);
+  }
+}
index 2d4be71..c603a11 100644 (file)
@@ -63,6 +63,9 @@ public class UniprotTest
           + "<feature type=\"signal peptide\" evidence=\"7\"><location><begin position=\"1\"/><end position=\"18\"/></location></feature>"
           + "<feature type=\"propeptide\" description=\"Activation peptide\" id=\"PRO_0000027399\" evidence=\"9 16 17 18\"><location><begin position=\"19\"/><end position=\"20\"/></location></feature>"
           + "<feature type=\"chain\" description=\"Granzyme B\" id=\"PRO_0000027400\"><location><begin position=\"21\"/><end position=\"247\"/></location></feature>"
+          + "<feature type=\"sequence variant\"><original>M</original><variation>L</variation><location><position position=\"41\"/></location></feature>"
+          + "<feature type=\"sequence variant\" description=\"Pathogenic\"><original>M</original><variation>L</variation><location><position position=\"41\"/></location></feature>"
+          + "<feature type=\"sequence variant\" description=\"Pathogenic\"><original>M</original><location><position position=\"41\"/></location></feature>"
           + "<sequence length=\"10\" mass=\"27410\" checksum=\"8CB760AACF88FE6C\" modified=\"2008-01-15\" version=\"1\">MHAPL VSKDL</sequence></entry>"
           + "</uniprot>";
 
@@ -98,7 +101,7 @@ public class UniprotTest
      * Check sequence features
      */
     Vector<UniprotFeature> features = entry.getFeature();
-    assertEquals(3, features.size());
+    assertEquals(6, features.size());
     UniprotFeature sf = features.get(0);
     assertEquals("signal peptide", sf.getType());
     assertNull(sf.getDescription());
@@ -118,6 +121,27 @@ public class UniprotTest
     assertEquals(21, sf.getBegin());
     assertEquals(247, sf.getEnd());
 
+    sf = features.get(3);
+    assertEquals("sequence variant", sf.getType());
+    assertEquals("Variation: 'L' Original: 'M'", sf.getDescription());
+    assertEquals(41, sf.getPosition());
+    assertEquals(41, sf.getBegin());
+    assertEquals(41, sf.getEnd());
+
+    sf = features.get(4);
+    assertEquals("sequence variant", sf.getType());
+    assertEquals("Pathogenic Variation: 'L' Original: 'M'",
+            sf.getDescription());
+    assertEquals(41, sf.getPosition());
+    assertEquals(41, sf.getBegin());
+    assertEquals(41, sf.getEnd());
+
+    sf = features.get(5);
+    assertEquals("sequence variant", sf.getType());
+    assertEquals("Pathogenic Original: 'M'", sf.getDescription());
+    assertEquals(41, sf.getPosition());
+    assertEquals(41, sf.getBegin());
+    assertEquals(41, sf.getEnd());
     /*
      * Check cross-references
      */
@@ -163,11 +187,11 @@ public class UniprotTest
             new StringReader(UNIPROT_XML)).get(0);
 
     /*
-     * name formatted as source | accession ids | names
-     * source database converted to Jalview canonical name
+     * name formatted with Uniprot Entry name
      */
-    String expectedName = "UNIPROT|A9CKP4|A9CKP5|A9CKP4_AGRT5|A9CKP4_AGRT6";
-    assertEquals(expectedName, Uniprot.getUniprotEntryId(entry));
+    String expectedName = "A9CKP4_AGRT5|A9CKP4_AGRT6";
+    assertEquals(expectedName,
+            Uniprot.getUniprotEntryId(entry));
   }
 
   /**
index 573022d..51cff6e 100644 (file)
@@ -58,7 +58,7 @@ public class XfamFetcherTest
   @Test(groups = { "External" })
   public void testPfamFullAndSeed() throws Exception
   {
-    PfamFull pff = new PfamFull();
+    Pfam pff = new PfamFull();
     PfamSeed pfseed = new PfamSeed();
 
     AlignmentI fullpf = pff.getSequenceRecords(pff.getTestQuery());
index 40737ca..c559966 100644 (file)
@@ -47,33 +47,26 @@ public class EBIFetchClientTest
     /*
      * EMBL
      */
-    assertEquals("http://www.ebi.ac.uk/ena/data/view/x53838&display=xml",
+    assertEquals("https://www.ebi.ac.uk/ena/data/view/x53838&display=xml",
             EBIFetchClient.buildUrl("X53838", "EMBL", "display=xml"));
 
     /*
      * EMBLCDS
      */
-    assertEquals("http://www.ebi.ac.uk/ena/data/view/caa37824&display=xml",
+    assertEquals("https://www.ebi.ac.uk/ena/data/view/caa37824&display=xml",
             EBIFetchClient.buildUrl("CAA37824", "EMBL", "display=xml"));
 
     /*
-     * Uniprot
-     */
-    assertEquals(
-            "http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/uniprot/p00340/uniprotxml",
-            EBIFetchClient.buildUrl("P00340", "UNIPROT", "uniprotxml"));
-
-    /*
      * PDB / pdb
      */
-    assertEquals("http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/pdb/3a6s/pdb",
+    assertEquals("https://www.ebi.ac.uk/Tools/dbfetch/dbfetch/pdb/3a6s/pdb",
             EBIFetchClient.buildUrl("3A6S", "PDB", "pdb"));
 
     /*
      * PDB / mmCIF
      */
     assertEquals(
-            "http://www.ebi.ac.uk/Tools/dbfetch/dbfetch/pdb/3a6s/mmCIF",
+            "https://www.ebi.ac.uk/Tools/dbfetch/dbfetch/pdb/3a6s/mmCIF",
             EBIFetchClient.buildUrl("3A6S", "PDB", "mmCIF"));
   }
 
index b92766e..75c1413 100644 (file)
@@ -83,103 +83,103 @@ public class SiftsClientTest
   @BeforeTest(alwaysRun = true)
   public void populateExpectedMapping() throws SiftsException
   {
-    expectedMapping.put(51, new int[] { 1, 2 });
-    expectedMapping.put(52, new int[] { 2, 7 });
-    expectedMapping.put(53, new int[] { 3, 12 });
-    expectedMapping.put(54, new int[] { 4, 24 });
-    expectedMapping.put(55, new int[] { 5, 33 });
-    expectedMapping.put(56, new int[] { 6, 40 });
-    expectedMapping.put(57, new int[] { 7, 47 });
-    expectedMapping.put(58, new int[] { 8, 55 });
-    expectedMapping.put(59, new int[] { 9, 62 });
-    expectedMapping.put(60, new int[] { 10, 69 });
-    expectedMapping.put(61, new int[] { 11, 76 });
-    expectedMapping.put(62, new int[] { 12, 83 });
-    expectedMapping.put(63, new int[] { 13, 87 });
-    expectedMapping.put(64, new int[] { 14, 95 });
-    expectedMapping.put(65, new int[] { 15, 102 });
-    expectedMapping.put(66, new int[] { 16, 111 });
-    expectedMapping.put(67, new int[] { 17, 122 });
-    expectedMapping.put(68, new int[] { 18, 131 });
-    expectedMapping.put(69, new int[] { 19, 137 });
-    expectedMapping.put(70, new int[] { 20, 144 });
-    expectedMapping.put(71, new int[] { 21, 152 });
-    expectedMapping.put(72, new int[] { 22, 160 });
-    expectedMapping.put(73, new int[] { 23, 167 });
-    expectedMapping.put(74, new int[] { 24, 179 });
-    expectedMapping.put(75, new int[] { 25, 187 });
-    expectedMapping.put(76, new int[] { 26, 195 });
-    expectedMapping.put(77, new int[] { 27, 203 });
-    expectedMapping.put(78, new int[] { 28, 208 });
-    expectedMapping.put(79, new int[] { 29, 213 });
-    expectedMapping.put(80, new int[] { 30, 222 });
-    expectedMapping.put(81, new int[] { 31, 231 });
-    expectedMapping.put(82, new int[] { 32, 240 });
-    expectedMapping.put(83, new int[] { 33, 244 });
-    expectedMapping.put(84, new int[] { 34, 252 });
-    expectedMapping.put(85, new int[] { 35, 260 });
-    expectedMapping.put(86, new int[] { 36, 268 });
-    expectedMapping.put(87, new int[] { 37, 275 });
-    expectedMapping.put(88, new int[] { 38, 287 });
-    expectedMapping.put(89, new int[] { 39, 293 });
-    expectedMapping.put(90, new int[] { 40, 299 });
-    expectedMapping.put(91, new int[] { 41, 310 });
-    expectedMapping.put(92, new int[] { 42, 315 });
-    expectedMapping.put(93, new int[] { 43, 319 });
-    expectedMapping.put(94, new int[] { 44, 325 });
-    expectedMapping.put(95, new int[] { 45, 331 });
-    expectedMapping.put(96, new int[] { 46, 337 });
-    expectedMapping.put(97, new int[] { 47, 343 });
-    expectedMapping.put(98, new int[] { 48, 349 });
-    expectedMapping.put(99, new int[] { 49, 354 });
-    expectedMapping.put(100, new int[] { 50, 358 });
-    expectedMapping.put(101, new int[] { 51, 367 });
-    expectedMapping.put(102, new int[] { 52, 375 });
-    expectedMapping.put(103, new int[] { 53, 384 });
-    expectedMapping.put(104, new int[] { 54, 391 });
-    expectedMapping.put(105, new int[] { 55, 395 });
-    expectedMapping.put(106, new int[] { 56, 401 });
-    expectedMapping.put(107, new int[] { 57, 409 });
-    expectedMapping.put(108, new int[] { 58, 417 });
-    expectedMapping.put(109, new int[] { 59, 426 });
-    expectedMapping.put(110, new int[] { 60, 434 });
-    expectedMapping.put(111, new int[] { 61, 442 });
-    expectedMapping.put(112, new int[] { 62, 451 });
-    expectedMapping.put(113, new int[] { 63, 457 });
-    expectedMapping.put(114, new int[] { 64, 468 });
-    expectedMapping.put(115, new int[] { 65, 476 });
-    expectedMapping.put(116, new int[] { 66, 484 });
-    expectedMapping.put(117, new int[] { 67, 492 });
-    expectedMapping.put(118, new int[] { 68, 500 });
-    expectedMapping.put(119, new int[] { 69, 509 });
-    expectedMapping.put(120, new int[] { 70, 517 });
-    expectedMapping.put(121, new int[] { 71, 525 });
-    expectedMapping.put(122, new int[] { 72, 534 });
-    expectedMapping.put(123, new int[] { 73, 538 });
-    expectedMapping.put(124, new int[] { 74, 552 });
-    expectedMapping.put(125, new int[] { 75, 559 });
-    expectedMapping.put(126, new int[] { 76, 567 });
-    expectedMapping.put(127, new int[] { 77, 574 });
-    expectedMapping.put(128, new int[] { 78, 580 });
-    expectedMapping.put(129, new int[] { 79, 585 });
-    expectedMapping.put(130, new int[] { 80, 590 });
-    expectedMapping.put(131, new int[] { 81, 602 });
-    expectedMapping.put(132, new int[] { 82, 609 });
-    expectedMapping.put(133, new int[] { 83, 616 });
-    expectedMapping.put(134, new int[] { 84, 622 });
-    expectedMapping.put(135, new int[] { 85, 630 });
-    expectedMapping.put(136, new int[] { 86, 637 });
-    expectedMapping.put(137, new int[] { 87, 644 });
-    expectedMapping.put(138, new int[] { 88, 652 });
-    expectedMapping.put(139, new int[] { 89, 661 });
-    expectedMapping.put(140, new int[] { 90, 668 });
-    expectedMapping.put(141, new int[] { 91, 678 });
-    expectedMapping.put(142, new int[] { 92, 687 });
-    expectedMapping.put(143, new int[] { 93, 696 });
-    expectedMapping.put(144, new int[] { 94, 705 });
-    expectedMapping.put(145, new int[] { 95, 714 });
-    expectedMapping.put(146, new int[] { 96, 722 });
-    expectedMapping.put(147, new int[] { 97, 729 });
+    expectedMapping.put(51, new int[] { 1, 2, 1 });
+    expectedMapping.put(52, new int[] { 2, 7, 2 });
+    expectedMapping.put(53, new int[] { 3, 12, 3 });
+    expectedMapping.put(54, new int[] { 4, 24, 4 });
+    expectedMapping.put(55, new int[] { 5, 33, 5 });
+    expectedMapping.put(56, new int[] { 6, 40, 6 });
+    expectedMapping.put(57, new int[] { 7, 47, 7 });
+    expectedMapping.put(58, new int[] { 8, 55, 8 });
+    expectedMapping.put(59, new int[] { 9, 62, 9 });
+    expectedMapping.put(60, new int[] { 10, 69, 10 });
+    expectedMapping.put(61, new int[] { 11, 76, 11 });
+    expectedMapping.put(62, new int[] { 12, 83, 12 });
+    expectedMapping.put(63, new int[] { 13, 87, 13 });
+    expectedMapping.put(64, new int[] { 14, 95, 14 });
+    expectedMapping.put(65, new int[] { 15, 102, 15 });
+    expectedMapping.put(66, new int[] { 16, 111, 16 });
+    expectedMapping.put(67, new int[] { 17, 122, 17 });
+    expectedMapping.put(68, new int[] { 18, 131, 18 });
+    expectedMapping.put(69, new int[] { 19, 137, 19 });
+    expectedMapping.put(70, new int[] { 20, 144, 20 });
+    expectedMapping.put(71, new int[] { 21, 152, 21 });
+    expectedMapping.put(72, new int[] { 22, 160, 22 });
+    expectedMapping.put(73, new int[] { 23, 167, 23 });
+    expectedMapping.put(74, new int[] { 24, 179, 24 });
+    expectedMapping.put(75, new int[] { 25, 187, 25 });
+    expectedMapping.put(76, new int[] { 26, 195, 26 });
+    expectedMapping.put(77, new int[] { 27, 203, 27 });
+    expectedMapping.put(78, new int[] { 28, 208, 28 });
+    expectedMapping.put(79, new int[] { 29, 213, 29 });
+    expectedMapping.put(80, new int[] { 30, 222, 30 });
+    expectedMapping.put(81, new int[] { 31, 231, 31 });
+    expectedMapping.put(82, new int[] { 32, 240, 32 });
+    expectedMapping.put(83, new int[] { 33, 244, 33 });
+    expectedMapping.put(84, new int[] { 34, 252, 34 });
+    expectedMapping.put(85, new int[] { 35, 260, 35 });
+    expectedMapping.put(86, new int[] { 36, 268, 36 });
+    expectedMapping.put(87, new int[] { 37, 275, 37 });
+    expectedMapping.put(88, new int[] { 38, 287, 38 });
+    expectedMapping.put(89, new int[] { 39, 293, 39 });
+    expectedMapping.put(90, new int[] { 40, 299, 40 });
+    expectedMapping.put(91, new int[] { 41, 310, 41 });
+    expectedMapping.put(92, new int[] { 42, 315, 42 });
+    expectedMapping.put(93, new int[] { 43, 319, 43 });
+    expectedMapping.put(94, new int[] { 44, 325, 44 });
+    expectedMapping.put(95, new int[] { 45, 331, 45 });
+    expectedMapping.put(96, new int[] { 46, 337, 46 });
+    expectedMapping.put(97, new int[] { 47, 343, 47 });
+    expectedMapping.put(98, new int[] { 48, 349, 48 });
+    expectedMapping.put(99, new int[] { 49, 354, 49 });
+    expectedMapping.put(100, new int[] { 50, 358, 50 });
+    expectedMapping.put(101, new int[] { 51, 367, 51 });
+    expectedMapping.put(102, new int[] { 52, 375, 52 });
+    expectedMapping.put(103, new int[] { 53, 384, 53 });
+    expectedMapping.put(104, new int[] { 54, 391, 54 });
+    expectedMapping.put(105, new int[] { 55, 395, 55 });
+    expectedMapping.put(106, new int[] { 56, 401, 56 });
+    expectedMapping.put(107, new int[] { 57, 409, 57 });
+    expectedMapping.put(108, new int[] { 58, 417, 58 });
+    expectedMapping.put(109, new int[] { 59, 426, 59 });
+    expectedMapping.put(110, new int[] { 60, 434, 60 });
+    expectedMapping.put(111, new int[] { 61, 442, 61 });
+    expectedMapping.put(112, new int[] { 62, 451, 62 });
+    expectedMapping.put(113, new int[] { 63, 457, 63 });
+    expectedMapping.put(114, new int[] { 64, 468, 64 });
+    expectedMapping.put(115, new int[] { 65, 476, 65 });
+    expectedMapping.put(116, new int[] { 66, 484, 66 });
+    expectedMapping.put(117, new int[] { 67, 492, 67 });
+    expectedMapping.put(118, new int[] { 68, 500, 68 });
+    expectedMapping.put(119, new int[] { 69, 509, 69 });
+    expectedMapping.put(120, new int[] { 70, 517, 70 });
+    expectedMapping.put(121, new int[] { 71, 525, 71 });
+    expectedMapping.put(122, new int[] { 72, 534, 72 });
+    expectedMapping.put(123, new int[] { 73, 538, 73 });
+    expectedMapping.put(124, new int[] { 74, 552, 74 });
+    expectedMapping.put(125, new int[] { 75, 559, 75 });
+    expectedMapping.put(126, new int[] { 76, 567, 76 });
+    expectedMapping.put(127, new int[] { 77, 574, 77 });
+    expectedMapping.put(128, new int[] { 78, 580, 78 });
+    expectedMapping.put(129, new int[] { 79, 585, 79 });
+    expectedMapping.put(130, new int[] { 80, 590, 80 });
+    expectedMapping.put(131, new int[] { 81, 602, 81 });
+    expectedMapping.put(132, new int[] { 82, 609, 82 });
+    expectedMapping.put(133, new int[] { 83, 616, 83 });
+    expectedMapping.put(134, new int[] { 84, 622, 84 });
+    expectedMapping.put(135, new int[] { 85, 630, 85 });
+    expectedMapping.put(136, new int[] { 86, 637, 86 });
+    expectedMapping.put(137, new int[] { 87, 644, 87 });
+    expectedMapping.put(138, new int[] { 88, 652, 88 });
+    expectedMapping.put(139, new int[] { 89, 661, 89 });
+    expectedMapping.put(140, new int[] { 90, 668, 90 });
+    expectedMapping.put(141, new int[] { 91, 678, 91 });
+    expectedMapping.put(142, new int[] { 92, 687, 92 });
+    expectedMapping.put(143, new int[] { 93, 696, 93 });
+    expectedMapping.put(144, new int[] { 94, 705, 94 });
+    expectedMapping.put(145, new int[] { 95, 714, 95 });
+    expectedMapping.put(146, new int[] { 96, 722, 96 });
+    expectedMapping.put(147, new int[] { 97, 729, 97 });
   }
 
   @BeforeTest(alwaysRun = true)
@@ -311,7 +311,7 @@ public class SiftsClientTest
     atom.atomIndex = 7;
     atoms.add(atom);
     int actualAtomIndex = siftsClient.getAtomIndex(1, atoms);
-    Assert.assertEquals(actualAtomIndex, -1);
+    Assert.assertEquals(actualAtomIndex, siftsClient.UNASSIGNED);
     actualAtomIndex = siftsClient.getAtomIndex(43, atoms);
     Assert.assertEquals(actualAtomIndex, 7);
   }
index abe9d4b..ced9ac0 100755 (executable)
@@ -314,13 +314,13 @@ and any path to a file to save to the file]]></string>
                                                                <string><![CDATA[logo.gif]]></string>
                                                        </property>
                                                        <property name="smallIconPath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/resources/images/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/resources/images/]]></string>
                                                        </property>
                                                        <property name="largeIconPath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/resources/images/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/resources/images/]]></string>
                                                        </property>
                                                        <property name="macOSXIconPath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
                                                        </property>
                                                        <property name="macOSXIconName">
                                                                <string><![CDATA[mac_logo.icns]]></string>
@@ -367,7 +367,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/dist/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/dist/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -419,7 +419,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -471,7 +471,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -523,7 +523,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -575,7 +575,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -627,7 +627,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -679,7 +679,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -731,7 +731,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -783,7 +783,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -835,7 +835,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -887,7 +887,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -939,7 +939,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -991,7 +991,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1043,7 +1043,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1095,7 +1095,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1147,7 +1147,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1199,7 +1199,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1251,7 +1251,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1303,7 +1303,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1355,7 +1355,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1407,7 +1407,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1459,7 +1459,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1511,7 +1511,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1563,7 +1563,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1615,7 +1615,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1667,7 +1667,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1719,7 +1719,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1771,7 +1771,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1823,7 +1823,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1875,7 +1875,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1927,7 +1927,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -1979,7 +1979,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2025,13 +2025,13 @@ and any path to a file to save to the file]]></string>
                                                                <string><![CDATA[664]]></string>
                                                        </property>
                                                        <property name="sourceName">
-                                                               <string><![CDATA[groovy-all-2.4.6-indy.jar]]></string>
+                                                               <string><![CDATA[groovy-all-2.4.12-indy.jar]]></string>
                                                        </property>
                                                        <property name="overrideUnixPermissions">
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2043,7 +2043,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>true</boolean>
                                                        </property>
                                                        <property name="destinationName">
-                                                               <string><![CDATA[groovy-all-2.4.6-indy.jar]]></string>
+                                                               <string><![CDATA[groovy-all-2.4.12-indy.jar]]></string>
                                                        </property>
                                                        <property name="fileSize">
                                                                <long>6149494</long>
@@ -2083,7 +2083,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2135,7 +2135,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2187,7 +2187,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2239,7 +2239,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2291,7 +2291,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2320,6 +2320,58 @@ and any path to a file to save to the file]]></string>
                                                </object>
                                        </method>
                                        <method name="addElement">
+            <object class="com.zerog.ia.installer.actions.InstallZipfile" objectID="9a1f46efeef911a">
+              <property name="belongsToUninstallPhase">
+                <boolean>false</boolean>
+              </property>
+              <property name="rollbackEnabledCancel">
+                <boolean>true</boolean>
+              </property>
+              <property name="rollbackEnabledError">
+                <boolean>true</boolean>
+              </property>
+              <property name="ruleExpression">
+                <string><![CDATA[]]></string>
+              </property>
+              <property name="unixPermissions">
+                <string><![CDATA[664]]></string>
+              </property>
+              <property name="sourceName">
+                <string><![CDATA[VAqua4.jar]]></string>
+              </property>
+              <property name="overrideUnixPermissions">
+                <boolean>false</boolean>
+              </property>
+              <property name="sourcePath">
+                <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
+              </property>
+              <property name="shouldUninstall">
+                <boolean>true</boolean>
+              </property>
+              <property name="rollbackEnabledCancel">
+                <boolean>true</boolean>
+              </property>
+              <property name="rollbackEnabledError">
+                <boolean>true</boolean>
+              </property>
+              <property name="destinationName">
+                <string><![CDATA[VAqua4.jar]]></string>
+              </property>
+              <property name="fileSize">
+                <long>1355141</long>
+              </property>
+              <property name="macBinary">
+                <boolean>false</boolean>
+              </property>
+              <property name="targetCheckKind">
+                <int>0</int>
+              </property>
+              <property name="ruleExpression">
+                <string><![CDATA[]]></string>
+              </property>
+            </object>
+          </method>
+          <method name="addElement">
                                                <object class="com.zerog.ia.installer.actions.InstallZipfile" objectID="1936efeefab93">
                                                        <property name="belongsToUninstallPhase">
                                                                <boolean>false</boolean>
@@ -2343,7 +2395,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2395,7 +2447,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2447,7 +2499,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2499,7 +2551,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2551,7 +2603,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2603,7 +2655,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2655,7 +2707,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2707,7 +2759,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2759,7 +2811,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2811,7 +2863,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2863,7 +2915,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2915,7 +2967,7 @@ and any path to a file to save to the file]]></string>
                                                                <boolean>false</boolean>
                                                        </property>
                                                        <property name="sourcePath">
-                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib/]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib/]]></string>
                                                        </property>
                                                        <property name="shouldUninstall">
                                                                <boolean>true</boolean>
@@ -2949,7 +3001,7 @@ and any path to a file to save to the file]]></string>
                                <string><![CDATA[The installer cannot run on your configuration. It will now quit.]]></string>
                        </property>
                        <property name="userSplashPath">
-                               <string><![CDATA[/home/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
+                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
                        </property>
                        <property name="userSplashName">
                                <string><![CDATA[jalview.gif]]></string>
@@ -4242,7 +4294,7 @@ Press "Done" to quit the installer.]]></string>
                                                                <boolean>true</boolean>
                                                        </property>
                                                        <property name="buildOutputLocation">
-                                                               <string><![CDATA[/opt/homes/cruisecontrol/live/cruisecontrol/checkout/next-release-jalview/utils/InstallAnywhere/Jalview_Build_Output]]></string>
+                                                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/Jalview_Build_Output]]></string>
                                                        </property>
                                                        <property name="relatedProjectSettings">
                                                                <object class="com.zerog.ia.installer.RelatedProjectSettings" objectID="97f2363da6ac">
@@ -5270,7 +5322,7 @@ Press "Done" to quit the installer.]]></string>
                                        </method>
                                        <method name="put">
                                                <string><![CDATA[com.zerog.ia.installer.options.valid.vm.list]]></string>
-                                               <string><![CDATA[1.8+]]></string>
+                                               <string><![CDATA[1.8*]]></string>
                                        </method>
                                        <method name="put">
                                                <string><![CDATA[com.zerog.ia.project.build.last.date]]></string>
@@ -5366,7 +5418,7 @@ Press "Done" to quit the installer.]]></string>
                                        </method>
                                        <method name="put">
                                                <string><![CDATA[com.zerog.ia.installer.options.platform.macosx.vm.version]]></string>
-                                               <string><![CDATA[1.8+]]></string>
+                                               <string><![CDATA[1.8*]]></string>
                                        </method>
                                        <method name="put">
                                                <string><![CDATA[com.zerog.ia.build.platform.java.novm]]></string>
@@ -5406,7 +5458,7 @@ Press "Done" to quit the installer.]]></string>
                                                <boolean>true</boolean>
                                        </property>
                                        <property name="backgroundImagePath">
-                                               <string><![CDATA[/home/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
+                                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
                                        </property>
                                        <property name="backgroundImageName">
                                                <string><![CDATA[align.gif]]></string>
@@ -7230,6 +7282,7 @@ and any path to a file to read from that file]]></string>
                                                                                <object refID="24485f8ca673"/>
                                                                                <object refID="24485f8ba674"/>
                                                                                <object refID="24485f8ca674"/>
+                                                                               <object refID="9a1f46efeef911a"/>
                                                                                <object class="com.zerog.ia.installer.actions.CreateShortcut" objectID="3cd8e2ffa672">
                                                                                        <property name="belongsToUninstallPhase">
                                                                                                <boolean>false</boolean>
@@ -7291,7 +7344,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7332,7 +7385,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7373,7 +7426,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/doc/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/doc/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7445,7 +7498,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7486,7 +7539,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7527,7 +7580,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7568,7 +7621,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7609,7 +7662,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7659,7 +7712,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7700,7 +7753,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7741,7 +7794,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7782,7 +7835,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7823,7 +7876,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7864,7 +7917,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7905,7 +7958,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7946,7 +7999,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/lib]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/lib]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -7981,6 +8034,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <object refID="24485f8ca673"/>
                                                                                                <object refID="24485f8ba674"/>
                                                                                                <object refID="24485f8ca674"/>
+                                                                                               <object refID="9a1f46efeef911a"/>
                                                                                                <object refID="b1a16838a449"/>
                                                                                                <object refID="b1a16839a449"/>
                                                                                                <object refID="495aeddb8b3d"/>
@@ -8036,7 +8090,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples/groovy]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples/groovy]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -8077,7 +8131,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/home/cruisecontrol/jalview/examples]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/examples]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>false</boolean>
@@ -8133,7 +8187,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/opt/homes/cruisecontrol/live/cruisecontrol/checkout/next-release-jalview/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -8174,7 +8228,7 @@ and any path to a file to read from that file]]></string>
                                                                                                <boolean>false</boolean>
                                                                                        </property>
                                                                                        <property name="sourcePath">
-                                                                                               <string><![CDATA[/opt/homes/cruisecontrol/live/cruisecontrol/checkout/next-release-jalview/]]></string>
+                                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/]]></string>
                                                                                        </property>
                                                                                        <property name="shouldUninstall">
                                                                                                <boolean>true</boolean>
@@ -8359,7 +8413,7 @@ and any path to a file to read from that file]]></string>
                                                                                <string><![CDATA[]]></string>
                                                                        </property>
                                                                        <property name="imagePath">
-                                                                               <string><![CDATA[/home/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
+                                                                               <string><![CDATA[/homes/cruisecontrol/jalview/utils/InstallAnywhere/]]></string>
                                                                        </property>
                                                                        <property name="imageName">
                                                                                <string><![CDATA[bartonGroup.gif]]></string>
index 6310934..6f0115e 100644 (file)
@@ -20,7 +20,7 @@
 <project name="jalviewInstallAnywhere" default="build" basedir=".">
   <property name="IA_LOCATION" value="/home/cruisecontrol/InstallAnywhere 2013/"/>
   <property name="IA_PROJECT" location="Jalview.iap_xml"/>
-  <property name="ABS_PATH" value="/home/cruisecontrol/jalview"/> <!-- \/utils\/InstallAnywhere"/> --> <!--/home/cruisecontrol/jalview"/> -->
+  <property name="ABS_PATH" value="/homes/cruisecontrol/jalview"/> <!-- \/utils\/InstallAnywhere"/> --> <!--/home/cruisecontrol/jalview"/> -->
   <!-- location of top level of jalview distribution directory -->
   <property name="CUR_PATH" location="../../." />
   <property name="USER_HOME" location="~" />
index 4489a93..c870f6d 100644 (file)
@@ -24,6 +24,7 @@ import java.io.FileReader;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TreeSet;
 import java.util.regex.Pattern;
 
@@ -89,7 +90,9 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
 
   private int javaCount;
 
-  private HashSet<String> invalidKeys;
+  private Set<String> invalidKeys;
+
+  private Set<String> dynamicKeys;
 
   /**
    * Runs the scan given the path to the root of Java source directories
@@ -125,7 +128,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
   private void doMain(String srcPath) throws IOException
   {
     System.out.println("Scanning " + srcPath
-            + " for calls to MessageManager");
+            + " for calls to MessageManager\n");
     sourcePath = srcPath;
     loadMessages();
     File dir = new File(srcPath);
@@ -134,7 +137,10 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
       System.out.println(srcPath + " not found");
       return;
     }
-    invalidKeys = new HashSet<String>();
+
+    invalidKeys = new HashSet<>();
+    dynamicKeys = new HashSet<>();
+
     if (dir.isDirectory())
     {
       scanDirectory(dir);
@@ -152,17 +158,60 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
   private void reportResults()
   {
     System.out.println("\nScanned " + javaCount + " source files");
-    System.out.println("Message.properties has " + messages.size()
+    System.out.println(
+            "Messages.properties has " + messages.size()
             + " keys");
-    System.out.println("Found " + invalidKeys.size()
-            + " possibly invalid parameter calls");
+    if (!invalidKeys.isEmpty())
+    {
+      System.out.println("Found " + invalidKeys.size()
+              + " possibly invalid parameter call"
+              + (invalidKeys.size() > 1 ? "s" : ""));
+    }
 
-    System.out.println(messageKeys.size()
-            + " keys not found, either unused or constructed dynamically");
+    System.out.println("Keys not found, assumed constructed dynamically:");
+    int dynamicCount = 0;
     for (String key : messageKeys)
     {
-      System.out.println("    " + key);
+      if (isDynamic(key))
+      {
+        System.out.println("    " + key);
+        dynamicCount++;
+      }
+    }
+
+    if (dynamicCount < messageKeys.size())
+    {
+      System.out.println((messageKeys.size() - dynamicCount)
+              + " keys not found, possibly unused");
+      for (String key : messageKeys)
+      {
+        if (!isDynamic(key))
+        {
+          System.out.println("    " + key);
+        }
+      }
+    }
+    System.out
+            .println("(Run i18nAnt.xml to compare other message bundles)");
+  }
+
+  /**
+   * Answers true if the key starts with one of the recorded dynamic key stubs,
+   * else false
+   * 
+   * @param key
+   * @return
+   */
+  private boolean isDynamic(String key)
+  {
+    for (String dynamic : dynamicKeys)
+    {
+      if (key.startsWith(dynamic))
+      {
+        return true;
+      }
     }
+    return false;
   }
 
   /**
@@ -275,14 +324,17 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
         continue;
       }
 
+      String messageKey = getMessageKey(method, methodArgs);
+
       if (METHOD3 == method)
       {
         System.out.println(String.format("Dynamic key at %s line %s %s",
                 path.substring(sourcePath.length()), lineNos, line));
+        String key = messageKey.substring(1, messageKey.length() - 1);
+        dynamicKeys.add(key);
         continue;
       }
 
-      String messageKey = getMessageKey(method, methodArgs);
       if (messageKey == null)
       {
         System.out.println(String.format("Trouble parsing %s line %s %s",
@@ -370,7 +422,7 @@ public class MessageBundleChecker implements BufferedLineReader.LineCleaner
     messages.load(reader);
     reader.close();
 
-    messageKeys = new TreeSet<String>();
+    messageKeys = new TreeSet<>();
     for (Object key : messages.keySet())
     {
       messageKeys.add((String) key);
index ac9e260..122b8d0 100644 (file)
     <!-- 
        Suppress check of externally sourced code 
     --> 
-    <suppress checks="[a-zA-Z0-9]*" files="com[\\/]*"/>
-    <suppress checks="[a-zA-Z0-9]*" files="ext[\\/]*"/>
-    <suppress checks="[a-zA-Z0-9]*" files="org[\\/]*"/>
-    <suppress checks="[a-zA-Z0-9]*" files="uk[\\/]*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]com[\\/]github*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]com[\\/]stevesoft*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]ext[\\/]edu*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]ext[\\/]vamsas*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]org[\\/]jibble*"/>
+    <suppress checks="[a-zA-Z0-9]*" files="[\\/]uk[\\/]ac*"/>
     
     <!-- 
        ImportControl can only handle one top level package
index b41aab3..c47aaec 100644 (file)
                <subpackage name="datamodel">
                <disallow pkg="jalview.gui"/>
                <allow pkg="fr.orsay.lri.varna"/>
-                       <subpackage name="xdb">
-                               <subpackage name="embl">
-                               <allow pkg="org.exolab.castor"/>
-                           </subpackage>
+                       <subpackage name="xdb.embl">
+                       <allow pkg="org.exolab.castor"/>
                    </subpackage>
            </subpackage>
                
+               <subpackage name="ext">
+                       <subpackage name="ensembl">
+                       <allow pkg="javax.ws"/>
+                       <allow pkg="org.json"/>
+                       </subpackage>
+                       <subpackage name="htsjdk">
+                       <allow pkg="htsjdk"/>
+                       </subpackage>
+                       <subpackage name="jmol">
+                       <allow pkg="MCview"/>
+                       <allow pkg="org.jmol"/>
+                       </subpackage>
+                       <subpackage name="paradise">
+                       <allow pkg="org.apache"/>
+                       <allow pkg="org.json"/>
+                       </subpackage>
+                       <subpackage name="rbvi">
+                       <allow pkg="ext.edu.ucsf"/>
+                       <allow pkg="javax.servlet"/>
+                       </subpackage>
+                       <subpackage name="so">
+                       <allow pkg="org.biojava"/>
+                       </subpackage>
+                       <subpackage name="varna">
+                       <allow pkg="fr.orsay"/>
+                       </subpackage>
+           </subpackage>
+               
                <subpackage name="fts">
                <allow pkg="javax.swing"/>
                <allow pkg="javax.ws"/>
                <allow pkg="javax.servlet"/>
                </subpackage>
 
+               <subpackage name="schemes">
+                       <allow pkg="org.exolab.castor" class="jalview.schemes.ColourSchemeLoader"/>
+               </subpackage>
+
                <subpackage name="structure">
                <allow pkg="MCview"/>
                </subpackage>
+               
+               <subpackage name="urls">
+                       <allow pkg="javax.swing" class="jalview.urls.UrlLinkTableModel"/>
+                       <allow pkg="org.json"/>
+               </subpackage>
 
                <subpackage name="util">
                <allow pkg="javax.swing"/>
diff --git a/utils/proguard.jar b/utils/proguard.jar
deleted file mode 100755 (executable)
index dfb7f29..0000000
Binary files a/utils/proguard.jar and /dev/null differ
diff --git a/utils/proguard_5.3.3.jar b/utils/proguard_5.3.3.jar
new file mode 100755 (executable)
index 0000000..08f4a4c
Binary files /dev/null and b/utils/proguard_5.3.3.jar differ